title: # 목차
style: nestedList # TOC style (nestedList|nestedOrderedList|inlineFirstLevel)
minLevel: 0 # Include headings from the specified level
maxLevel: 5 # Include headings up to the specified level
includeLinks: true # Make headings clickable
hideWhenEmpty: false # Hide TOC if no headings are found
debugInConsole: false # Print debug info in Obsidian console

파이썬 특화 안티패턴: 기본편

파이썬답지 않은 코드의 함정

파이썬은 명확성과 가독성을 중시하는 고유한 철학을 가진 언어입니다. “파이썬스러운(Pythonic)” 코드를 작성하는 것은 단순히 작동하는 코드를 만드는 것을 넘어, 언어의 특성과 강점을 활용하여 효율적이고 유지보수하기 쉬운 코드를 만드는 것을 의미합니다. 이 문서에서는 파이썬 초보자부터 중급자까지 흔히 빠지는 파이썬 특화 안티패턴과 이를 개선할 수 있는 방법을 알아보겠습니다.

1. 파이썬 철학과 안티패턴

파이썬의 철학은 import this 명령을 통해 볼 수 있는 “The Zen of Python”에 잘 요약되어 있습니다. 이 중 몇 가지 중요한 원칙은 다음과 같습니다:

  • 명확함이 함축성보다 낫다
  • 단순함이 복잡함보다 낫다
  • 가독성이 중요하다
  • 특별한 경우보다는 일반적인 원칙을 따르는 것이 좋다
  • 실용성이 이상보다 중요하다

안티패턴은 이러한 원칙에 반하는 코딩 습관으로, 단기적으로는 쉬워 보이지만 장기적으로는 유지보수성과 효율성을 떨어뜨립니다.

# 안티패턴: 파이썬 철학 무시하기
def cL(l):
    r = []
    i = 0
    while i < len(l):
        if l[i] % 2 == 0:
            r.append(l[i] * 2)
        i += 1
    return r
 
# 파이썬스러운 방식
def double_even_numbers(numbers):
    """짝수를 찾아 두 배로 만든 리스트를 반환합니다."""
    return [num * 2 for num in numbers if num % 2 == 0]

위 예제에서 첫 번째 함수는 불분명한 이름, 임의적인 약어, C 스타일의 루프 사용 등 파이썬의 철학에 어긋나는 코드입니다. 반면 두 번째 함수는 명확한 이름, 독스트링(docstring), 리스트 컴프리헨션을 사용하여 파이썬의 표현력을 활용합니다.

2. 변수 및 자료형 관련 안티패턴

2.1 명확하지 않은 변수명 사용

파이썬은 가독성을 중시하므로, 변수명은 그 용도를 명확히 나타내야 합니다.

# 안티패턴: 불분명한 변수명
def f(x, y):
    z = x * y
    return z
 
# 파이썬스러운 방식
def calculate_area(width, height):
    area = width * height
    return area

2.2 파이썬 명명 규칙 무시

파이썬은 특정 명명 규칙을 권장합니다:

  • 변수와 함수는 snake_case
  • 클래스는 CamelCase
  • 상수는 UPPER_CASE
  • 프라이빗 속성은 밑줄로 시작(_private_var)
# 안티패턴: 다른 언어의 명명 규칙 사용
class userAccount:
    def __init__(self):
        self.UserName = ""
        self.EmailAddress = ""
    
    def SaveToDatabase(self):
        pass
 
# 파이썬스러운 방식
class UserAccount:
    def __init__(self):
        self.username = ""
        self.email_address = ""
    
    def save_to_database(self):
        pass

2.3 불필요한 타입 변환

파이썬은 필요할 때 자동으로 타입 변환을 수행하므로, 불필요한 명시적 변환은 피해야 합니다.

# 안티패턴: 불필요한 타입 변환
def add_numbers(a, b):
    # 이미 숫자인 경우 불필요한 변환
    sum_value = int(a) + int(b)
    return str(sum_value)  # 불필요하게 문자열로 변환
 
# 파이썬스러운 방식
def add_numbers(a, b):
    return a + b  # 파이썬이 적절한 연산 수행

2.4 타입 힌트 무시

파이썬 3.5 이상에서는 타입 힌트를 통해 코드의 가독성과 IDE 지원을 향상시킬 수 있습니다.

# 안티패턴: 타입 힌트 부재
def process_data(data, factor):
    return [item * factor for item in data]
 
# 파이썬스러운 방식
from typing import List, Any, Union, Optional
 
def process_data(data: List[Union[int, float]], factor: float) -> List[float]:
    """데이터의 각 요소에 인자를 곱합니다."""
    return [item * factor for item in data]

