46 minute read

좋은 파이썬 코드를 작성하는 125가지 방법

Effective Python 3rd

https://a.co/d/0cacRuDT

1. Pythonic Thinking

Item 1: 어떤 파이썬 버전을 쓰는지 알아라

  • 신규 프로젝트는 Python 3를 사용해라.
  • 커맨드라인에서 실행하는 python/python3가 “네가 의도한 파이썬(가상환경)”인지, “시스템 파이썬”인지부터 확인해라.
  • Python 2는 공식적으로 코어 개발자들에게 더이상 메인테인 받지 않는다.

Item 2: PEP 8 스타일 가이드를 따라라

  • 파이썬 코드를 작성할 때는 항상 PEP 8을 따라라.
  • 공통 스타일을 공유하면 파이썬 커뮤니티/팀에서 협업이 쉬워진다.
  • 일관된 스타일을 유지해서 코드를 수정하기 쉽게 만들어라.
  • Black 그리고 pylint 같은 커뮤니티 툴로 PEP 8 준수를 자동화해라.

Item 3: 파이썬이 항상 컴파일 할때 에러를 감지할거라고 기대하지 마라

  • 파이썬은 에러 체크 대부분을 runtime으로 미룬다.
  • 실행하기 전에는 “당연히 잡힐 것 같은 실수”도 안 잡힐 수 있다고 생각해라.
  • linterstatic analysis로 흔한 에러를 실행 전에 잡아라.

Item 4: 복잡한 표현(코드) 대신 helper function을 작성해라

  • 한 줄로 억지로 우겨 넣어서 너무 복잡하게 만들지 마라.
  • 복잡한 expressionhelper function으로 빼라.
  • 특히 그 로직을 반복해서 쓸 거면 확실하게 함수로 옮겨라.

Item 5: indexing 말고 unpacking을 선호해라

  • 시퀀스를 index로 덕지덕지 접근하지 마라.
  • 가능한 곳에서는 unpacking으로 여러 값을 한 번에 받아라.
  • unpacking으로 시각적 잡음을 줄이고 가독성을 올려라.

Item 6: 항상 single-element tuple은 괄호로 감싸라

  • tuple 리터럴은 보통 괄호가 optional 이고, trailing comma도 케이스에 따라 들어간다.
  • single-element tuple은 원소 뒤에 trailing comma가 필수다. 괄호는 있어도 되고 없어도 된다.
  • 표현식 끝에 실수로 trailing comma를 붙이면 의미가 single-element tuple로 바뀐다. 프로그램을 깨먹을 수 있으니 조심해라.

Item 7: 간단한 inline 로직이면 conditional expression을 고려해라

  • 파이썬은 if를 표현식 자리에도 넣을 수 있게 conditional expression을 제공한다.
  • conditional expression은 다른 언어의 삼항연산자랑 순서가 다르다. 헷갈리지 않게 조심해라.
  • 애매해지거나 처음 보는 사람이 읽기 어려워지면 쓰지 마라.
  • 이득이 확실하지 않으면 일반 if 문이랑 helper function을 써라.

Item 8: assignment expression으로 반복을 막아라

  • assignment expression은 walrus operator :=로 “할당 + 평가”를 한 번에 한다. 반복을 줄여라.
  • 큰 표현식 안의 subexpression으로 쓰면 괄호로 감싸라.
  • 파이썬에 switch/casedo/while은 없지만, 이런 패턴 일부는 assignment expression으로 더 명확하게 흉내낼 수 있다.

Item 9: 흐름 제어에서 구조 분해가 필요하면 match를 고려해라. if로 충분하면 쓰지 마라

  • 단순 ifmatch로 억지로 바꾸지 마라. 실수하기 쉽고 직관적이지 않은 함정이 있다.
  • matchisinstance 체크 + destructuring을 flow control과 같이 묶어야 할 때 강하다. heterogeneous object graph나 semi-structured data 해석할 때 써라.
  • case patternlist/tuple/dict 같은 built-in 구조랑 사용자 정의 클래스에 쓸 수 있다. 근데 타입마다 semantics가 다르다. 바로 안 보이면 함부로 쓰지 마라.

2. Strings and Slicing

Item 10: bytesstr의 차이를 알아라

  • bytes는 8-bit 값의 시퀀스고, str은 유니코드 코드 포인트의 시퀀스다.
  • 네가 다루는 입력이 어떤 문자 시퀀스 타입인지 보장하려면 helper function을 써라.
  • bytesstr은 같이 섞어서 연산하지 마라(예: >, ==, +, %). 애초에 같이 못 쓰는 경우가 많다.
  • 파일에 binary 데이터를 읽고/쓸 때는 항상 바이너리 모드로 열어라(예: "rb", "wb").
  • 파일에 유니코드 텍스트를 읽고/쓸 때는 시스템 기본 인코딩을 믿지 마라. openencoding=...을 명시해라.

Item 11: C-style % / str.format 말고 f-string을 써라

  • % 포맷팅은 gotcha도 많고 장황하다. 쓰지 마라.
  • str.format은 포맷 스펙 미니-언어 같은 유용한 개념은 있지만, 결국 %의 실수를 반복한다. 피하는 게 낫다.
  • f-string은 문자열에 값을 넣는 최신 문법이고, % 방식의 큰 문제를 해결한다.
  • f-string은 짧고 강력하다. 포맷 스펙 안에 임의의 파이썬 expression을 바로 넣을 수 있다.

Item 12: 객체 출력할 때 repr vs str 차이를 알아라

  • built-in 타입에 print를 호출하면 사람이 읽기 쉬운 문자열(str)이 나온다. 타입 정보 같은 건 숨겨질 수 있다.
  • built-in 타입에 repr를 호출하면 “출력 가능한 표현”이 나온다. 경우에 따라 repr 문자열은 eval로 다시 원래 값으로 복구될 수도 있다.
  • 포맷 문자열에서 %sstr처럼 사람이 읽기 쉬운 문자열을 만든다. %rrepr처럼 출력 가능한 표현을 만든다. f-string은 기본이 str이고, !r을 붙이면 repr로 바뀐다.
  • 커스텀 클래스는 __repr____str__을 정의해라. 디버깅이 쉬워지고, 사람 대상 UI/로그에 객체를 붙이기도 편해진다.

Item 13: 리스트에서는 암시적 문자열 결합 말고 명시적으로 붙여라

  • 파이썬은 문자열 리터럴 두 개가 코드에서 붙어 있으면 +가 있는 것처럼 자동으로 합친다(C 언어 스타일과 비슷하다).
  • list/tuple 리터럴 안에서는 암시적 결합을 피하라. 작성자 의도가 애매해져서 읽는 사람이 헷갈린다. +로 명시적으로 붙여라.
  • 함수 호출에서는 positional argument가 1개이고 keyword argument만 추가되는 형태라면 암시적 결합을 써도 괜찮다.
  • positional argument가 여러 개라면 암시적 결합을 쓰지 마라. +로 명시적으로 붙여라.

Item 14: 시퀀스 슬라이싱을 제대로 알아라

  • 슬라이싱을 쓸 때 불필요하게 장황하게 쓰지 마라. 시작 인덱스에 0을 넣거나, 끝 인덱스에 시퀀스 길이를 넣지 마라.
  • 슬라이싱은 범위를 넘어가는 인덱스에 관대하다. 앞/뒤 경계 슬라이스를 쉽게 표현해라(예: a[:20], a[-20:]).
  • list 슬라이스에 값을 할당하면 원본 시퀀스의 해당 구간이 통째로 교체된다. 길이가 달라도 그대로 치환된다.

