
개발자라면 클린 전반적으로 지켜지는 클린코드 규약을 따라야 하는데요. 파이선 개발에 있어서 기본적인 규칙을 따라야 한다는 클린코드의 관점을 파이썬스러움(Pythonic Code)이라고 표현을 합니다.
이런 파이선 특유의 명칭이 생긴 이유는 다른 언어와 차별화 된 파이선만의 기능을 최대한 활용해서 깨끗한 코드를 만들 수 있기 때문인데요.
이제부터 각각의 파이썬스러운 코드 규칙에 대해 설명하도록 하겠습니다.
1. join, split을 활용해 파이서닉 스러운 string, list 만들기
파이써닉하지 못한 join: 문장 끝에 필요없는 스페이스가 추가되는 단점, 선언 변수가 생기고 줄이 길어진다
# Non-Pythonic
words = ['python', 'is', 'fun']
sentence = ''
for word in words:
sentence += word + ' '
파이서닉 한 join:
# Pythonic
words = ['python', 'is', 'fun']
sentence = ' '.join(words)
print(sentence) # Output: python is fun
파이서닉 한 split(): 일반 케이스
sentence = "this is a sentence with irregular spacing"
words = sentence.split()
print(words)
# Output: ['this', 'is', 'a', 'sentence', 'with', 'irregular', 'spacing']
파이서닉 한 split(): delimiter 활용
csv_data = "apple,banana,cherry,date"
fruits = csv_data.split(',')
print(fruits)
# Output: ['apple', 'banana', 'cherry', 'date']
파이서닉 한 split(): split 수 제한, 제한을 넘어선 문장은 뭉쳐있다.
log_line = "INFO:user_login:user_id=123:details=successful"
parts = log_line.split(':', maxsplit=2)
print(parts)
# Output: ['INFO', 'user_login', 'user_id=123:details=successful']
파이서닉 한 split(): 정규표현식을 쓴 split
import re
text = "apple,banana;cherry orange"
items = re.split(r'[;, ]+', text)
print(items)
# Output: ['apple', 'banana', 'cherry', 'orange']
파이서닉 한 split(): 청크 단위로 Split 하기
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
chunk_size = 3
chunks = [data[i:i + chunk_size] for i in range(0, len(data), chunk_size)]
print(chunks)
# Output: [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]
2. list comprehension, dictionary comprehension
컴프리헨션은 파이선에서 특정 데이터의 조합과 구성을 리스트와 딕셔너리로 효율적이고 보다 간결하게 만들기 위해 사용되는 기법이다.
파이써닉하지 못한 list 제작: for룹을 돌리는 문작을 작성하므로 코드가 길어진다
# Non-Pythonic
squares = []
for i in range(10):
squares.append(i * i)
print(squares)
# Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
파이서닉 한 list comprehension
# Pythonic with a condition
even_squares = [i * i for i in range(10) if i % 2 == 0]
print(even_squares)
# Output: [0, 4, 16, 36, 64]
파이써닉하지 못한 dictionary 제작: for룹을 돌리는 문작을 작성하므로 코드가 길어진다
# Non-Pythonic
numbers = [1, 2, 3, 4]
squares_dict = {}
for num in numbers:
squares_dict[num] = num**2
print(squares_dict) # Output: {1: 1, 2: 4, 3: 9, 4: 16}
파이서닉 한 dictionary comprehension 제작
# Pythonic
numbers = [1, 2, 3, 4]
squares_dict = {num: num**2 for num in numbers}
print(squares_dict) # Output: {1: 1, 2: 4, 3: 9, 4: 16}
3. Iteration & Loop
List나 Dictionary와 같은 데이터 집합을 순회할 때 사용하는 기법
파이써닉하지 못한 list 순회: range에 len을 사용하여 코드를 길게 만드는 단점
# Non-Pythonic
fruits = ['apple', 'banana', 'cherry']
for i in range(len(fruits)):
print(fruits[i])
파이서닉 한 list 순회
# Pythonic
fruits = ['apple', 'banana', 'cherry']
for fruit in fruits:
print(fruit)
파이서닉 한 list 순회 Enumerate 활용
# Pythonic with index
for index, fruit in enumerate(fruits):
print(f"Fruit at index {index} is {fruit}")
파이서닉 한 list 순회: 두개의 list를 동시 순차할 때 zip 활용
names = ["김승윤", "이서아", "박도윤"]
roles = ["개발자", "디자이너", "기획자"]
for index, (name, role) in enumerate(zip(names, roles), start=1):
print(f"팀원 {index}: {name} ({role})")
4. Asterisk
함수 정의에서 *를 단독으로 사용하여, 그 뒤에 오는 인수들은 반드시 키워드로만 전달하도록 강제하는 기능입니다. 이는 함수의 가독성과 명확성을 높여줍니다.
파이써닉하지 못한 예시: 인수를 리스트로 받기
def calculate_sum(numbers_list):
total = 0
for num in numbers_list:
total += num
return total
# 함수를 호출할 때마다 리스트를 만들어야 함
print(calculate_sum([1, 2, 3]))
print(calculate_sum([10, 20, 30, 40, 50]))
파이서닉 한 예: 키워드 전용 인수 강제
def create_user_pythonic(name, age, *, is_admin, notify_by_email):
print(f"{name}({age})님 생성. 관리자: {is_admin}, 이메일 알림: {notify_by_email}")
# create_user_pythonic("허진호", 29, True, False) -> TypeError 발생!
# 반드시 키워드로 전달해야 하므로 코드가 명확해짐
create_user_pythonic("허진호", 29, is_admin=True, notify_by_email=False)
6. Variadic Arguments
가변 인수란, 함수를 호출할 때 넘겨주는 인수의 개수가 정해져 있지 않고 유동적으로 변할 수 있다는 의미입니다.
방법1: Positional Arguments - 정해지지 않은 수의위치 인수들을 튜플(tuple)로 묶어서 받습니다.
파이써닉하지 못한 예시: 가변 인수를 처리하기 위해 리스트나 튜플을 단일 인수로 받는 방식입니다. 함수를 호출할 때마다 매번 리스트를 생성해야 해서 번거롭습니다.
def sum_numbers(numbers_list):
# 함수는 리스트 하나만 인수로 받습니다.
total = 0
for num in numbers_list:
total += num
return total
# 호출할 때 리스트를 직접 만들어 전달해야 합니다.
result = sum_numbers([1, 2, 3, 4, 5])
print(f"합계: {result}")
파이서닉 한 예: *args를 사용하면 함수를 호출할 때 인수를 쉼표로 구분하여 자유롭게 전달할 수 있습니다. 파이썬이 이 인수들을 알아서 하나의 튜플로 묶어줍니다.
def sum_numbers_pythonic(*args):
# args는 (1, 2, 3, 4, 5) 형태의 튜플이 됩니다.
print(f"받은 인수들: {args}")
return sum(args)
# 인수를 원하는 만큼 자유롭게 전달할 수 있습니다.
result = sum_numbers_pythonic(1, 2, 3, 4, 5)
print(f"합계: {result}")
방법2: 키워드 인수 (Keyword Arguments) 처리 - 정해지지 않은 수의 키워드 인수들을 딕셔너리(dictionary)로 묶어서 받습니다.
파이써닉하지 못한 예시: 함수가 딕셔너리 하나를 인수로 받아 내부에서 처리하는 방식입니다. 호출할 때마다 딕셔너리를 만들어야 합니다.
def display_profile(profile_dict):
# 함수는 딕셔너리 하나만 인수로 받습니다.
print("--- 사용자 정보 ---")
for key, value in profile_dict.items():
print(f"{key}: {value}")
# 호출할 때 딕셔너리를 직접 만들어 전달해야 합니다.
user_info = {'name': '유수진', 'email': 'dev@example.com', 'level': 5}
display_profile(user_info)
파이서닉 한 예: **kwargs를 사용하면 이름=값 형태의 키워드 인수를 자유롭게 전달할 수 있습니다. 파이썬이 이 인수들을 알아서 하나의 딕셔너리로 묶어줍니다.
def display_profile_pythonic(**kwargs):
# kwargs는 {'name': '유수진', ...} 형태의 딕셔너리가 됩니다.
print(f"받은 인수들: {kwargs}")
print("--- 사용자 정보 ---")
for key, value in kwargs.items():
print(f"{key}: {value}")
# 키워드 인수를 원하는 만큼 자유롭게 전달할 수 있습니다.
display_profile_pythonic(name='유수진', email='dev@example.com', level=5, status='active')
7. 컨테이너 타입 데이터 Unpacking
언패킹은 컨테이너 안의 요소들을 풀어서 여러 변수에 나누어 담거나, 함수의 인수로 전달하는 과정을 의미합니다.
파이써닉하지 못한 예시: 컨테이너의 각 요소를 인덱스나 키를 이용해 수동으로 하나씩 꺼내서 함수에 전달하는 방식입니다. 코드가 길어지고 컨테이너의 크기가 바뀌면 코드를 수정해야 합니다.
def create_rgb(red, green, blue):
"""RGB 값을 받아 색상 코드를 출력하는 함수"""
print(f"R:{red}, G:{green}, B:{blue}")
# 리스트 언패킹 (못한 방식)
color_list = [255, 165, 0] # 주황색
create_rgb(color_list[0], color_list[1], color_list[2])
# 딕셔너리 언패킹 (못한 방식)
color_dict = {'red': 65, 'green': 105, 'blue': 225} # 로얄 블루
create_rgb(red=color_dict['red'], green=color_dict['green'], blue=color_dict['blue'])
파이서닉 한 예:
def create_rgb_pythonic(red, green, blue):
"""RGB 값을 받아 색상 코드를 출력하는 함수"""
print(f"R:{red}, G:{green}, B:{blue}")
# 리스트/튜플 언패킹
color_list = [255, 165, 0]
create_rgb_pythonic(*color_list) # * 하나를 붙이면 알아서 풀어서 전달
# 딕셔너리 언패킹
color_dict = {'red': 65, 'green': 105, 'blue': 225}
create_rgb_pythonic(**color_dict) # ** 두개를 붙이면 키워드 인수로 풀어서 전달
# (딕셔너리의 키가 함수의 매개변수 이름과 일치해야 합니다)
8. 파일과 리소스 다루기
파이써닉하지 못한 예시:
# Non-Pythonic
f = open('my_file.txt', 'w')
try:
f.write('Hello, world!')
finally:
f.close() # You must remember this!
파이서닉 한 예:
# Pythonic
with open('my_file.txt', 'w') as f:
f.write('Hello, world!')
9. Dictionary 참조
파이써닉하지 못한 예시:
# Non-Pythonic
data = {'name': 'Alice', 'age': 30}
if 'city' in data:
city = data['city']
else:
city = 'Unknown'
print(city) # Output: Unknown
파이서닉 한 예:
# Pythonic
data = {'name': 'Alice', 'age': 30}
city = data.get('city', 'Unknown')
print(city) # Output: Unknown
10. 변수 스와핑
파이써닉하지 못한 예시:
# Non-Pythonic
a = 5
b = 10
temp = a
a = b
b = temp
print(f"a = {a}, b = {b}") # Output: a = 10, b = 5
파이서닉 한 예:
# Pythonic
a = 5
b = 10
a, b = b, a
print(f"a = {a}, b = {b}") # Output: a = 10, b = 5
11. Chained 비교문
파이써닉하지 못한 예시:
# Non-Pythonic
age = 25
if age > 18 and age < 30:
print("You are in your 20s.")
파이서닉 한 예:
# Pythonic
age = 25
if 18 < age < 30:
print("You are in your 20s.")
12. Ask for Forgiveness, Not Permission (EAFP)
파이써닉하지 못한 예시:
# Non-Pythonic (LBYL)
data = {'name': 'Alice', 'age': 30}
# We check if the key 'job' exists before using it
if 'job' in data and data['job'] is not None:
print(data['job'])
else:
print("No job specified.")
파이서닉 한 예:
# Pythonic (EAFP)
data = {'name': 'Alice', 'age': 30}
try:
print(data['job'])
except KeyError:
print("No job specified.")
13. 빈 리스트 비교
일반적으로 파이선은 None과 ""의 구분을 명확히 해야하는데요. 이 둘은 다른 두가지 형태를 비교하는 구문이기 때문입니다.
- variable is None: 변수가 '값이 없음'이라는 특별한 상태인지 확인합니다.
- variable == "": 변수가 '비어있는 문자열'이라는 값을 가지고 있는지 확인합니다.
None은 파이썬에서 "값이 없음"을 나타내는 유일무이한 싱글턴(singleton) 객체입니다. 프로그램 전체에서 None은 단 하나만 존재합니다.
is 연산자는 두 변수가 같은 메모리 주소를 가리키는 동일한 객체인지를 확인하는 식별성(identity) 비교입니다. None은 항상 유일한 객체이므로, None인지 확인할 때는 is를 사용하는 것이 가장 정확하고 빠릅니다.
파이써닉하지 못한 예시:
my_var = None
if my_var == None: # 작동은 하지만, Pythonic하지 않음
print("변수가 None과 '값이' 같습니다.")
파이서닉 한 예:
my_var = None
if my_var is None: # None을 확인할 때는 반드시 is를 사용합니다.
print("변수가 None 객체 '자체'입니다.")
비어있는 객체 확인하기
파이써닉하지 못한 예시:
my_list = []
if len(my_list) == 0:
print("리스트가 비어있습니다. (len() 사용)")
파이서닉 한 예:
my_list = []
if not my_list:
print("리스트가 비어있습니다. (Pythonic 방식)")
파이썬에서는 비어있는 컨테이너 타입을 암시적으로 False로 취급합니다. 이를 "Falsy"하다고 표현합니다. 반대로 내용이 하나라도 있으면 "Truthy"하다고 하여 True로 취급합니다.
이 원리를 이용하면 if not my_list: 라는 코드는 "my_list가 비어있다면(False라면)" 이라는 의미가 되어, len()을 호출할 필요 없이 훨씬 간결하고 직관적인 코드가 완성됩니다.
다양한 "Falsy" 값들: 이 규칙은 다른 여러 값에도 동일하게 적용됩니다.
- [] (빈 리스트)
- () (빈 튜플)
- {} (빈 딕셔너리)
- "" (빈 문자열)
- 0 (숫자 0)
- None
따라서 비어있는 문자열을 확인할 때도 if not my_string:을 사용할 수 있습니다.
14. 메모리 최적화를 위한 Generator Expression
파이써닉하지 못한 예시:
# Non-Pythonic (for large data)
# This creates a list with one million numbers in memory
total = sum([i * i for i in range(1000000)])
print(total)
파이서닉 한 예:
# Pythonic
# This creates a generator object that yields values on demand
total = sum(i * i for i in range(1000000))
print(total)
15. 함수에 list, dictionary 값의 매개변수를 전달 할 때
파이써닉하지 못한 예시:
# Non-Pythonic
def move_character(x, y, z):
print(f"Moving to coordinates ({x}, {y}, {z})")
# Positional arguments from a list
coords_list = [10, 20, 5]
move_character(coords_list[0], coords_list[1], coords_list[2])
# Keyword arguments from a dictionary
coords_dict = {'x': 10, 'y': 20, 'z': 5}
move_character(x=coords_dict['x'], y=coords_dict['y'], z=coords_dict['z'])
파이서닉 한 예:
# Pythonic
def move_character(x, y, z):
print(f"Moving to coordinates ({x}, {y}, {z})")
# Unpacking a list for positional arguments
coords_list = [10, 20, 5]
move_character(*coords_list)
# Unpacking a dictionary for keyword arguments
coords_dict = {'x': 10, 'y': 20, 'z': 5}
move_character(**coords_dict)
16. Mutable 기본 매개변수의 위험성 제어
파이써닉하지 못한 예시:
# Non-Pythonic (Dangerous!)
def add_item(item, my_list=[]):
my_list.append(item)
return my_list
print(add_item(1)) # Output: [1]
print(add_item(2)) # Output: [1, 2] <-- Unexpected!
print(add_item(3)) # Output: [1, 2, 3] <-- The list persists across calls.
파이서닉 한 예:
# Pythonic
def add_item(item, my_list=None):
if my_list is None:
my_list = [] # Create a new list only when needed
my_list.append(item)
return my_list
print(add_item(1)) # Output: [1]
print(add_item(2)) # Output: [2]
print(add_item(3, [10, 20])) # Output: [10, 20, 3]
17. 사용하지 않는 변수는 _으로 처리하기
파이써닉하지 못한 예시:
# Non-Pythonic
person = ('John', 'Doe', 42)
first, last, dummy = person
print(f"{first} {last}")
for unused in range(5):
print("Hello!")
파이서닉 한 예:
# Pythonic
person = ('John', 'Doe', 42)
first, last, _ = person # We don't need the age
print(f"{first} {last}")
for _ in range(5): # We don't need the loop counter
print("Hello!")
18. 그룹화 할 때 collections.defaultdict 사용하기
파이써닉하지 못한 예시:
# Non-Pythonic
fruits = [('apple', 'red'), ('banana', 'yellow'), ('apple', 'green'), ('cherry', 'red')]
grouped = {}
for fruit, color in fruits:
if color not in grouped:
grouped[color] = [] # Initialize the list for a new color
grouped[color].append(fruit)
print(grouped)
# Output: {'red': ['apple', 'cherry'], 'yellow': ['banana'], 'green': ['apple']}
파이서닉 한 예:
# Pythonic
from collections import defaultdict
fruits = [('apple', 'red'), ('banana', 'yellow'), ('apple', 'green'), ('cherry', 'red')]
grouped = defaultdict(list) # The factory for new keys is 'list'
for fruit, color in fruits:
grouped[color].append(fruit) # No check needed!
print(grouped)
# Output: defaultdict(<class 'list'>, {'red': ['apple', 'cherry'], 'yellow': ['banana'], 'green': ['apple']})
19. 조합을 구할 때 itertools 활용하기
파이써닉하지 못한 예시:
# Non-Pythonic
team_a = ['Alice', 'Bob']
team_b = ['Charlie', 'David']
pairings = []
for p1 in team_a:
for p2 in team_b:
pairings.append((p1, p2))
print(pairings)
# Output: [('Alice', 'Charlie'), ('Alice', 'David'), ('Bob', 'Charlie'), ('Bob', 'David')]
파이서닉 한 예:
# Pythonic
import itertools
team_a = ['Alice', 'Bob']
team_b = ['Charlie', 'David']
# Get the Cartesian product of the two lists
pairings = list(itertools.product(team_a, team_b))
print(pairings)
# Output: [('Alice', 'Charlie'), ('Alice', 'David'), ('Bob', 'Charlie'), ('Bob', 'David')]
20. Decorator
서로를 계속 호출하는 상호 재귀(Mutually Recursive) 함수들은 자칫하면 무한 루프에 빠지거나 호출 흐름을 추적하기 어려워 관리가 힘들어질 수 있습니다.
이때 파이썬의 @(Annotation)을 활용하면, 재귀의 깊이를 제어하거나 호출 상태를 기록하는 관리 로직을 실제 함수 비즈니스 로직과 분리하여 코드를 매우 깔끔하고 용이하게 만들 수 있습니다.
파이써닉하지 못한 예시:
- 로직 중복: 깊이를 체크하고 출력하는 코드가 두 함수에 똑같이 반복됩니다. (DRY 원칙 위배)
- 낮은 가독성: 함수의 핵심 로직(ping이 pong을 부르는 것)과 관리 로직(깊이 체크)이 섞여 있어 코드를 이해하기 어렵습니다.
# 못한 예시: 깊이를 인자로 넘기며 로직 중복 발생
MAX_DEPTH = 5
def ping(depth=0):
print(f"{' ' * depth} -> ping 호출됨 (깊이: {depth})")
# 관리 로직: 깊이 체크
if depth >= MAX_DEPTH:
print(f"{' ' * depth} 최대 깊이에 도달하여 중단합니다.")
return
# 비즈니스 로직: pong 호출
pong(depth + 1)
print(f"{' ' * depth} <- ping 종료됨")
def pong(depth=0):
print(f"{' ' * depth} -> pong 호출됨 (깊이: {depth})")
# 관리 로직: 깊이 체크 (ping과 동일한 코드가 중복됨)
if depth >= MAX_DEPTH:
print(f"{' ' * depth} 최대 깊이에 도달하여 중단합니다.")
return
# 비즈니스 로직: ping 호출
ping(depth + 1)
print(f"{' ' * depth} <- pong 종료됨")
print("--- 못한 예시 실행 ---")
ping()
파이서닉 한 예:
# Pythonic한 예시: 관리 로직을 데코레이터로 분리
import functools
def recursion_manager(func):
"""상호 재귀 호출을 관리하는 데코레이터"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 깊이를 wrapper 함수의 속성으로 관리하여 호출간에 공유
if not hasattr(wrapper, "depth"):
wrapper.depth = 0
print(f"{' ' * wrapper.depth} -> {func.__name__} 호출됨 (깊이: {wrapper.depth})")
# 관리 로직: 깊이 체크
if wrapper.depth >= 5:
print(f"{' ' * wrapper.depth} 최대 깊이에 도달하여 중단합니다.")
return
wrapper.depth += 1
# 원래 함수(ping 또는 pong)의 비즈니스 로직 실행
result = func(*args, **kwargs)
wrapper.depth -= 1
print(f"{' ' * wrapper.depth} <- {func.__name__} 종료됨")
return result
return wrapper
# @데코레이터만 붙여주면 관리 기능이 자동으로 적용됨
@recursion_manager
def ping():
# 비즈니스 로직만 남음
pong()
@recursion_manager
def pong():
# 비즈니스 로직만 남음
ping()
print("\n--- Pythonic한 예시 실행 ---")
ping()
장점:
- 관심사의 분리(Separation of Concerns): 함수의 핵심 로직과 관리 로직이 완벽하게 분리되었습니다.
- 재사용성: recursion_manager 데코레이터는 다른 재귀 함수에도 그대로 가져다 쓸 수 있습니다.
- 유지보수 용이성: 최대 깊이를 10으로 바꾸고 싶다면, 데코레이터 내부의 5라는 숫자 하나만 수정하면 됩니다.
21. collections.namedtuple
튜플 문서화를 한 collections.namedtuple
파이써닉하지 못한 예시:
# Non-Pythonic
# Using a plain tuple
color = (255, 165, 0)
print(f"Red component is {color[0]}") # What is index 0?
# Using a dictionary
color_dict = {'red': 255, 'green': 165, 'blue': 0}
# This is mutable and less lightweight
파이서닉 한 예:
# Pythonic
from collections import namedtuple
# Define the structure of our named tuple
Color = namedtuple('Color', ['red', 'green', 'blue'])
# Create an instance
orange = Color(red=255, green=165, blue=0)
print(f"Red component is {orange.red}") # Clean and readable
print(f"Green component is {orange[1]}") # Still accessible by index
22. any() 와 all() 활용
파이써닉하지 못한 예시:
# Non-Pythonic
widgets = [
{'name': 'widget1', 'in_stock': True},
{'name': 'widget2', 'in_stock': False},
{'name': 'widget3', 'in_stock': True}
]
# Check if at least one widget is out of stock
any_out_of_stock = False
for widget in widgets:
if not widget['in_stock']:
any_out_of_stock = True
break # Stop as soon as we find one
print(f"Is any widget out of stock? {any_out_of_stock}")
파이서닉 한 예:
# Pythonic
widgets = [
{'name': 'widget1', 'in_stock': True},
{'name': 'widget2', 'in_stock': False},
{'name': 'widget3', 'in_stock': True}
]
# Reads like English: "if not widget['in_stock'] for any widget"
is_any_out_of_stock = any(not widget['in_stock'] for widget in widgets)
# Reads like English: "if widget['in_stock'] for all widgets"
are_all_in_stock = all(widget['in_stock'] for widget in widgets)
print(f"Is any widget out of stock? {is_any_out_of_stock}") # True
print(f"Are all widgets in stock? {are_all_in_stock}") # False'개발 언어 > 파이선' 카테고리의 다른 글
| 파이선 경로 탐색 기초 - Queue, Dequeue, DFS, BFS, Dijkstra (0) | 2025.08.16 |
|---|---|
| 파이선 리스트의 고급 기능 (4) | 2025.08.10 |