3. 반복 및 이터레이션 안티패턴

3.1 C 스타일 인덱스 기반 루프

파이썬에서는 컬렉션 요소를 직접 반복하는 것이 일반적이며, 인덱스가 필요한 경우 enumerate()를 사용합니다.

# 안티패턴: C 스타일 인덱스 루프
def process_names(names):
    result = []
    for i in range(len(names)):
        result.append(f"Name {i}: {names[i]}")
    return result
 
# 파이썬스러운 방식: enumerate 사용
def process_names(names):
    return [f"Name {i}: {name}" for i, name in enumerate(names)]

3.2 리스트 컴프리헨션의 오용

리스트 컴프리헨션은 파이썬의 강력한 기능이지만, 복잡해지면 가독성이 떨어집니다.

# 안티패턴: 과도하게 복잡한 리스트 컴프리헨션
def transform_data(data):
    # 너무 복잡하고 읽기 어려움
    return [x * y for x in data if x > 0 for y in range(1, x + 1) if y % 2 == 0]
 
# 파이썬스러운 방식: 복잡한 로직 분리
def transform_data(data):
    result = []
    for x in data:
        if x > 0:
            for y in range(1, x + 1):
                if y % 2 == 0:
                    result.append(x * y)
    return result

3.3 range(len()) 대신 enumerate() 사용하기

인덱스와 값이 모두 필요한 경우, range(len())보다 enumerate()를 사용하는 것이 더 파이썬스럽습니다.

# 안티패턴: range(len()) 사용
def find_even_numbers(numbers):
    result = []
    for i in range(len(numbers)):
        if numbers[i] % 2 == 0:
            result.append((i, numbers[i]))
    return result
 
# 파이썬스러운 방식: enumerate() 사용
def find_even_numbers(numbers):
    return [(i, num) for i, num in enumerate(numbers) if num % 2 == 0]

3.4 이터레이터 프로토콜 무시

파이썬은 이터레이터 기반 프로그래밍을 권장합니다. 특히 대용량 데이터를 처리할 때는 전체 데이터를 메모리에 로드하는 대신 이터레이터나 제너레이터를 사용하는 것이 좋습니다.

# 안티패턴: 전체 파일을 메모리에 로드
def count_lines(filename):
    with open(filename, 'r') as f:
        lines = f.readlines()  # 전체 파일을 메모리에 로드
    return len(lines)
 
# 파이썬스러운 방식: 이터레이터 활용
def count_lines(filename):
    with open(filename, 'r') as f:
        return sum(1 for _ in f)  # 한 줄씩 처리

4. 함수 및 인자 처리 안티패턴

4.1 기본 인자로 가변 객체 사용

함수의 기본 인자로 리스트, 딕셔너리 같은 가변 객체를 사용하면 예상치 못한 결과가 발생할 수 있습니다.

# 안티패턴: 가변 기본 인자
def add_item(item, items=[]):  # 위험: items는 한 번만 생성됨
    items.append(item)
    return items
 
# 예상치 못한 동작:
print(add_item(1))  # [1]
print(add_item(2))  # [1, 2] - 새 리스트가 아닌 이전 리스트에 추가됨
 
# 파이썬스러운 방식
def add_item(item, items=None):
    if items is None:
        items = []  # 함수 호출마다 새 리스트 생성
    items.append(item)
    return items

4.2 키워드 인자의 장점 무시

파이썬은 키워드 인자를 통해 함수 호출의 명확성을 높일 수 있습니다.

# 안티패턴: 위치 인자만 사용
def create_user(name, email, active, admin, credits):
    # 위치 인자만 사용하면 의미를 파악하기 어려움
    pass
 
create_user("John Doe", "john@example.com", True, False, 500)  # 의미가 불분명
 
# 파이썬스러운 방식: 키워드 인자 활용
def create_user(name, email, active=True, admin=False, credits=0):
    pass
 
create_user(
    name="John Doe",
    email="john@example.com",
    active=True,
    admin=False,
    credits=500
)  # 각 인자의 의미가 명확

4.3 *args**kwargs의 남용

가변 인자 *args와 키워드 가변 인자 **kwargs는 유연성을 제공하지만, 남용하면 함수의 의도가 불분명해집니다.

# 안티패턴: 불필요한 *args, **kwargs 사용
def process_data(*args, **kwargs):
    # 이 함수가 무엇을 기대하는지 알기 어려움
    data = args[0]
    factor = kwargs.get('factor', 1)
    return [item * factor for item in data]
 
# 파이썬스러운 방식: 명시적 인자 사용
def process_data(data, factor=1):
    """데이터의 각 요소에 인자를 곱합니다."""
    return [item * factor for item in data]