Item 15: 한 표현식에서 striding과 slicing을 같이 쓰지 마라

  • 시작/끝/stride를 한 번에 지정하는 슬라이스는 읽기 어렵고 혼란스럽다.
  • stride가 필요하면 start/end 없이 양수의 stride만 쓰는 쪽을 선호해라. 음수 stride는 가능하면 피하라.
  • start/end/stride를 한 번에 써야 한다면 두 단계로 나눠라(먼저 stride로 뽑고, 그 다음 slice로 자르기). 아니면 itertoolsislice를 써라.

Item 16: slicing보다 catch-all unpacking을 선호해라

  • unpacking에는 starred expression을 넣을 수 있다. 남는 값들을 한 번에 list로 받아라.
  • starred expression은 패턴 어디에나 둘 수 있다. 결과는 항상 0개 이상 값을 담는 list가 된다.
  • 리스트를 겹치지 않는 조각으로 나눌 때는, slicing과 indexing을 여러 줄로 쓰지 마라. catch-all unpacking이 더 덜 위험하고 실수도 줄어든다.

3. Loops 그리고 Iterators

Item 17: range 말고 enumerate를 써라

  • enumerate는 이터레이터를 돌면서 각 아이템의 인덱스를 같이 받는 문법을 간결하게 제공한다.
  • range로 루프 돌리고 시퀀스를 index로 찍는 방식 대신 enumerate를 써라.
  • enumerate는 두 번째 인자로 시작 카운트를 지정할 수 있다(기본은 0이다).

Item 18: 이터레이터를 병렬로 처리하려면 zip을 써라

  • zip은 여러 이터레이터를 병렬로 순회할 때 써라.
  • zip은 튜플을 만들어내는 lazy generator다. 무한 입력에도 쓸 수 있다.
  • 길이가 다른 이터레이터를 넣으면 가장 짧은 쪽에 맞춰서 조용히 잘린다.
  • 조용히 잘리는 걸 막고 싶으면 zipstrict=True를 넘겨라. 길이가 다르면 runtime 에러로 터지게 해라.

Item 19: for/while 뒤에 붙는 else 블록은 피하라

  • 파이썬은 for/while 바로 뒤에 else 블록을 붙일 수 있는 특수 문법이 있다.
  • 루프 뒤 else는 루프 바디가 break를 만나지 않았을 때만 실행된다.
  • 루프 뒤 else는 직관적이지 않다. 헷갈리니까 쓰지 마라.

Item 20: 루프가 끝난 뒤 for 변수는 절대 쓰지 마라

  • for 루프 변수는 루프가 끝나도 현재 스코프에서 접근 가능하다. 그거 믿고 쓰지 마라.
  • 루프가 한 번도 돌지 않으면 for 변수는 스코프에 할당조차 안 될 수 있다.
  • generator expression이랑 list comprehension은 기본적으로 루프 변수를 밖으로 새지 않게 만든다.
  • exception handler도 예외 인스턴스 변수를 밖으로 새지 않게 한다.

Item 21: 인자 이터레이션은 방어적으로 해라

  • 함수/메서드가 입력 인자를 여러 번 순회하는지 조심해라. 인자가 iterator면 값이 사라지거나 이상한 동작이 난다.
  • 파이썬 iterator protocoliter/nextfor 루프 같은 동작을 어떻게 연결하는지 이해해라.
  • 너만의 iterable 컨테이너가 필요하면 __iter__를 generator로 구현해라.
  • 값이 iterator인지 확인하려면 iter(x) is x 패턴을 써라(같은 객체가 나오면 iterator일 가능성이 크다). 또는 isinstance(x, collections.abc.Iterator)로 판별해라.

Item 22: 순회 중에는 컨테이너를 절대 수정하지 마라. 대신 복사본이나 캐시를 써라

  • list/dict/set을 순회하면서 요소를 추가/삭제하지 마라. 예측하기 어려운 runtime 에러나 버그가 난다.
  • 수정이 필요하면 컨테이너의 복사본을 순회해라.
  • 성능 때문에 복사를 피해야 하면, 변경 사항을 두 번째 컨테이너(캐시)에 모아뒀다가 나중에 원본에 합쳐라.

Item 23: 효율적인 short-circuit 로직엔 anyall에 이터레이터를 넘겨라

  • all은 모든 항목이 truthy면 True를 반환한다. falsey를 만나면 즉시 멈추고 False를 반환한다.
  • any는 반대로 동작한다. 모든 항목이 falsey면 False고, truthy를 만나면 즉시 멈추고 True를 반환한다.
  • any/all은 항상 True 또는 False만 반환한다. and/or처럼 “마지막으로 평가된 값”을 반환하지 않는다.
  • any/alllist comprehension을 쓰지 마라. generator expression을 써서 short-circuit 이점을 살려라.

Item 24: 이터레이터/제너레이터 작업엔 itertools를 고려해라

  • itertools는 이터레이터/제너레이터를 다룰 때 쓰는 표준 도구 모음이다.
  • 큰 분류는 3가지로 생각해라: 이터레이터 연결, 출력 필터링, 조합 생성.
  • 더 고급 함수, 파라미터, 유용한 레시피는 공식 문서를 참고해라.

4. Dictionaries

Item 25: dict 삽입 순서(insertion ordering)에 의존할 땐 조심해라

  • Python 3.7부터 dict를 순회하면 키가 처음 추가된 순서대로 나온다고 기대해도 된다.
  • 하지만 dict처럼 동작하는 “딕셔너리 비슷한 타입”은 dict 인스턴스가 아닐 수 있다. 이런 타입은 삽입 순서를 보장한다고 가정하지 마라.
  • 딕셔너리 같은 클래스에 안전하게 대응하려면 셋 중 하나로 가라.

    • 삽입 순서에 의존하지 않는 코드로 짜라.
    • 런타임에 dict 타입인지 명시적으로 검사해라.
    • 타입 힌트와 static analysis로 입력을 dict로 강제해라.

Item 26: 키가 없을 때는 in/KeyError 말고 get을 우선 써라

  • 딕셔너리에서 missing key를 처리하는 흔한 방법은 4개다: in, KeyError, get, setdefault.
  • 기본 타입(카운터 같은 것) 다룰 때는 get이 제일 무난하고 깔끔하다.
  • 기본값 생성 비용이 크거나 예외가 날 수 있으면 get을 쓰고, 필요하면 assignment expression으로 중복을 줄여라.
  • setdefault가 좋아 보이면 defaultdict가 더 맞는지 먼저 검토해라.

Item 27: 내부 상태 관리에는 setdefault 말고 defaultdict를 써라

  • 가능한 키가 계속 늘어날 수 있는 내부 상태용 딕셔너리를 만든다면 collections.defaultdict를 우선 고려해라.
  • 딕셔너리가 외부에서 들어오는 값이라 네가 생성 과정을 컨트롤 못 하면, 접근은 기본적으로 get으로 해라.
  • 그래도 코드가 확 짧아지고 기본값 생성 비용이 낮은 일부 상황에서는 setdefault를 써도 된다.

Item 28: 키에 따라 기본값이 달라져야 하면 __missing__을 써라

  • 기본값 생성 비용이 크거나 예외가 날 수 있으면 dict.setdefault는 나쁜 선택이다.
  • defaultdict의 factory 함수는 인자를 받을 수 없다. 그래서 “키에 의존하는 기본값”을 만들 수 없다.
  • 키에 따라 기본값을 만들어야 하면 dict를 상속하고 __missing__을 정의해서 처리해라.

Item 29: 딕셔너리/리스트/튜플을 깊게 중첩하지 말고 클래스를 조합해라

  • 값이 또 딕셔너리인 딕셔너리, 긴 튜플, 복잡하게 중첩된 built-in 타입 덩어리를 만들지 마라.
  • 완전한 클래스가 필요해지기 전까지는 가벼운 데이터 컨테이너로 dataclasses를 먼저 써라.
  • 내부 상태용 딕셔너리가 복잡해지기 시작하면 bookkeeping 코드를 여러 클래스로 쪼개서 관리해라.

