
파이썬 클린코드 2nd edition 을 읽고 정리한 내용입니다.
Pythonic 파이썬스럽다
파이썬으로 작업을 처리하는 고유한 관용구를 따른 코드
파이썬스러운 코드를 작성하는 이유
- 일반적으로 더 나은 성능을 보여줌
- 이해하기가 쉬우며, 실수를 줄이고 문제의 본질에 집중할 수 있게 됨
인덱스와 슬라이스
파이썬에서는 음수를 사용하여 끝에서부터 접근할 수 있고(다른 언어와 다른 점), 슬라이스를 활용하여 특정 구간의 요소를 가져올 수도 있다. 슬라이스를 사용하는 경우, 시작 인덱스는 포함되지만 마지막 인덱스는 제외된다.
슬라이스의 경우 내장객체이므로 이렇게 호출을 할 수도 있다.
my_numbers = (1,2,3,4,5,6,7,8,9,10)
interval = (1,7,2) # 시작, 중지, 간격
my_numbers[interval]
자체 시퀀스 생성
이러한 기능은 __getitem__이라는 매직키워드로 인해서 작동한다. __getitem__은 myobj[key]에서 key에 해당하는 값을 파라미터로 전달하게 된다. 사용자 정의 클래스에서 __getitem__을 구현하는 경우 인덱스나 슬라이스와 같은 기능을 사용할 수 있다.
이렇게 사용자가 정의한 시퀀스에서는 다음과 같은 사항을 주의해야 한다.
- 범위로 인덱싱하는 결과는 해당 클래스와 같은 타입의 인스턴스여야 함(미묘한 오류 방지 목적)
- 슬라이스에 의해 제공된 범위는 파이썬이 하는 것처럼 마지막 요소는 제외해야 함(일관성 유지 목적)
컨텍스트 관리자
패턴에 잘 대응되기 때문에 유용하게 활용할 수 있다. 작업이 비정상적으로 종료되었을 경우에도 할당된 모든 리소스를 해제해줘야 하는데, 이런 경우 일반적으로는 finally를 이용하지만 파이썬스러운 방식으로는 다음과 같이 해결 가능하다.
# 일반적인 경우
fd = open(filename)
try :
process_file(fd)
finally :
fd.close()
# 파이썬스러운 코드
with open(filename) as fd:
process_file(fd)
이렇게 구현하는 경우 예외사항이 생기더라도 해당 블록이 완료되면 자동으로 파일이 종료된다.
컨텍스트 관리자는 __enter__와 __exit__두 개의 매직 매서드로 구성되어 있다. with문에서는 __enter__를 호출해서 그 결과를 as로 할당한다. 이후 작업을 위한 새로운 컨텍스트로 진입 후 __exit__이 호출되고 작업이 종료되는 순서로 진행된다.
이러한 컨텍스트 관리자는 독립적으로 유지되어야 하는 코드를 분리하는 좋은 방법이다.
제한적인 리소스를 효과적으로 활용하기 위해서 사용이 끝난 리소스는 종료해주어야 하는데, 계속 열려있다면 resource leak현상이 발생하게 된다. 하지만 복잡한 과정을 수행하는 경우 오류가 발생할 확률은 높아지고, finally를 통해서 파일닫기를 구현해 놓았더라도 파일 닫기를 수행하기 전에 오류가 발생해 닫을 수 없게 된다.(리소스 계속 열려있음) 그래서 컨텍스트 관리자를 통해 작업의 시작과 끝에 특정 행동을 수행할 수 있도록 관리해주며, 이를 통해 과정에서 불필요한 부분에 대한 신경을 덜 쓸 수 있게 된다.
컨텍스트 관리자 구현
컨텍스트 관리자는 __enter__, __exit__ 매직메서드만 구현하면 컨텍스트 관리자 프로토콜을 지원할 수 있게 되며, 이 외에도 다른 방법은 존재한다. contextlib모듈을이용한다면 컨텍스트 관리자를 보다 쉽게 구현할 수 있다.
contextlib.contextmanager 데코레이터를 적용하면 해당 함수의 코드를 컨텍스트 관리자로 변환한다.
import contextlib
@contextlib.contextmanager
def db_handler():
try :
stop_database()
yield
finally :
start_database()
with db_handler():
db_backup()
contextlib.contextmanager를 적용하기 위해서는 제너레이터 함수라는 형태가 되어야 하는데, 위의 예시에서는 yield를 사용하여 제너레이터 함수를 만들었다. 이렇게 데코레이터를 적용하면 yield앞의 모든 것이 __enter__메서드의 일부로 취급되고, 그 뒤가 __exit__으로 취급된다.
contextlib.ContextDecorator 클래스를 이용하는 방법도 있다.
class dbhandler_decorator(contextlib.ContextDecorator):
def __enter__(self):
stop_database()
return self
def __exit__(self, ext_type, ex_value, ex_traceback):
start_database()
@dbhandler_decorator
def offline_backup():
run("pg_dump database")
이 클래스를 이용하는 경우, 보통의 컨텍스트 관리자와 같이 __enter__과 __exit__을 구현해줘야 한다. 대신 with문이 없고 함수를 호출하기만 하면 자동으로 실행된다.
contextlib.supress라는 함수를 이용한다면 안전하다고 생각되는 경우, 예외를 무시하는 기능을 한다. 이 경우 로직에서 자체적으로 처리하는 예외임을 명시하게 된다.
import contextlib
with contextlib.supress(DataConversionException) :
parse_data(input_json_or_dict)
제너레이터 함수
제너레이터 함수는 이터레이터를 생성하는 함수. 이터레이터의 조건들이 있지만, 제너레이터는 yield만 사용해주면 되기 때문에 훨씬 간단하게 작성할 수 있다. 또한 next를 통해 차례대로 값에 접근하므로 사이즈가 커져도 메모리 사용량은 동일하다.(메모리 효율적)
yield
일반적인 함수에서는 결과 값을 호출부로 반환하고, 함수를 종류시킨 후 메모리에서 정리하게 된다. 하지만 (제너레이터 함수에서) yield를 만나면 그 상태에서 정지 후 값을 반환하기 때문에 그 상태가 그대로 유지되게 된다. 유지된다는 것이 포인트
참고 : https://dojang.io/mod/page/view.php?id=2412
컴프리헨션과 할당 표현식
컴프리헨션을 사용하면 간결하게 코드를 작성할 수 있고, 가독성 또한 높아지게 된다. 또한 데이터에 대해 변환을 해야하는 경우에는 할당 표현식을 사용할 수 있다.
# 일반
numbers = []
for i in range(10):
numbers.append(run_calculation(i))
# 컴프리헨션
numbers = [run_calculation(i) for i in range(10)]
- 만약 두 함수의 내부 차이가 궁금한 경우 dis.dis(func)을 이용하여 내부 어셈블리 코드를 확인할 수 있다.
할당표현식
:= 를 이용해 표현식 안에서 변수에 값을 할당하는 방법으로, 기호 때문에 바다코끼리 연산자라고도 부른다. (파이썬 3.8이상부터 사용 가능)
만약 비교하는 부분이 없으면, 괄호도 생략이 가능해진다.# 할당표현식 미사용 word = 'hi' word_len = len(word) if word_len < 10: print('말이 짧다') # 할당표현식 사용 word = 'hi' if (word_len := len(word)) < 10: print('말이 짧다')
# while에서의 예시 import random while (i := random.randint(1, 6)) != 3: print(i) # list에서의 예시 def ten(): return 10 b = [a := ten(), a + 1, a + 2]참고 : https://dojang.io/mod/page/view.php?id=2480
프로퍼티, 속성과 객체 메서드의 다른 타입들
파이썬 객체의 모든 속성과 함수는 퍼블릭이며, 호출자가 객체의 속성을 호출하지 못하도록 할 수 없다. 하지만 밑줄 하나로 시작하는 속성의 경우에는 private속성을 의미하며, 외부에서 호출을 금지시켜주지는 않지만 외부에서 호출되지 않기를 희망할 수는 있다.
class Connector:
def __init__(self, source):
self.source = source
self._timeout = 60
위의 Connector의 경우 source파라미터를 통해 생성되고, source와 timeout이라는 두 개의 속성을 가지고 있다. source는 퍼블릭, timeout은 프라이빗이다. 이 경우 timeout은 Connector에서만 사용되고 외부에서는 호출하지 않을 것이기 때문에 이 속성을 변경할 때 생길 파급 효과를 고려하지 않아도 된다.
밑줄 두 개를 사용하는 경우 이름 맹글링(name mangling)이라고 하며, 이는 변수의 이름을 "_<class_name>__<attribute_name>" 으로 변경하는 역할을 한다. 이것은 클래스의 메서드를 이름 충돌 없이 오버라이드 하기 위해서 만들어졌으므로, 밑줄 하나와는 다른 경우이다.
프로퍼티
객체의 일부 속성을 퍼블릭으로 공개하고 싶을 때 일반적으로는 프로퍼티를 사용한다. 이렇게 프로퍼티를 사용하는 경우 getter, setter를 간결하게 캡슐화 할 수 있다. 또한 이런 프로퍼티를 이용해서 내부의 데이터를 일관성있게 관리할 수 있게 된다.
프로퍼티
* getter : 값을 가져오는 메서드
* setter : 값을 저장하는 메서드
@property를 이용하면 getter와 setter를 간단하게 구현할 수 있다. 값을 가져오는 메서드에는 @property 값을 저장하는 메서드에는 @메서드이름.setter를 붙이면 값을 저장하고, 가져오는걸 손쉽게 할 수 있게 된다.
참고 : https://dojang.io/mod/page/view.php?id=2476
보다 간결한 구문으로 클래스 만들기
파이썬에서는 보일러 플레이트라고하는 값을 초기화 하는 코드가 있다.
def __init__(self, x,y):
self.x = x
self.y = y
dataclasses모듈에서 제공하는 @dataclass 데코레이터를 활용하면 __init__메소드를 자동으로 생성하므로 이러한 코드를 더 단순화시킬 수 있다. 또한 field라는 객체도 제공하는데, 해당 속성에 특별한 특징이 있다는 것을 표시하는 것이다.
이터러블 객체
파이썬에는 리스트, 튜플, 세트, 딕셔너리 등 기본적으로 반복 가능한 객체를 지원하고 있지만, 이런 것 이외에도 나만의 이터러블을 만들 수도 있다. 이렇게 하기 위해서는 매직 메서드를 이용할 수 있다.
이터러블 객체의 조건
- __iter__또는 __next__메서드가 포함
- __len__과__getitem__를 가지는 시퀀스
시퀀스는 __len__과 __getitem__을 구현하고, 포함된 요소를 한 번에 하나씩 차례대로 가져올 수 있어야 한다. 그렇지 않으면 반복이 동작하지 않게 된다.
이터러블을 사용하면 메모리는 적게 사용하는 대신 O(n)의 시간 복잡도가 소요되는 반면, 시퀀스로 구현하는 경우에는 메모리가 더 사용되는 대신 시간복잡도가 O(1)에 그치게 된다.
시간복잡도
시간복잡도란 알고리즘의 성능을 설명하는 것, 시간복잡도가 작을수록 효율적인 알고리즘임을 의미한다.
시간복잡도를 표기하는 방법
* Big-O(빅-오) : 상한
* Big-Ω(빅-오메가) : 하한
* Bid-θ(빅-세타) : 평균
O(1)은 상수, O(n)은 직선, O(log n) 로그시간 등이 있다.
참고 :
https://blog.chulgil.me/algorithm/
https://www.ics.uci.edu/~pattis/ICS-33/lectures/complexitypython.txt
컨테이너 객체
컨테이너는 __contains__메서드를 구현한 객체로 객체가 가지고 있는 데이터들을 가리키는 주소를 가지고 있는 객체이다. __contains__를 사용하면 값이 존재하는지를 판단하여 boolean값을 반환하는데, 잘 사용하면 코드의 가독성이 높아지는 효과가 있다.
객체의 동적인 속성
__getattr__ 매직 매서드를 사용하면 객체가 속성에 접근하는 방법을 제어할 수 있다. <myobj>.<myattribute>형태로 객체의 속성에 접근하려고 하는 경우, 일단 먼저 속성 __dict__에서 호출하고자 하는 속성이 있는지 확인 후에 그 속성 객체에 대해 __getattribute__를 호출한다. 만약 해당 속성이 존재하지 않는다면 조회하려는 속성의 이름을 파라미터로 __getattr__을 호출하게 되는데, 기본적으로 해당 속성이 없다고 안내하거나 오류를 발생시킬 수 있다.
호출형 객체
__call__은 객체를 일반 함수처럼 호출 시 호출되는 매직매서드로, 호출 시 사용된 파라미터가 모두 __call__메서드에 그대로 전달된다. 이렇게 활용하면, 객체에 상태 저장이 가능하기 때문에 호출이 발생할 때 정보를 저장하고 나중에 활용이 가능하다는 장점이 있다.
from collections import defalutdict
clas CallCount :
def __init__(self) :
self._counts = defaultdidct(int)
def __call__(self, argument) :
self._counts[argument] += 1
return self._counts[argument]
# 실행
cc = CallCount()
cc(1)
>>> 1
cc(2)
>>> 2
매직 메서드
| 사용 예 | 매직 메서드 | 비고 |
| obj[key] obj[i:j] obj[i:j:k] |
__getitem__(key) |
첨자형 객체 |
| with obj : | __enter__ __exit__ |
컨텍스트 관리자 |
| for i in obj : | __iter__ / __next__ __len__ / __getitem__ |
이터러블 객체 시퀀스 |
| obj.<attribute> | __getattr__ | 동적 속성 조회 |
| obj(*args, **kwargs) | __call__(*args, **kwargs) | 호출형 객체 |
파이썬에서 유의할 점
발생하는 잠재적인 문제들을 피할 수 있는 관용적인 코드를 작성하는 것 또한 중요하다.
변경 가능한 파라미터의 기본 값
변경 가능한 객체를 함수의 기본 인자로 사용하면 안되며, 이 인자를 직접 수정하는 것 또한 하지 말아야 한다.
# Wrong
def wrong_ex(user_metadata: dict = {"name": "John", "age": 30}):
name = user_data.pop("name")
age = user_data.pop("age")
return f'{name} ({age})'
# Good
def user_display(user_metadata:dict = None);
user_metadata = user_metadata or {"name": "John", "age": 30}
name = user_metadata.pop("name")
age = user_metadata.pop("age")
return f'{name} ({age})'
내장 타입 확장
colections모듈을 사용하여 내장 타입을 올바르게 확장해야 한다. collections.UserDict / collections.UserList 를 사용하면 관련된 모든 부분을 찾아서 업데이트 해주기 때문에 중간에 적용이 안되는 등의 문제를 해결할 수 있다.
from collections import UserList
clas GoodList(UserList) :
def __getitem__(self, index):
value = super().__getitem__(index)
if index % 2 == 0 :
prefix = "짝수"
else :
prefix = "홀수"
return f"[{prefix}] {value}"
2장...부터 너무어렵다
마지막에 변경가능한 값을 함수의 기본 인자로 사용하면 안된다는 걸 보고 매우 찔렸다......
왜냐면 지금 만든 함수들을 리스트로 엄청나게 많이 받고있기 때문이다....
'Python > 파이썬클린코드' 카테고리의 다른 글
| [파이썬 클린코드] Chapter5 : 데코레이터를 사용한 코드 개선 (1) | 2023.03.28 |
|---|---|
| [파이썬 클린코드] Chapter4 : SOLID원칙-2 (0) | 2023.03.17 |
| [파이썬 클린코드] Chapter4 : SOLID원칙-1 (0) | 2023.03.16 |
| [파이썬 클린코드] Chapter3 : 좋은 코드의 일반적인 특징 (0) | 2023.03.12 |
| [파이썬 클린코드] Chapter1 : 코드 포맷팅과 도구 (0) | 2023.02.26 |