5. 파이썬 내장 기능 활용 부족

5.1 내장 함수 및 모듈 활용 부족

파이썬은 다양한 내장 함수와 표준 라이브러리를 제공합니다. 이들을 활용하면 코드를 더 간결하고 효율적으로 만들 수 있습니다.

# 안티패턴: 내장 기능 활용 부족
def find_max_value(numbers):
    if not numbers:
        return None
    max_val = numbers[0]
    for num in numbers[1:]:
        if num > max_val:
            max_val = num
    return max_val
 
# 파이썬스러운 방식: 내장 함수 활용
def find_max_value(numbers):
    if not numbers:
        return None
    return max(numbers)  # 내장 함수 사용

5.2 컬렉션 모듈 무시

파이썬의 collections 모듈은 특화된 컬렉션 자료형을 제공합니다.

# 안티패턴: 기본 자료형만 사용
def count_elements(items):
    counts = {}
    for item in items:
        if item in counts:
            counts[item] += 1
        else:
            counts[item] = 1
    return counts
 
# 파이썬스러운 방식: collections 모듈 활용
from collections import Counter
 
def count_elements(items):
    return Counter(items)  # 간결하고 효율적

5.3 zip(), map(), filter() 미활용

파이썬은 데이터 처리를 위한 다양한 내장 함수를 제공합니다.

# 안티패턴: 기본 루프 사용
def combine_lists(list1, list2):
    result = []
    for i in range(min(len(list1), len(list2))):
        result.append((list1[i], list2[i]))
    return result
 
# 파이썬스러운 방식: zip() 활용
def combine_lists(list1, list2):
    return list(zip(list1, list2))  # 두 리스트를 효율적으로 결합

5.4 컨텍스트 매니저(with 문) 미사용

파이썬은 리소스 관리를 위한 컨텍스트 매니저(with 문)를 제공합니다.

# 안티패턴: 수동 리소스 관리
def read_file_contents(filename):
    file = open(filename, 'r')
    try:
        return file.read()
    finally:
        file.close()  # 파일을 명시적으로, 직접 닫아야 함
 
# 파이썬스러운 방식: with 문 사용
def read_file_contents(filename):
    with open(filename, 'r') as file:  # 자동으로 리소스 관리
        return file.read()

6. 예외 처리 안티패턴

6.1 “보다 용서를 구하기 쉽다(EAFP)” 스타일 무시

파이썬은 “허락보다 용서를 구하기 쉽다(Easier to Ask for Forgiveness than Permission, EAFP)” 스타일을 선호합니다. 즉, 예외가 발생할 수 있는 작업을 시도하고 예외가 발생하면 처리하는 방식입니다.

# 안티패턴: "허락 먼저 구하기(Look Before You Leap, LBYL)" 스타일
def get_dict_value(dictionary, key):
    if key in dictionary:  # 먼저 키 존재 여부 확인
        return dictionary[key]
    else:
        return None
 
# 파이썬스러운 방식: EAFP 스타일 사용
def get_dict_value(dictionary, key):
    try:
        return dictionary[key]  # 일단 시도
    except KeyError:
        return None  # 예외 발생시 처리

6.2 과도하게 일반적인 예외 처리

특정 예외에 대해서만 처리하고 전체 예외를 잡는 것은 피해야 합니다.

# 안티패턴: 광범위한 예외 잡기
def process_file(filename):
    try:
        with open(filename, 'r') as f:
            data = f.read()
        # 데이터 처리 로직
        return data
    except:  # 모든 예외를 잡음 - 위험함
        return None
 
# 파이썬스러운 방식: 특정 예외 처리
def process_file(filename):
    try:
        with open(filename, 'r') as f:
            data = f.read()
        # 데이터 처리 로직
        return data
    except FileNotFoundError:
        print(f"File not found: {filename}")
        return None
    except PermissionError:
        print(f"Permission denied: {filename}")
        return None

6.3 빈 except 블록 사용

예외를 잡지만 아무 조치도 취하지 않는 빈 except 블록은 피해야 합니다.