5. Functions

Item 30: 함수 인자는 변형될 수 있다는 걸 알아라

  • 파이썬 인자는 reference로 전달된다. 함수/메서드가 인자의 attribute나 내부 값을 바꿀 수 있다.
  • 입력을 수정할 거면 이름이랑 문서로 “수정한다”는 걸 분명히 드러내라. 아니면 수정하지 마라.
  • 실수로 원본을 건드리기 싫으면, 받은 컬렉션/객체를 복사해서 써라.

Item 31: 3개 초과 언패킹 강요하지 말고 전용 결과 객체를 반환해라

  • 함수는 튜플로 여러 값을 반환하고, 호출자는 unpacking으로 받을 수 있다.
  • catch-all starred expression으로 여러 반환값을 받을 수도 있다.
  • 4개 이상 변수로 언패킹하게 만들지 마라. 실수하기 쉽다. 대신 가벼운 클래스 인스턴스를 반환해라.

Item 32: None 반환하지 말고 예외를 던져라

  • 특별한 의미로 None을 반환하게 만들지 마라. NoneBoolean에서 falsey라서 0이나 "" 같은 값이랑 섞이면 버그 난다.
  • 특수 상황은 None이 아니라 예외로 표현해라. 문서화하고 호출자가 예외를 처리하게 만들어라.
  • 타입 힌트로 “이 함수는 어떤 경우에도 None을 반환하지 않는다”를 명확히 해라.

Item 33: 클로저 스코프랑 nonlocal을 제대로 알아라

  • 클로저 함수는 자신을 감싸는 스코프의 변수를 참조할 수 있다.
  • 기본적으로 클로저는 바깥 스코프 변수에 “할당”해서 영향을 주지 못한다.
  • 바깥 스코프 변수를 수정해야 하면 nonlocal을 써라. 모듈 레벨 이름을 수정해야 하면 global을 써라.
  • nonlocal은 단순한 경우 외에는 남발하지 마라. 복잡해지면 코드가 읽기 어려워진다.

Item 34: 가변 위치 인자로 visual noise를 줄여라

  • 함수가 위치 인자를 여러 개 받게 하려면 정의에서 *args를 써라.
  • 시퀀스의 아이템을 위치 인자로 풀어서 넘기려면 호출에서 *를 써라.
  • generator에 *를 바로 쓰지 마라. 메모리를 과하게 잡아먹어서 터질 수 있다.
  • *args 받는 함수에 새 위치 인자를 추가하지 마라. 호출자 쪽 버그가 조용히 생기기 쉽다.

Item 35: keyword 인자로 옵션 동작을 제공해라

  • 함수 인자는 position으로도, keyword로도 넘길 수 있다.
  • 위치 인자만 쓰면 의도가 헷갈리는 곳은 keyword로 명확히 해라.
  • 기본값 있는 keyword 인자는 기존 호출자를 다 안 고치고도 새 동작을 추가할 수 있게 해준다.
  • optional keyword 인자는 항상 keyword로 넘겨라. position으로 넘기지 마라.

Item 36: 동적인 기본값은 None과 docstring으로 지정해라

  • 기본 인자 값은 함수 정의 시점(모듈 로드 시점)에 한 번만 평가된다. 동적인 값(함수 호출, 새 객체, 컨테이너)은 이상한 버그를 만든다.
  • 동적으로 초기화돼야 하는 기본값은 placeholder로 None을 써라. 실제 기본값 의도는 docstring에 적어라. 함수 바디에서 None 체크해서 올바른 기본 동작을 트리거해라.
  • None placeholder 방식은 타입 힌트랑 같이 써도 깔끔하게 맞는다.

Item 37: keyword-only / positional-only 인자로 호출 의도를 강제해라

  • keyword-only 인자는 특정 인자를 반드시 keyword로 넘기게 만들어서 호출 의도를 명확하게 한다. 인자 리스트에서 * 뒤에 둬라(* 단독이든 *args든 상관없다).
  • positional-only 인자는 특정 파라미터를 keyword로 못 넘기게 해서 결합도를 줄인다. 인자 리스트에서 / 앞에 둬라.
  • /* 사이에 있는 파라미터는 기본 규칙대로 position/keyword 둘 다 허용해라.

Item 38: 데코레이터는 functools.wraps로 정의해라

  • 데코레이터는 런타임에 한 함수가 다른 함수를 수정하는 문법이다.
  • 데코레이터를 잘못 쓰면 디버거 같은 introspection 도구에서 이상하게 보일 수 있다.
  • 커스텀 데코레이터를 만들 때는 반드시 functools.wraps를 써서 메타데이터가 깨지지 않게 해라.

Item 39: glue 함수는 lambda보다 functools.partial을 선호해라

  • lambda는 인자 재배치나 특정 값 고정으로 인터페이스를 맞출 때 짧게 쓸 수 있다.
  • functools.partial은 위치/키워드 인자를 고정해서 새 함수를 만드는 범용 도구다.
  • 인자 순서를 바꿔야 하는 경우에만 lambda를 쓰고, 그 외에는 partial로 고정해라.

Item 40: map/filter 말고 comprehension을 써라

  • list comprehensionmap/filter보다 보통 더 명확하다. lambda를 강요하지 않기 때문이다.
  • list comprehensionif 절로 입력 항목을 쉽게 건너뛸 수 있다. map은 단독으로 이걸 못 한다.
  • dictsetcomprehension으로 만들 수 있다.
  • list comprehension은 평가 시 결과를 전부 메모리에 만든다. 입력이 크면 메모리 많이 먹는다고 생각해라.

Item 41: comprehension에 control subexpression 2개 넘게 넣지 마라

  • comprehension은 여러 레벨 루프와 여러 조건을 지원한다.
  • 제어 subexpression이 2개를 넘어가면 읽기 너무 어렵다. 피하라.

Item 42: assignment expression으로 comprehension의 반복을 줄여라

  • assignment expressioncomprehension/generator expression에서 한 번 계산한 값을 같은 식 안에서 재사용하게 해준다. 가독성과 성능을 같이 챙겨라.
  • 조건 밖에서 assignment expression을 억지로 쓰지 마라. 안정적으로 동작하지 않는 경우가 있다.
  • comprehension에서 assignment expression으로 만든 변수는 바깥 스코프로 새어 나간다. 반면 comprehension의 루프 변수는 기본적으로 새지 않는다.

Item 43: 리스트 반환 대신 generator를 고려해라

  • 누적 결과 리스트를 만들어 반환하는 것보다 generator가 더 명확한 경우가 많다.
  • generator는 함수 바디의 yield로 전달한 값들을 순서대로 만들어내는 이터레이터를 반환한다.
  • generator는 모든 입력/출력을 메모리에 쌓지 않는다. 그래서 입력이 매우 커도 동작하게 만들어라.

Item 44: 큰 list comprehension이면 generator expression을 고려해라

  • 입력이 큰데 list comprehension을 쓰면 메모리 때문에 문제가 날 수 있다.
  • generator expression은 출력을 하나씩 만들기 때문에 메모리 문제를 피한다.
  • generator expression은 다른 generator expressionfor 입력으로 넘겨서 조합해라.
  • 체이닝해도 빠르고 메모리 효율이 좋다.

Item 45: 여러 generator는 yield from으로 합쳐라

  • yield from은 중첩된 여러 generator를 하나의 generator로 합치는 문법이다.
  • yield from으로 중첩 generator를 수동으로 돌면서 yield하는 boilerplate를 없애라.

Item 46: generator에 데이터를 넣으려면 send 말고 “입력 이터레이터”를 인자로 넘겨라

  • send는 generator에 값을 주입해서 yield 표현식이 값을 받게 만들 수 있다.
  • sendyield from을 섞지 마라. 예상 못 한 타이밍에 None이 튀어나오는 등 놀라운 동작이 생길 수 있다.
  • generator들을 조합해야 하면 send로 밀어 넣지 말고, 입력 이터레이터를 인자로 전달하는 구조로 짜라. send는 피하라.

Item 47: 상태 전이는 generator throw 말고 클래스로 관리해라

  • throw는 generator 안에서 “마지막 yield 지점”에 예외를 다시 던질 수 있다.
  • throw를 쓰면 예외 raise/catch를 위해 중첩과 boilerplate가 늘어난다. 가독성이 깨진다.
  • 반복 처리 + 상태 전이가 필요하면 stateful 클래스를 만들어라. iteration과 state transition을 메서드로 분리해라.

7. Classes and Interfaces

Item 48: 단순 인터페이스면 클래스 말고 함수를 받아라

  • 클래스 정의/인스턴스화 대신, 단순한 컴포넌트 인터페이스는 함수로 해결해라.
  • 파이썬에서 함수/메서드 레퍼런스는 first-class다. 다른 타입처럼 표현식에 넣어라.
  • 클래스 인스턴스를 함수처럼 호출 가능하게 하려면 __call__을 구현해라.
  • 상태가 필요한 “함수”가 필요하면 stateful closure로 억지로 만들지 마라. __call__을 가진 클래스를 고려해라.

Item 49: isinstance 분기 함수 말고 OOP 다형성을 써라

  • isinstance로 타입을 검사해서 동작을 바꾸는 코드를 남발하지 마라.
  • 다형성(polymorphism)은 런타임에 “가장 구체적인 서브클래스 구현”으로 메서드 호출을 디스패치하는 OOP 기법이다.
  • 많은 타입 분기가 필요하면 isinstance 체인 대신 다형성으로 설계해라. 읽기/유지보수/확장/테스트가 쉬워진다.

Item 50: OOP 대신 함수형 스타일이 필요하면 functools.singledispatch를 고려해라

  • OOP는 클래스 중심으로 코드가 흩어지기 쉽다. 큰 프로그램에서 관리가 어려워질 수 있다.
  • single dispatch는 메서드 다형성 대신 “함수 기반” 동적 디스패치를 제공해서 관련 기능을 한곳에 모으기 좋다.
  • 파이썬은 functoolssingledispatch 데코레이터를 제공한다. 필요하면 써라.
  • 서로 독립적인 시스템들이 같은 데이터 타입을 대상으로 동작한다면, OOP 대신 single dispatch의 함수형 스타일이 더 맞을 수 있다.

Item 51: 가벼운 클래스 정의엔 dataclasses를 선호해라

  • dataclasses@dataclass로 boilerplate 없이 가볍고 실용적인 클래스를 정의해라.
  • 표준 OOP 문법의 장황함과 실수 포인트를 줄이는 데 dataclasses가 도움이 된다.
  • dataclassesasdict/astuple 같은 변환 헬퍼와 field 같은 고급 속성 제어도 제공한다.
  • 나중에 커스터마이즈가 더 필요해지면 dataclasses 없이도 OOP 패턴을 직접 구현할 수 있어야 한다. 그 길로 옮길 준비를 해라.

Item 52: @classmethod 다형성으로 객체를 범용적으로 생성해라

  • 파이썬 클래스는 생성자가 사실상 하나다. __init__만 있다고 생각해라.
  • 대안 생성자가 필요하면 @classmethod로 만들어라.
  • @classmethod 다형성으로 “여러 구체 하위 클래스”를 공통 방식으로 생성/연결할 수 있게 해라.

Item 53: 부모 클래스 초기화는 super로 해라

  • 파이썬의 표준 MRO(메서드 해석 순서)가 부모 초기화 순서 문제랑 다이아몬드 상속 문제를 정리해준다.
  • 부모 초기화나 부모 메서드 호출은 super()(인자 0개)로 해라.

Item 54: 기능은 mix-in 클래스로 조합하는 걸 고려해라

  • mix-in으로 해결 가능하면, 인스턴스 속성이나 __init__까지 얹는 다중 상속은 피 해라.
  • 클래스별 커스터마이징이 필요하면, 인스턴스 단위로 끼울 수 있는 “플러그형 동작”도 같이 고려해라.
  • mix-in은 필요에 따라 인스턴스 메서드도 넣고, 클래스 메서드도 넣어라.
  • 작은 동작을 mix-in으로 만들고, 여러 개를 조합해서 복잡한 기능을 구성해라.

Item 55: private 속성보다 public 속성을 선호해라

  • 파이썬에서 private 속성은 컴파일러가 엄격하게 강제하지 않는다.
  • 처음부터 “서브클래스가 내부 API/속성을 더 활용할 수 있게” 설계해라. 아예 막아버리는 방향으로 가지 마라.
  • 접근을 강제로 막으려 하지 말고, protected 필드는 문서로 규칙을 잡아서 서브클래스가 따라오게 해라.
  • 서브클래스를 네가 통제 못 하는 상황에서 “이름 충돌”을 피해야 할 때만 private 속성을 고려해라.

Item 56: 불변 객체를 생성할 때는 dataclasses를 선호하라

  • 불변 객체를 사용하는 함수형 스타일의 코드는 상태를 변경하고 부작용을 일으키는 명령형 스타일의 코드보다 더 견고한 경우가 많다.
  • 사용자 정의 불변 객체를 만드는 가장 쉬운 방법은 내장 모듈인 dataclasses를 사용하는 것이다. 클래스 정의 시 dataclass 데코레이터를 적용하고 frozen=True 인자를 전달하면 된다.
  • dataclasses 모듈의 replace 헬퍼 함수는 일부 속성만 변경된 불변 객체의 복사본을 생성할 수 있게 해주며, 이를 통해 함수형 스타일의 코드를 더 쉽게 작성할 수 있다.
  • dataclass로 생성된 불변 객체는 값 기준으로 동등성 비교가 가능하고 안정적인 해시 값을 가지므로, 딕셔너리의 키나 집합(set)의 요소로 사용할 수 있다.

Item 57: 사용자 정의 컨테이너 타입을 만들 때는 collections.abc 클래스를 상속하라

  • 단순한 사용 사례의 경우, listdict와 같은 파이썬의 내장 컨테이너 타입을 직접 상속해도 괜찮다.
  • 내장 타입을 상속하지 않고 사용자 정의 컨테이너 타입을 올바르게 구현하려면 구현해야 할 메서드의 수가 매우 많다는 점에 주의해야 한다.
  • 사용자 정의 컨테이너 클래스가 요구되는 동작을 정확히 따르도록 보장하려면, collections.abc에 정의된 인터페이스를 상속하도록 하라.

8. Metaclasses and Attributes

Item 58: setter와 getter 메서드 대신 일반 속성을 사용하라

  • 새로운 클래스 인터페이스는 단순한 공개 속성(public attribute)을 사용해 정의하고, setter와 getter 메서드 정의는 피하라.
  • 객체의 속성에 접근할 때 특별한 동작이 필요하다면 @property를 사용하라.
  • 최소 놀람의 원칙(rule of least surprise)을 따르고, @property 메서드에서 예상치 못한 부작용을 피하라.
  • @property 메서드는 반드시 빠르게 동작해야 한다. I/O를 포함하거나 부작용을 일으키는 느리거나 복잡한 작업은 일반 메서드를 사용하라.

Item 59: 속성을 리팩터링하는 대신 @property 사용을 고려하라

  • 기존 인스턴스 속성에 새로운 기능을 부여하기 위해 @property를 사용하라.
  • @property를 활용해 더 나은 데이터 모델로 점진적으로 개선하라.
  • @property를 과도하게 사용하게 된다면, 클래스와 모든 호출 지점을 함께 리팩터링하는 것을 고려하라.

Item 60: 재사용 가능한 @property 메서드를 위해 디스크립터를 사용하라

  • 재사용 가능한 @property 메서드의 동작과 검증 로직을 위해 사용자 정의 디스크립터 클래스를 정의하라.
  • 메모리 누수를 방지하기 위해 __set_name__setattr, getattr를 함께 사용하여 디스크립터에 필요한 데이터를 객체 인스턴스의 딕셔너리에 저장하라.
  • 속성 접근과 설정 시 __getattribute__가 디스크립터 프로토콜을 어떻게 사용하는지에 대해 정확히 이해하려고 너무 집착하지 말라.

Item 61: Lazy Attribute를 위해 __getattr__, __getattribute__, __setattr__ 사용하기

  • 객체의 속성을 지연 로딩하고 저장하기 위해 __getattr____setattr__를 사용하라.
  • __getattr__는 존재하지 않는 속성에 접근할 때만 호출되고, 반면 __getattribute__는 어떤 속성이든 접근할 때마다 항상 호출된다는 점을 이해하라.
  • 객체의 속성에 접근하기 위해 super().__getattribute__super().__getattr__를 호출하여 __getattribute____setattr__ 메서드 구현에서 무한 재귀를 피하라.

Item 62: __init_subclass__로 서브클래스 검증하기

  • 메타클래스의 __new__ 메서드는 클래스 구문의 전체 본문이 처리된 이후 실행된다.
  • 메타클래스는 클래스가 정의된 후 생성되기 전에 클래스를 검사하거나 수정하는 데 사용할 수 있지만, 보통은 필요 이상으로 무겁다.
  • 서브클래스가 정의되는 시점(해당 타입의 객체가 생성되기 전)에 올바르게 구성되었는지 보장하기 위해 __init_subclass__를 사용하라.
  • 여러 계층의 클래스와 다중 상속에서 조합 가능한 검증을 가능하게 하려면, 클래스의 __init_subclass__ 정의 내부에서 반드시 super().__init_subclass__를 호출하라.

Item 63: __init_subclass__로 클래스 등록 자동화

  • 클래스 등록(class registration)은 모듈형 파이썬 프로그램을 구성할 때 유용한 패턴이다.
  • 메타클래스는 기반 클래스가 상속될 때마다 등록 코드를 자동 실행할 수 있게 해준다.
  • 메타클래스를 이용하면 등록 호출을 빠뜨리는 실수를 방지할 수 있다.
  • 하지만 표준 메타클래스 방식보다 __init_subclass__ 사용을 권장한다. 더 명확하고, 초보자에게 이해하기 쉽다.

Item 64: __set_name__로 클래스 속성 어노테이션

  • 메타클래스는 클래스가 완전히 정의되기 전에 속성을 수정할 수 있게 해준다.
  • 디스크립터(descriptor)와 메타클래스는 선언적 동작 및 런타임 introspection에 강력한 조합이다.
  • 디스크립터 클래스에 __set_name__을 정의하면, 자신이 속한 클래스와 속성 이름을 인지할 수 있다.

Item 65: 클래스 본문 정의 순서를 활용해 속성 간 관계 설정하기

  • 클래스 본문에서 정의된 속성과 메서드는 런타임에 클래스 객체의 __dict__ 인스턴스 딕셔너리를 통해 확인할 수 있다.
  • 클래스 본문의 정의 순서는 __dict__에 그대로 보존된다. 이 특성 덕분에 속성과 메서드의 상대적 위치를 고려하는 코드 작성이 가능하다. 예를 들어 객체 필드를 CSV 컬럼 인덱스에 매핑하는 경우에 특히 유용하다.
  • 디스크립터와 메서드 데코레이터를 활용하면 정의 순서를 기반으로 프로그램 동작을 더욱 강력하게 제어할 수 있다.

Item 66: 조합 가능한 클래스 확장에는 메타클래스보다 클래스 데코레이터를 선호하라

  • 클래스 데코레이터는 클래스를 인자로 받아 새로운 클래스를 반환하거나 기존 클래스를 수정하는 단순한 함수다.
  • 최소한의 보일러플레이트로 클래스의 모든 메서드나 속성을 수정하고 싶을 때 유용하다.
  • 메타클래스는 함께 조합하기 어렵지만, 여러 클래스 데코레이터는 충돌 없이 동일 클래스에 적용할 수 있다.

9. Concurrency and Parallelism

Item 67: 자식 프로세스 관리는 subprocess를 사용하라

  • 자식 프로세스를 실행하고 입출력 스트림을 관리하려면 subprocess 모듈을 사용한다.
  • 자식 프로세스는 파이썬 인터프리터와 병렬로 실행되므로 CPU 코어 활용을 극대화할 수 있다.
  • 간단한 사용에는 run() 편의 함수를, UNIX 스타일 파이프라인 같은 고급 사용에는 Popen 클래스를 사용한다.
  • 데드락이나 멈춤 현상을 방지하려면 communicate() 메서드의 timeout 파라미터를 활용한다.

Item 68: 병렬 처리 목적이라면 스레드를 피하고, Blocking I/O에는 활용하라

  • 파이썬 스레드는 GIL(Global Interpreter Lock) 때문에 여러 CPU 코어에서 완전한 병렬 실행이 불가능하다.
  • 그럼에도 스레드는 동시에 여러 작업을 수행하는 것처럼 보이게 하는 간단한 방법을 제공한다.
  • 계산과 동시에 Blocking I/O를 처리하거나 여러 시스템 호출을 병행할 때 스레드는 여전히 유용하다.

Item 69: 스레드의 데이터 경쟁을 막으려면 Lock을 사용하라

  • GIL이 존재하더라도 스레드 간 데이터 경쟁 보호는 개발자의 책임이다.
  • 여러 스레드가 동일 객체를 동시에 수정하면 데이터 구조가 손상될 수 있다.
  • 프로그램의 불변성을 지키려면 threading 모듈의 Lock 클래스를 사용해 상호 배제를 보장한다.

Item 70: 스레드 간 작업 조율에는 Queue를 사용하라

  • 파이프라인 구조는 특히 I/O 중심 프로그램에서 여러 스레드를 활용한 동시 실행을 가능하게 한다.
  • 동시성 파이프라인 구현 시 바쁜 대기, 종료 신호 전달, 작업 완료 판단, 메모리 폭증 등의 문제를 고려해야 한다.
  • Queue 클래스는 Blocking 연산, 버퍼 크기 제한, join, 종료 처리 등 안정적인 파이프라인 구축에 필요한 기능을 제공한다.

Item 71: 언제 동시성이 필요한지 판단하라

  • 프로그램의 규모와 복잡도가 증가하면 여러 실행 흐름 지원이 필요해진다.
  • 가장 흔한 동시성 조율 패턴은 fan-out(작업 분산)과 fan-in(작업 합류)이다.
  • 파이썬은 fan-out과 fan-in을 구현할 수 있는 다양한 방법을 제공한다.

Item 72: 필요할 때마다 새로운 Thread 인스턴스 생성을 피해라

  • 스레드는 여러 단점을 가진다. 많은 수의 스레드를 시작하고 실행하는 비용이 크고, 각 스레드는 상당한 메모리를 요구하며, 동기화를 위해 Lock 같은 별도 도구가 필요하다.
  • Thread는 예외를 스레드를 시작한 코드나 대기 중인 다른 스레드로 직접 전달하는 기본 메커니즘이 없다. 이로 인해 디버깅이 어려워진다.

Item 73: Queue 기반 동시성 사용 시 리팩터링 필요성을 이해하라

  • 고정된 수의 워커 스레드와 Queue를 함께 사용하면 fan-out / fan-in 구조의 확장성이 개선된다.
  • 기존 코드를 Queue 기반 구조로 리팩터링하는 작업은 상당한 노력이 필요하며, 특히 파이프라인 단계가 여러 개인 경우 더 복잡해진다.
  • 고정된 워커 스레드 수는 프로그램이 활용할 수 있는 I/O 병렬성의 상한을 제한한다.

Item 74: 스레드가 필요하다면 ThreadPoolExecutor를 고려하라

  • ThreadPoolExecutor는 비교적 적은 리팩터링으로 I/O 병렬 처리를 가능하게 한다.
  • fan-out이 필요할 때마다 새로운 스레드를 생성하는 비용을 줄일 수 있다.
  • 스레드 경계를 넘어 예외를 자동으로 전파하므로 디버깅이 쉬워진다.
  • 직접 스레드를 사용할 때 발생할 수 있는 메모리 폭증 문제를 완화하지만, max_workers를 미리 지정해야 하므로 병렬성은 제한된다.

Item 75: 코루틴으로 높은 수준의 I/O 동시성을 달성하라

  • async 키워드로 정의된 함수는 코루틴이다. 호출자는 await로 결과를 기다린다.
  • 코루틴은 수만 개 이상의 작업을 효율적으로 동시에 실행하는 방법을 제공한다.
  • fan-out / fan-in 구조로 I/O를 병렬화하면서, 스레드 기반 I/O에서 발생하는 문제들을 피할 수 있다.

Item 76: Thread 기반 I/O를 asyncio로 이전하는 방법을 이해하라

  • 파이썬은 코루틴에서 사용할 수 있는 비동기 버전의 for 루프, with 문, 제너레이터, 컴프리헨션, 이터레이터, 그리고 라이브러리 헬퍼 함수를 제공한다.
  • 내장 모듈 asyncio는 스레드 및 블로킹 I/O를 사용하던 기존 코드를 코루틴과 비동기 I/O로 이전하는 작업을 단순화한다.

Item 77: asyncio 전환 과정에서 스레드와 코루틴을 함께 활용하라

  • 이벤트 루프의 awaitable 메서드 run_in_executor를 사용하면 코루틴에서 동기 함수를 ThreadPoolExecutor 워커 스레드로 실행할 수 있다. 이는 점진적인 상향식/하향식 마이그레이션을 돕는다.
  • 이벤트 루프의 run_until_complete 메서드는 동기 코드에서 코루틴이 끝날 때까지 실행할 수 있게 한다. asyncio.run_coroutine_threadsafe 함수는 스레드 경계를 넘어 동일한 기능을 제공한다.

Item 78: asyncio 이벤트 루프의 응답성을 극대화하라

  • 코루틴 내부에서 블로킹 I/O나 스레드 시작 같은 시스템 호출을 수행하면 프로그램 응답성이 저하되고 지연(latency)이 증가할 수 있다.
  • asyncio.run(..., debug=True) 옵션을 사용하면 이벤트 루프의 반응을 방해하는 코루틴을 탐지할 수 있다.
  • 비동기/동기 경계를 넘나드는 코드의 가독성을 높이기 위해 코루틴 친화적인 인터페이스를 제공하는 헬퍼 스레드 클래스를 고려할 수 있다.

Item 79: 진정한 병렬 처리를 위해 concurrent.futures를 고려하라

  • multiprocessing 모듈은 일부 파이썬 계산 작업을 최소한의 노력으로 병렬화할 수 있는 강력한 도구를 제공한다.
  • 병렬 처리 기능은 concurrent.futures 내장 모듈과 ProcessPoolExecutor 클래스를 통해 사용하는 것이 가장 단순하다.
  • 다른 대안을 모두 검토하기 전까지는 multiprocessing의 복잡한 고급 기능 사용을 피하는 편이 좋다.

10. Robustness

Item 80: try/except/else/finally 각 블록을 적극 활용하라

  • try/finally 구문은 try 블록에서 예외 발생 여부와 관계없이 정리(cleanup) 코드를 실행하게 한다.
  • else 블록은 try 블록에 들어가는 코드를 최소화하고, 성공 경로를 시각적으로 분리하는 데 도움을 준다.
  • else 블록은 try 블록이 성공적으로 끝난 이후, finally 이전에 추가 작업을 수행할 때 유용하다.

Item 81: 내부 가정은 assert, 예상 가능한 오류는 raise

  • raise 문은 호출자에게 예상된 오류 상황을 전달하는 데 사용한다.
  • 함수가 직접 발생시키는 예외는 명시적인 인터페이스의 일부이므로 문서화해야 한다.
  • assert 문은 개발자의 가정을 검증하고 코드 독자에게 의도를 전달하는 용도로 사용한다.
  • 실패한 assert는 함수의 인터페이스가 아니며 호출자가 처리 대상으로 삼아서는 안 된다.

Item 82: 재사용 가능한 try/finally에는 contextlibwith

  • with 문은 try/finally 패턴을 재사용하고 코드의 시각적 복잡도를 줄인다.
  • 내장 모듈 contextlib@contextmanager 데코레이터를 사용하면 사용자 정의 컨텍스트 매니저를 쉽게 만들 수 있다.
  • 컨텍스트 매니저에서 yield된 값은 with 문 내부에서 사용할 수 있다.

Item 83: try 블록은 가능한 짧게 유지하라

  • try 블록에 과도한 코드를 넣으면 의도하지 않은 예외까지 잡힐 수 있다.
  • 추가 코드는 else 블록이나 별도의 try 구문으로 분리하는 것이 좋다.

Item 84: 예외 변수의 스코프 소멸에 주의하라

  • except에서 선언된 예외 변수는 해당 블록 내부에서만 유효하다.
  • 이후 스코프에서 사용하려면 다른 변수에 명시적으로 저장해야 한다.

Item 85: 광범위한 Exception 처리에 주의하라

  • except Exception:은 프로그램 일부를 보호하는 데 도움이 될 수 있다.
  • 그러나 의도하지 않은 오류까지 숨길 위험이 있다.
  • 광범위 예외 처리 시에는 반드시 로깅 또는 출력으로 가시성을 확보해야 한다.

Item 86: ExceptionBaseException의 차이를 이해하라

  • 파이썬 내부 동작에서는 BaseException 하위 클래스가 발생할 수 있다.
  • except Exception:은 이를 잡지 않는다.
  • try/finally, with 등은 BaseException도 정상적으로 처리한다.
  • BaseException을 직접 처리하는 것은 신중해야 한다.

Item 87: 향상된 예외 보고에는 traceback 활용

  • 처리되지 않은 예외는 기본적으로 스택 트레이스를 출력한다.
  • 고도의 동시성 환경에서는 트레이스백이 불완전하게 보일 수 있다.
  • traceback 모듈을 사용하면 예외 정보를 세밀하게 제어할 수 있다.

Item 88: 명시적 예외 체이닝으로 트레이스백 명확화

  • except 블록에서 새 예외 발생 시 기존 예외는 __context__에 저장된다.
  • raise ... from ... 구문은 원인 예외를 __cause__로 명시한다.
  • 명시적 체이닝은 오류 원인 분석을 쉽게 만든다.

Item 89: 제너레이터에서 리소스 정리에 주의하라

  • 일반 함수와 달리 제너레이터의 finally는 반복 종료 시 실행된다.
  • 부분 반복 후 참조 해제 시 GeneratorExit 예외가 주입된다.
  • 리소스는 제너레이터 내부 생성보다 외부에서 전달하고 관리하는 편이 안전하다.

Item 90: __debug__False로 설정하지 말라

  • 기본값 True에서 모든 assert가 실행된다.
  • 최적화 옵션 사용 시 assert는 무시된다.
  • assert는 버그 원인 추적에 중요한 역할을 한다.

Item 91: 개발 도구가 아니라면 exec, eval 사용을 피해라

  • eval은 문자열 표현식을 실행한다.
  • exec은 코드 블록을 실행하고 스코프에 영향을 준다.
  • 보안 및 유지보수 위험이 크므로 제한적으로 사용해야 한다.

11. Performance

Item 92: 최적화 전에 반드시 프로파일링하라

  • 최적화에 앞서 프로파일링이 중요한 이유는 성능 저하의 원인이 직관적이지 않은 경우가 많기 때문이다.
  • profile 모듈보다 더 정확한 정보를 제공하는 cProfile 모듈을 사용한다.
  • Profile 객체의 runcall 메서드를 활용하면 함수 호출 트리를 격리된 상태로 측정할 수 있다.
  • Stats 객체를 사용하면 필요한 프로파일링 결과만 선택해 분석할 수 있다.

Item 93: 성능 핵심 코드는 timeit으로 마이크로벤치마크하라

  • 내장 모듈 timeit은 자료구조 및 알고리즘 선택을 과학적으로 비교하는 데 유용하다.
  • 신뢰성 있는 측정을 위해 setup 코드를 사용해 초기화 시간을 제외하고, 결과는 비교 가능한 지표로 정규화한다.
  • python -m timeit CLI를 사용하면 코드 스니펫 성능을 빠르게 확인할 수 있다.

Item 94: 언제 다른 언어로 대체할지 판단하라

  • 다른 언어로 재작성하는 것은 타당한 선택일 수 있으나, 먼저 가능한 최적화 기법을 모두 검토해야 한다.
  • CPU 병목을 C 확장 모듈이나 네이티브 라이브러리로 옮기면 성능 개선 효과가 크다. 단, 비용과 버그 위험이 따른다.
  • 파이썬 생태계에는 적은 변경으로 성능을 향상시키는 다양한 도구와 라이브러리가 존재한다.

Item 95: 네이티브 라이브러리 연동에는 ctypes를 고려하라

  • 내장 모듈 ctypes는 다른 언어로 작성된 네이티브 라이브러리를 쉽게 통합할 수 있게 한다.
  • Python C 확장 API 대비 빌드 복잡도가 낮아 빠른 개발이 가능하다.
  • 단, C 타입 시스템 제약으로 인해 Pythonic한 API 설계에는 한계가 있다.

Item 96: 성능과 사용성을 모두 원한다면 확장 모듈을 고려하라

  • C로 작성된 확장 모듈은 네이티브 속도로 실행되며 Python API와 긴밀히 통합된다.
  • 메모리 관리 및 예외 전파 등 Python API의 특수성은 학습 난이도가 높다.
  • Python 프로토콜 및 내장 데이터 타입을 활용하는 것이 확장 모듈의 가장 큰 가치다.

Item 97: 시작 속도 개선을 위해 바이트코드 캐싱을 활용하라

  • CPython은 소스 파일을 바이트코드로 컴파일한 뒤 가상 머신에서 실행한다.
  • 생성된 바이트코드는 디스크에 캐시되므로, 이후 실행 시 다시 컴파일하지 않아도 된다.
  • 프로그램 시작 속도를 최적화하려면 바이트코드 파일을 미리 생성하고, 운영체제 메모리에 캐시된 상태를 유지하는 것이 가장 효과적이다.

Item 98: 동적 import로 모듈을 지연 로딩하라

  • -X importtime 플래그를 사용하면 모듈 및 의존성 로딩 시간을 확인할 수 있다.
  • 모듈을 함수 내부에서 동적으로 import하면 실제로 기능이 필요할 때까지 초기화를 지연할 수 있다.
  • 이미 로드된 모듈을 다시 확인하는 비용은 매우 작기 때문에, 콜드 스타트 지연을 줄일 수 있다면 충분히 감수할 만하다.

Item 99: memoryviewbytearray로 제로 카피를 활용하라

  • memoryview는 버퍼 프로토콜을 지원하는 객체의 슬라이스를 복사 없이 읽고 쓸 수 있게 한다.
  • bytearray는 변경 가능한 bytes 유사 타입으로, 네트워크 수신 등에서 효율적인 데이터 처리를 지원한다.
  • memoryviewbytearray를 감싸 임의 위치에 데이터를 복사 없이 삽입할 수 있게 한다.

Data Structures and Algorithms

Item 100: 복잡한 정렬 기준에는 key 파라미터를 활용하라

  • listsort 메서드는 문자열, 정수, 튜플 등 기본 타입의 자연 순서를 기준으로 정렬한다.
  • 일반 객체는 특별 메서드로 자연 순서를 정의하지 않는 한 정렬할 수 없다.
  • key 파라미터에 함수를 전달하면 각 원소를 대체할 값을 기준으로 정렬할 수 있다.
  • key 함수가 튜플을 반환하면 여러 정렬 기준을 결합할 수 있다. 단항 마이너스(-)를 사용하면 개별 기준의 정렬 방향을 반전할 수 있다.
  • 음수 변환이 불가능한 타입은 우선순위가 낮은 기준부터 높은 기준 순으로 여러 번 sort를 호출해 해결한다.

Item 101: sortsorted의 차이를 이해하라

  • sort는 제자리(in-place) 정렬로 메모리 사용이 적고 성능이 가장 좋다.
  • sorted는 모든 이터러블을 입력으로 받을 수 있으며 원본 데이터를 변경하지 않는다.

Item 102: 정렬된 시퀀스 검색에는 bisect를 고려하라

  • 정렬된 리스트에서 index나 반복문으로 검색하면 선형 시간(O(n))이 걸린다.
  • bisect 모듈의 bisect_left 함수는 이진 탐색을 사용해 로그 시간(O(log n))에 값을 찾는다.

Item 103: 생산자-소비자 큐에는 deque를 사용하라

  • list로 FIFO 큐를 구현하면 appendpop(0)을 사용할 수 있다.
  • 그러나 pop(0)은 큐 길이가 길어질수록 성능이 급격히 저하된다.
  • collections 모듈의 dequeappendpopleft가 항상 상수 시간(O(1))이므로 큐에 적합하다.

Item 104: 우선순위 큐에는 heapq를 활용하라

  • 우선순위 큐는 중요도 순으로 항목을 처리한다.
  • list 연산으로 구현하면 큐가 커질수록 성능이 비효율적이다.
  • heapq 모듈은 효율적으로 확장 가능한 우선순위 큐 구현을 지원한다.
  • 우선순위를 비교하려면 항목이 자연 정렬 순서를 가져야 하며, 필요하다면 __lt__ 같은 메서드를 정의한다.

Item 105: 로컬 시간 처리에는 datetime을 사용하라

  • 시간대 변환에 time 모듈 사용을 피한다.
  • datetimezoneinfo 모듈을 사용해 신뢰성 있게 시간대 변환을 수행한다.
  • 내부 표현은 항상 UTC로 유지하고, 표시 직전에만 로컬 시간으로 변환한다.

Item 106: 정밀도가 중요하면 decimal을 사용하라

  • 파이썬은 다양한 수치 타입을 제공한다.
  • Decimal 클래스는 금전 계산처럼 높은 정밀도와 반올림 제어가 필요한 경우에 적합하다.
  • 정확한 계산이 필요하면 float 대신 strDecimal 생성자에 전달한다.

Item 107: pickle 직렬화는 copyreg로 유지보수성을 확보하라

  • pickle은 신뢰할 수 있는 프로그램 간 객체 직렬화에 적합하다.
  • 클래스 구조가 변경되면 이전에 직렬화된 데이터 역직렬화가 깨질 수 있다.
  • copyreg 모듈을 사용해 직렬화 방식을 등록하면 하위 호환성을 유지할 수 있다.

13. Testing and Debugging

Item 108: TestCase 하위 클래스로 관련 동작을 검증하라

  • unittest 모듈의 TestCase를 상속하고, 테스트할 동작마다 하나의 메서드를 정의한다. 테스트 메서드 이름은 반드시 test로 시작해야 한다.
  • 내장 assert 대신 assertEqualTestCase가 제공하는 헬퍼 메서드를 사용해 기대 동작을 검증한다.
  • 반복되는 테스트 케이스는 subTest를 활용해 데이터 기반 테스트로 작성한다.

Item 109: 단위 테스트보다 통합 테스트를 우선하라

  • 통합 테스트는 여러 컴포넌트가 함께 동작하는 방식을 검증하고, 단위 테스트는 개별 컴포넌트만 검증한다.
  • 파이썬의 동적 특성상 통합 테스트가 프로그램의 정확성에 대한 신뢰를 얻는 가장 좋은 방법이다.
  • 엣지 케이스가 많은 부분은 단위 테스트로 보완한다.

Item 110: setUp, tearDown으로 테스트를 격리하라

  • TestCasesetUptearDown을 사용해 각 테스트가 독립적으로 실행되도록 환경을 초기화한다.
  • 통합 테스트에서는 모듈 단위의 setUpModule, tearDownModule을 사용해 테스트 모듈 전체 생명주기를 관리한다.

Item 111: 복잡한 의존성은 Mock으로 대체하라

  • unittest.mock 모듈의 Mock 클래스를 사용하면 의존성 동작을 시뮬레이션할 수 있다.
  • assert_called_once_with 계열 메서드로 호출 방식까지 검증한다.
  • 키워드 전용 인자와 patch 함수를 활용해 테스트 대상 코드에 Mock을 주입한다.

Item 112: Mock을 쉽게 하도록 의존성을 캡슐화하라

  • Mock 설정이 반복된다면 의존성을 별도 클래스로 캡슐화해 테스트를 단순화한다.
  • Mock은 클래스를 흉내 내며, 호출 시 또 다른 Mock을 반환해 메서드처럼 동작한다.
  • 엔드투엔드 테스트를 위해 의존성 주입 지점을 명확히 분리하는 리팩터링을 고려한다.

Item 113: 부동소수점 비교에는 assertAlmostEqual을 사용하라

  • 연산 순서에 따라 부동소수점 결과는 미세하게 달라질 수 있다.
  • assertEqual은 전체 정밀도를 비교하므로 테스트가 불안정해질 수 있다.
  • assertAlmostEqual, assertNotAlmostEqual로 허용 오차를 지정한다.

Item 114: pdb로 인터랙티브 디버깅하라

  • 코드 중간에 breakpoint()를 호출하면 디버거를 시작할 수 있다.
  • pdb 명령어로 실행 제어와 상태 확인을 반복할 수 있다.
  • python -m pdb 또는 pdb.pm()으로 예외 발생 이후 디버깅할 수 있다.

Item 115: 메모리 분석에는 tracemalloc을 활용하라

  • 파이썬 프로그램의 메모리 사용과 누수를 파악하기는 쉽지 않다.
  • gc 모듈은 객체 존재 여부는 알 수 있지만, 할당 위치 정보는 제공하지 않는다.
  • tracemalloc 모듈은 메모리 사용의 원인을 추적하는 강력한 도구다.

14. Collaboration

Item 116: 커뮤니티 패키지 위치를 파악하라

  • Python Package Index(PyPI)는 커뮤니티가 유지·관리하는 다양한 패키지를 제공한다.
  • pip 명령어로 PyPI 패키지를 설치할 수 있다.
  • 대부분의 PyPI 패키지는 무료 오픈소스 소프트웨어다.

Item 117: 격리되고 재현 가능한 의존성에는 가상 환경을 사용하라

  • 가상 환경을 사용하면 동일한 머신에서 동일 패키지의 여러 버전을 충돌 없이 설치할 수 있다.
  • python -m venv로 생성하고, bin/activate로 활성화하며, deactivate로 비활성화한다.
  • pip freeze로 의존성을 저장하고, pip install -r requirements.txt로 동일 환경을 재현한다.

Item 118: 모든 함수·클래스·모듈에 독스트링을 작성하라

  • 모든 모듈, 클래스, 함수에 독스트링을 작성하고 코드 변경에 맞춰 유지한다.
  • 모듈 독스트링에는 주요 클래스와 함수의 개요를 설명한다.
  • 클래스 독스트링에는 동작 방식, 중요한 속성, 상속 관련 내용을 문서화한다.
  • type 애너테이션을 사용한다면 중복 정보를 독스트링에 반복하지 않는다.

Item 119: 패키지로 모듈을 구성하고 안정적인 API를 제공하라

  • 패키지는 다른 모듈을 포함하는 모듈이다. 고유한 절대 경로 이름으로 네임스페이스를 분리한다.
  • 디렉터리에 __init__.py 파일을 추가하면 패키지가 된다.
  • __all__ 특수 속성으로 외부에 공개할 API를 명시할 수 있다.
  • __init__.py에서 공개 이름만 import하거나 내부용 이름에 언더스코어를 붙여 구현 세부를 숨긴다.
  • 단일 팀·단일 코드베이스에서는 __all__이 필수는 아니다.

Item 120: 모듈 스코프 코드를 활용해 배포 환경을 구성하라

  • 프로그램은 서로 다른 배포 환경에서 실행될 수 있다.
  • 모듈 스코프에서 일반 파이썬 코드를 사용해 환경별 설정을 적용한다.
  • sys, os 모듈 등을 통해 외부 조건에 따라 모듈 내용을 구성할 수 있다.

Item 121: API 보호를 위해 루트 예외를 정의하라

  • 모듈에서 공통 루트 예외를 정의하고 그 하위 클래스만 발생시키면 호출자가 예외를 쉽게 처리할 수 있다.
  • 루트 예외를 잡으면 API 소비 코드의 버그를 찾는 데 도움이 된다.
  • Exception을 잡으면 API 구현 내부 버그를 찾는 데 유용하다.
  • 중간 루트 예외를 두면 향후 더 구체적인 예외를 추가해도 호환성을 유지할 수 있다.

Item 122: 순환 의존성을 끊는 방법을 이해하라

  • 두 모듈이 import 시점에 서로를 참조하면 순환 의존성이 발생해 프로그램이 시작 시 실패할 수 있다.
  • 공통 의존성을 하위 모듈로 분리해 구조를 재설계한다.
  • 최소한의 리팩터링으로 해결하려면 동적 import를 사용한다.

Item 123: 리팩터링과 마이그레이션에는 warnings를 활용하라

  • warnings 모듈로 API의 deprecated 사용을 알릴 수 있다.
  • -W error 옵션을 사용하면 경고를 예외로 처리해 테스트에서 문제를 조기에 발견할 수 있다.
  • 운영 환경에서는 경고를 로깅 시스템과 연동한다.
  • 경고가 적절한 시점에 발생하는지 테스트로 검증한다.

Item 124: 정적 분석을 위해 typing을 활용하라

  • typing 모듈과 타입 애너테이션 문법으로 타입 정보를 명시한다.
  • 정적 타입 검사 도구는 런타임 이전에 많은 버그를 발견하도록 돕는다.
  • 생산성을 해치지 않도록 점진적으로 도입하고 API 설계와 함께 고려한다.

Item 125: 프로그램 번들링에는 오픈소스 도구를 우선하라

  • 파이썬은 zip 아카이브에서 직접 모듈을 로드할 수 있다.
  • 하지만 많은 패키지가 데이터 파일 및 확장 모듈 의존성 때문에 zip 환경에서 정상 동작하지 않는다.
  • zipapp 대신 Pex 같은 오픈소스 도구를 활용하면 배포 편의성과 안정성을 동시에 확보할 수 있다.

Leave a comment