# 안티패턴: 빈 except 블록
def divide_numbers(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        pass  # 아무 처리 없음 - 오류를 숨김
 
# 파이썬스러운 방식: 적절한 예외 처리
def divide_numbers(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        print("Error: Division by zero")
        return None  # 또는 다른 적절한 기본값 반환

7. 문자열 처리 안티패턴

7.1 문자열 연결 시 + 연산자 남용

문자열을 연결할 때 + 연산자를 사용하면 매번 새로운 문자열 객체가 생성되므로 비효율적입니다.

# 안티패턴: + 연산자로 문자열 연결
def build_greeting(name, message):
    # 비효율적인 문자열 연결
    result = "Hello, " + name + "! " + message + " Have a nice day!"
    return result
 
# 파이썬스러운 방식: 문자열 포맷팅 사용
def build_greeting(name, message):
    # f-문자열 사용 (Python 3.6+)
    return f"Hello, {name}! {message} Have a nice day!"
    
    # 또는 str.format() 사용
    # return "Hello, {}! {} Have a nice day!".format(name, message)

7.2 문자열 메서드 활용 부족

파이썬의 문자열 클래스는 다양한 유용한 메서드를 제공합니다.

# 안티패턴: 직접 문자열 처리
def clean_and_split(text):
    # 공백 제거 후 단어로 분할
    text = text.strip()
    words = []
    word = ""
    for char in text:
        if char == " ":
            if word:
                words.append(word)
                word = ""
        else:
            word += char
    if word:
        words.append(word)
    return words
 
# 파이썬스러운 방식: 문자열 메서드 활용
def clean_and_split(text):
    return text.strip().split()  # 간결하고 효율적

8. 딕셔너리 및 데이터 구조 안티패턴

8.1 딕셔너리 메서드 활용 부족

파이썬의 딕셔너리는 다양한 유용한 메서드를 제공합니다.

# 안티패턴: 기본적인 접근 방식만 사용
def get_value(dictionary, key, default_value):
    if key in dictionary:
        return dictionary[key]
    else:
        return default_value
 
# 파이썬스러운 방식: 딕셔너리 메서드 활용
def get_value(dictionary, key, default_value):
    return dictionary.get(key, default_value)  # 간결하고 명확

8.2 비효율적인 딕셔너리 생성

딕셔너리 컴프리헨션이나 기존 메서드를 활용하여 딕셔너리를 효율적으로 생성할 수 있습니다.

# 안티패턴: 반복적인 딕셔너리 생성
def create_squared_dict(numbers):
    result = {}
    for num in numbers:
        result[num] = num ** 2
    return result
 
# 파이썬스러운 방식: 딕셔너리 컴프리헨션 사용
def create_squared_dict(numbers):
    return {num: num ** 2 for num in numbers}

8.3 데이터 클래스 미활용 (Python 3.7+)

Python 3.7 이상에서는 데이터 클래스를 통해 데이터 중심 클래스를 쉽게 만들 수 있습니다.

# 안티패턴: 모든 것을 수동으로 구현
class Product:
    def __init__(self, name, price, quantity):
        self.name = name
        self.price = price
        self.quantity = quantity
    
    def __eq__(self, other):
        if not isinstance(other, Product):
            return False
        return (self.name == other.name and
                self.price == other.price and
                self.quantity == other.quantity)
    
    def __repr__(self):
        return f"Product(name={self.name}, price={self.price}, quantity={self.quantity})"
 
# 파이썬스러운 방식: 데이터 클래스 사용
from dataclasses import dataclass
 
@dataclass
class Product:
    name: str
    price: float
    quantity: int
    # __eq__, __repr__ 등이 자동으로 생성됨

9. 모듈 및 패키지 관련 안티패턴

9.1 와일드카드 임포트 사용

모듈에서 모든 것을 임포트하는 와일드카드 임포트(from module import *)는 네임스페이스를 오염시킬 수 있습니다.

# 안티패턴: 와일드카드 임포트
from math import *
# sqrt와 같은 함수가 어디서 왔는지 명확하지 않음
result = sqrt(16) + pi
 
# 파이썬스러운 방식: 명시적 임포트
from math import sqrt, pi
# 또는
import math
result = math.sqrt(16) + math.pi

9.2 필요 이상의 모듈 임포트

필요한 것만 임포트하고, 가능한 지역 범위에서 임포트하는 것이 좋습니다.

# 안티패턴: 불필요한 전역 임포트
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
 
def simple_calculation(x, y):
    # 이 함수는 외부 라이브러리가 필요 없음
    return x + y
 
# 파이썬스러운 방식: 필요한 모듈만 임포트
def simple_calculation(x, y):
    return x + y
 
def complex_analysis(data):
    # 함수 내에서 필요한 모듈만 임포트
    import numpy as np
    import pandas as pd
    # 분석 로직
    return pd.DataFrame(np.mean(data))

10. 파이썬다운 성능 최적화

10.1 제너레이터 미활용

대용량 데이터를 처리할 때 제너레이터를 사용하면 메모리 효율성을 높일 수 있습니다.

# 안티패턴: 리스트 생성 후 반환
def process_large_file(filename):
    result = []
    with open(filename, 'r') as f:
        for line in f:
            if process_condition(line):
                result.append(process_line(line))
    return result  # 모든 결과를 메모리에 저장
 
# 파이썬스러운 방식: 제너레이터 활용
def process_large_file(filename):
    with open(filename, 'r') as f:
        for line in f:
            if process_condition(line):
                yield process_line(line)  # 한 번에 하나씩 생성

10.2 내장 함수 및 라이브러리 무시

직접 구현하기보다 내장 함수나 최적화된 라이브러리를 활용하는 것이 성능면에서 유리합니다.

# 안티패턴: 직접 구현
def find_most_common(items):
    counts = {}
    for item in items:
        if item in counts:
            counts[item] += 1
        else:
            counts[item] = 1
    
    max_count = 0
    most_common = None
    for item, count in counts.items():
        if count > max_count:
            max_count = count
            most_common = item
    return most_common
 
# 파이썬스러운 방식: 최적화된 라이브러리 활용
from collections import Counter
 
def find_most_common(items):
    counter = Counter(items)
    return counter.most_common(1)[0][0]  # 가장 빈도가 높은 항목

11. 파일 및 I/O 작업 안티패턴

11.1 파일 열기/닫기 수동 관리

파일을 열고 닫는 것을 수동으로 관리하면 예외 발생 시 리소스 누수가 발생할 수 있습니다.

# 안티패턴: 수동 파일 관리
def read_file(filename):
    f = open(filename, 'r')  # 파일 열기
    content = f.read()      # 예외 발생 시 파일이 닫히지 않을 수 있음
    f.close()               # 명시적으로 닫기
    return content
 
# 파이썬스러운 방식: 컨텍스트 매니저 사용
def read_file(filename):
    with open(filename, 'r') as f:  # 컨텍스트 매니저가 자동으로 파일 관리
        content = f.read()
    return content  # 블록을 나가면 자동으로 파일이 닫힘

11.2 텍스트와 바이너리 모드 혼동

파일 열기에서 텍스트 모드와 바이너리 모드를 명확히 구분하지 않으면 플랫폼 간 호환성 문제가 발생할 수 있습니다.

# 안티패턴: 모드 혼동
def read_image(filename):
    with open(filename, 'r') as f:  # 잘못된 모드 - 텍스트 모드
        return f.read()  # 바이너리 파일에 텍스트 디코딩 시도로 오류 발생
 
# 파이썬스러운 방식: 명확한 모드 지정
def read_image(filename):
    with open(filename, 'rb') as f:  # 바이너리 모드
        return f.read()  # 바이너리 데이터 그대로 반환

11.3 불필요한 파일 전체 읽기

대용량 파일의 경우 전체를 메모리에 로드하는 것은 비효율적입니다.

# 안티패턴: 파일 전체 읽기
def find_in_file(filename, search_term):
    with open(filename, 'r') as f:
        content = f.read()  # 전체 파일을 메모리에 로드
    
    for line_number, line in enumerate(content.split('\n'), 1):
        if search_term in line:
            return line_number
    return -1
 
# 파이썬스러운 방식: 라인별 처리
def find_in_file(filename, search_term):
    with open(filename, 'r') as f:
        for line_number, line in enumerate(f, 1):  # 한 번에 한 줄씩 처리
            if search_term in line:
                return line_number
    return -1

12. 클래스 및 객체지향 안티패턴

12.1 @property 미활용

getter/setter 메서드 대신 @property 데코레이터를 사용하면 더 파이썬스러운 인터페이스를 만들 수 있습니다.

# 안티패턴: 명시적 getter/setter
class Circle:
    def __init__(self, radius):
        self._radius = radius
    
    def get_radius(self):  # 명시적 getter
        return self._radius
    
    def set_radius(self, value):  # 명시적 setter
        if value < 0:
            raise ValueError("Radius cannot be negative")
        self._radius = value
    
    def get_area(self):  # 또 다른 getter
        return 3.14159 * self._radius ** 2
 
# 사용 예:
c = Circle(5)
c.set_radius(10)
print(c.get_area())
 
# 파이썬스러운 방식: @property 사용
class Circle:
    def __init__(self, radius):
        self._radius = radius
    
    @property
    def radius(self):  # 속성처럼 접근 가능
        return self._radius
    
    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("Radius cannot be negative")
        self._radius = value
    
    @property
    def area(self):  # 계산된 속성
        return 3.14159 * self._radius ** 2
 
# 사용 예:
c = Circle(5)
c.radius = 10  # 속성처럼 설정
print(c.area)  # 메서드가 아닌 속성처럼 접근

12.2 __init__ 이외의 초기화

파이썬에서는 객체 초기화를 __init__ 메서드에서 수행하는 것이 관례입니다.

# 안티패턴: 비표준 초기화
class User:
    def __init__(self, username):
        self.username = username
        # 다른 초기화는 다른 메서드에서 수행
    
    def initialize(self, email, age):  # 비표준 초기화 메서드
        self.email = email
        self.age = age
 
# 사용 예:
user = User("john_doe")
user.initialize("john@example.com", 30)  # 초기화가 두 단계로 나뉨
 
# 파이썬스러운 방식: __init__에서 모든 초기화 수행
class User:
    def __init__(self, username, email=None, age=None):
        self.username = username
        self.email = email
        self.age = age
 
# 사용 예:
user = User("john_doe", "john@example.com", 30)  # 한 번에 초기화
# 또는 선택적 매개변수 사용
user = User("john_doe")  # email과 age는 None으로 초기화

12.3 특수 메서드 미활용

파이썬의 특수 메서드(__str__, __repr__, __eq__ 등)를 활용하면 클래스를 더 표준적이고 사용하기 쉽게 만들 수 있습니다.

# 안티패턴: 특수 메서드 무시
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def to_string(self):  # 비표준 문자열 변환
        return f"Point({self.x}, {self.y})"
    
    def equals(self, other):  # 비표준 비교
        return self.x == other.x and self.y == other.y
 
# 사용 예:
p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1.to_string())  # 비표준 방법
print(p1.equals(p2))   # 비표준 비교
 
# 파이썬스러운 방식: 특수 메서드 활용
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __str__(self):  # 문자열 표현
        return f"Point({self.x}, {self.y})"
    
    def __repr__(self):  # 개발자용 표현
        return f"Point(x={self.x}, y={self.y})"
    
    def __eq__(self, other):  # 동등성 비교
        if not isinstance(other, Point):
            return False
        return self.x == other.x and self.y == other.y
 
# 사용 예:
p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1)         # __str__ 사용
print(repr(p1))   # __repr__ 사용
print(p1 == p2)   # __eq__ 사용

13. 함수형 프로그래밍 안티패턴

13.1 람다 함수의 과도한 사용

람다 함수는 간단한 함수를 한 줄로 정의할 때 유용하지만, 복잡한 로직에는 명명된 함수가 더 적합합니다.

# 안티패턴: 복잡한 람다 함수
# 과도하게 복잡한 람다 사용
transform = lambda x: sum([i * j for i, j in zip(x, range(1, len(x) + 1))]) if len(x) > 0 else 0
 
# 파이썬스러운 방식: 명명된 함수 사용
def transform(x):
    """
    각 요소에 위치 인덱스+1을 곱한 후 합계를 반환합니다.
    빈 리스트의 경우 0을 반환합니다.
    """
    if not x:
        return 0
    
    result = 0
    for i, value in enumerate(x, 1):
        result += value * i
    return result

13.2 함수형 도구 미활용

파이썬은 map(), filter(), functools 모듈 등을 통해 함수형 프로그래밍을 지원합니다.

# 안티패턴: 명령형 스타일만 사용
def process_numbers(numbers):
    results = []
    for num in numbers:
        if num % 2 == 0:  # 짝수만 선택
            results.append(num * 2)  # 2배로 변환
    return results
 
# 파이썬스러운 방식: 함수형 도구 활용
def process_numbers(numbers):
    # filter로 짝수 선택, map으로 각 요소에 2 곱하기
    return list(map(lambda x: x * 2, filter(lambda x: x % 2 == 0, numbers)))
 
# 또는 리스트 컴프리헨션 사용 (종종 더 가독성이 좋음)
def process_numbers_comprehension(numbers):
    return [x * 2 for x in numbers if x % 2 == 0]

13.3 고차 함수 무시

함수를 인자로 받거나 반환하는 고차 함수를 활용하면 코드의 재사용성과 모듈성을 높일 수 있습니다.

# 안티패턴: 반복적인 유사 함수
def sort_by_name(items):
    return sorted(items, key=lambda x: x['name'])
 
def sort_by_age(items):
    return sorted(items, key=lambda x: x['age'])
 
def sort_by_score(items):
    return sorted(items, key=lambda x: x['score'])
 
# 파이썬스러운 방식: 고차 함수 활용
def sort_by(key_name):
    """특정 키로 정렬하는 함수를 반환하는 고차 함수"""
    def sorter(items):
        return sorted(items, key=lambda x: x[key_name])
    return sorter
 
# 사용 예:
sort_by_name = sort_by('name')
sort_by_age = sort_by('age')
sort_by_score = sort_by('score')
 
people = [{'name': 'Alice', 'age': 30}, {'name': 'Bob', 'age': 25}]
print(sort_by_name(people))  # 이름으로 정렬
print(sort_by_age(people))   # 나이로 정렬

14. 비동기 프로그래밍 안티패턴

14.1 async/await 잘못 사용하기

Python 3.5부터 도입된 async/await 구문은 비동기 프로그래밍에 사용되지만, 잘못 사용하면 혼란을 초래할 수 있습니다.

# 안티패턴: 일반 함수에 async 사용
async def regular_function():
    # 비동기 작업 없이 async 사용
    result = 1 + 1
    return result  # await 호출 없음
 
# 또 다른 안티패턴: 비동기 함수에서 await 미사용
async def fetch_data(url):
    import requests
    # requests는 동기식 라이브러리지만 await 없이 사용
    response = requests.get(url)  # 블로킹 호출을 await 없이 사용
    return response.json()
 
# 파이썬스러운 방식: 적절한 async/await 사용
import aiohttp
import asyncio
 
async def fetch_data(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            # 비동기 호출에 await 사용
            return await response.json()
 
# 사용 예:
async def main():
    data = await fetch_data('https://api.example.com/data')
    print(data)
 
# 실행
asyncio.run(main())

14.2 동기와 비동기 코드 혼합

동기 코드와 비동기 코드를 적절하게 분리하지 않으면 성능 저하나 예기치 않은 동작이 발생할 수 있습니다.

# 안티패턴: 비동기 함수에서 블로킹 작업 수행
import asyncio
import time
 
async def process_data(data):
    # CPU 집약적인 작업을 비동기 함수 내에서 직접 수행
    time.sleep(1)  # 전체 이벤트 루프를 블로킹함
    return data * 2
 
# 파이썬스러운 방식: 블로킹 작업 분리
import asyncio
import concurrent.futures
 
async def process_data(data):
    # CPU 집약적인 작업을 스레드 풀로 오프로드
    loop = asyncio.get_event_loop()
    with concurrent.futures.ThreadPoolExecutor() as pool:
        result = await loop.run_in_executor(pool, cpu_bound_task, data)
    return result
 
def cpu_bound_task(data):
    # 별도의 스레드에서 실행될 동기 함수
    time.sleep(1)
    return data * 2

15. 테스트 관련 안티패턴

15.1 테스트 코드 부재

테스트가 없는 코드는 유지보수와 리팩토링이 어렵습니다.

# 안티패턴: 테스트 없는 코드
def calculate_discount(price, quantity):
    if quantity >= 10:
        return price * 0.9
    else:
        return price
 
# 파이썬스러운 방식: 단위 테스트 추가
import unittest
 
def calculate_discount(price, quantity):
    if quantity >= 10:
        return price * 0.9
    else:
        return price
 
class TestDiscount(unittest.TestCase):
    def test_no_discount_for_small_quantities(self):
        self.assertEqual(calculate_discount(100, 1), 100)
        self.assertEqual(calculate_discount(100, 9), 100)
    
    def test_discount_for_large_quantities(self):
        self.assertEqual(calculate_discount(100, 10), 90)
        self.assertEqual(calculate_discount(100, 20), 90)
 
if __name__ == '__main__':
    unittest.main()

15.2 과도한 모킹

단위 테스트에서 모든 의존성을 목(mock)으로 대체하면 실제 동작과 다른 테스트가 될 수 있습니다.

# 안티패턴: 과도한 모킹
from unittest.mock import Mock
 
def test_user_service():
    # 모든 것을 목으로 대체
    db = Mock()
    logger = Mock()
    email_sender = Mock()
    
    service = UserService(db, logger, email_sender)
    result = service.create_user("test", "test@example.com")
    
    # 정작 중요한 로직은 테스트하지 않고 메서드 호출만 확인
    db.save_user.assert_called_once()
    logger.info.assert_called_once()
    email_sender.send_welcome_email.assert_called_once()
 
# 파이썬스러운 방식: 적절한 수준의 통합 테스트
def test_user_service():
    # 실제 구성 요소 사용 (필요한 경우 테스트 더블 활용)
    db = TestDatabase()  # 실제 DB 인터페이스를 구현한 테스트용 DB
    logger = Mock()  # 로깅은 부수적이므로 목 사용
    email_sender = TestEmailSender()  # 이메일 발송 테스트 구현
    
    service = UserService(db, logger, email_sender)
    result = service.create_user("test", "test@example.com")
    
    # 실제 결과 검증
    self.assertTrue(result.success)
    self.assertIsNotNone(result.user_id)
    self.assertEqual(db.get_user(result.user_id).email, "test@example.com")
    self.assertEqual(len(email_sender.sent_emails), 1)

16. 변수 범위와 클로저 안티패턴

16.1 전역 변수 오용

전역 변수를 무분별하게 사용하면 코드의 예측 가능성과 테스트 용이성이 저하됩니다.

# 안티패턴: 전역 변수 오용
counter = 0  # 전역 변수
 
def increment():
    global counter
    counter += 1
    return counter
 
def process_item(item):
    if item > 0:
        return increment()
    return 0
 
# 파이썬스러운 방식: 명시적 상태 관리
class Counter:
    def __init__(self):
        self.value = 0
    
    def increment(self):
        self.value += 1
        return self.value
 
def process_item(item, counter):
    if item > 0:
        return counter.increment()
    return 0
 
# 사용 예:
counter = Counter()
result1 = process_item(5, counter)
result2 = process_item(10, counter)

16.2 클로저에서 가변 변수 참조

클로저에서 루프 변수와 같은 가변 변수를 참조하면 예상치 못한 결과가 발생할 수 있습니다.

# 안티패턴: 가변 루프 변수를 클로저에서 참조
def create_functions():
    functions = []
    for i in range(3):
        # 루프 변수 i를 클로저에서 참조
        def function():
            return i
        functions.append(function)
    return functions
 
# 사용 예:
functions = create_functions()
for f in functions:
    print(f())  # 모두 2를 출력 (마지막 i 값)
 
# 파이썬스러운 방식: 기본값으로 현재 값 고정
def create_functions():
    functions = []
    for i in range(3):
        # 함수 정의 시점의 i 값을 기본 인자로 고정
        def function(i=i):
            return i
        functions.append(function)
    return functions
 
# 사용 예:
functions = create_functions()
for f in functions:
    print(f())  # 0, 1, 2 출력

17. 개발 및 디버깅 안티패턴

17.1 print 문을 사용한 디버깅

개발 및 디버깅 시 print 문을 남발하면 코드가 지저분해지고 실수로 프로덕션에 배포될 수 있습니다.

# 안티패턴: print 문 디버깅
def complex_calculation(data):
    print("Data received:", data)  # 디버깅용 print
    result = 0
    for item in data:
        print("Processing item:", item)  # 디버깅용 print
        intermediate = item * 2
        print("Intermediate result:", intermediate)  # 디버깅용 print
        result += intermediate
    print("Final result:", result)  # 디버깅용 print
    return result
 
# 파이썬스러운 방식: 로깅 사용
import logging
 
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
 
def complex_calculation(data):
    logger.debug("Data received: %s", data)
    result = 0
    for item in data:
        logger.debug("Processing item: %s", item)
        intermediate = item * 2
        logger.debug("Intermediate result: %s", intermediate)
        result += intermediate
    logger.debug("Final result: %s", result)
    return result

17.2 부실한 예외 처리

예외가 발생했을 때 충분한 컨텍스트 정보 없이 처리하면 디버깅이 어려워집니다.

# 안티패턴: 부실한 예외 처리
def process_file(filename):
    try:
        with open(filename, 'r') as f:
            data = f.read()
        return data
    except:  # 모든 예외를 잡아 숨김
        return None  # 실패 원인에 대한 정보 없음
 
# 파이썬스러운 방식: 적절한 예외 처리 및 로깅
import logging
logger = logging.getLogger(__name__)
 
def process_file(filename):
    try:
        with open(filename, 'r') as f:
            data = f.read()
        return data
    except FileNotFoundError as e:
        logger.error("File not found: %s", filename)
        raise  # 또는 의미 있는 정보와 함께 예외 처리
    except PermissionError as e:
        logger.error("Permission denied for file: %s", filename)
        raise  # 또는 의미 있는 정보와 함께 예외 처리
    except Exception as e:
        logger.exception("Unexpected error processing file: %s", filename)
        raise  # 또는 의미 있는 정보와 함께 예외 처리

결론

파이썬 특화 안티패턴을 이해하고 피하는 것은 더 효율적이고 유지보수하기 쉬운 코드를 작성하는 첫걸음입니다. 이 문서에서 소개한 안티패턴들은 파이썬을 사용하는 개발자들이 흔히 빠지는 함정들입니다. “파이썬스러운” 방식으로 코드를 작성하면 언어의 강점을 최대한 활용하고 다른 파이썬 개발자들도 쉽게 이해할 수 있는 코드를 만들 수 있습니다.

파이썬의 철학을 기억하세요:

>>> import this
The Zen of Python, by Tim Peters
 
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

파이썬스러운 코드 작성을 습관화하면, 코드의 가독성, 유지보수성, 효율성이 향상될 뿐만 아니라 파이썬 커뮤니티의 일원으로서 다른 개발자들과의 협업도 더 원활해질 것입니다.