-
Chpater2 파이썬스러운 코드공부/파이썬 클린코드 2nd Edition 2024. 8. 8. 08:18728x90
관용구
특정 작업을 수행하기 위해 코드를 작성하는 특별한 방법
이 관용구를 따른 코드를 파이썬에서는 파이썬스럽다(Pythonic)고 함
파이썬스러운 코드를 왜 짤까?
1. 일반적으로 더 나은 성능을 내기 때문
2. 코드가
작고 소듕..작고 이해하기 쉽기 때문3. 전체 개발팀이 동일한 패턴과 구조에 익숙해지면=>실수를 줄이고 문제의 본질에 보다 집중 가능
인덱스와 슬라이스
파이썬에서 지원하는 것 중 특이한 점!
음수 인덱스
my_num=(1,2,3,4) my_num[-1] >>>4
slice 지원
*끝 인덱스는 포함하지 않음
my_num=[1,2,3,4] my_num[:2] >>>[1,2] #간격 지정 my_num[::2] >>>[1,3]
slice는 내장 객체이므로 직접 호출도 가능!
s=slice(None,3) my_num[s]==my_num[:3] >>>True
시퀀스란?__getitem__과 __len__을 구현한 객체, 반복 가능 ex)리스트, 튜플, 문자열 등
이 섹션에서는 시퀀스나 이터러블 객체를 만들지 않고 키로 객체의 특정 요소를 가져오는 방법에 대해 다룸
사용자 정의 클래스에 __getitem__을 구현하려는 경우 파이썬스럽게 만들기 위해선 몇 가지를 고려해야함
1. 클래스가 리스트의 래퍼인 경우, 리스트의 동일한 메서드를 호출하여 호환성 유지
from collections.abc import Sequence class Items: def __init__(self, *values): self._values=list(values) def __len__(self): return len(self._values) def __getitem__(self, item): return Self._values.__getitem__(item)
*클래스가 시퀀스임을 선언하기 위해 collections.abc 모듈의 Sequence 인터페이스를 구현해야함
이렇게 해야 해당 클래스가 어떤 클래스인지 바로 알 수 있고, 필요한 요건들 강제 구현하게 됨
여기서는 컴포지션을 사용함
2. collections.UserList 부모 클래스를 상속
3. 자신만의 시퀀스를 구현할 수도 있음
- 범위로 인덱싱하는 결과는 해당 클래스와 같은 타입의 인스턴스여야함
(미묘한 오류를 방지하기 위해)
ex) 튜플에서 특정 range를 요청하면 결과는 튜플, 문자열의 substring결과는 문자열
- slice에 의해 제공된 범위는 파이썬이 하는 것처럼 마지막 요소는 제외해야함
(일관성을 위해, 익숙하니까)
컨텍스트 관리자
리소스 관리와 관련해서 자주 봄
ex)파일을 열면 누수를 막기 위해 작업이 끝나면 적절히 닫히길 기대
그러나 예외나 오류가 발생한다면?
가능한 모든 조합과 실행 경로를 처리하여 디버깅하는 것이 어렵기 때문에 finally 블록에 정리 코드를 넣는 방법이 있음
fd=open(filename) try: process_file(fd) finally: fd.close() #파이썬스럽게 구현한다면? with open(filename) as fd: process_file(fd)
with문은 컨텍스트 관리자로 진입하게 함
open함수는 컨텍스트 관리자 프로토콜을 구현
=>예외가 발생해도 블록이 완료되면 파일이 자동으로 닫힘
컨텍스트 관리자는 __enter__와 __exit__로 구성
파이썬스럽게 구현한 코드를 좀 더 자세히 살펴보면
1. with문은 __enter__를 호출=>무엇을 return하든 as 이후에 지정된 변수에 할당
2. 해당 라인이 실행되면 다른 파이썬 코드가 실행될 수 있는 새로운 컨텍스트로 진입
3. 해당 블록에 대한 마지막 문장이 끝나면 컨텍스트 종료=>파이썬이 처음 호출한 원래 컨텍스트 관리자 객체의 __exit__호출
블록 내에서 예외/오류가 발생해도 __exit__는 호출됨
컨텍스트 관리자는 리소스 관리에서도 자주 사용되지만,
블록 전후에 필요로 하는 특정 로직을 제공하기 위해서도 사용됨
특히, 관심사를 분리하고 독립적으로 유지되어야하는 코드를 분리할 때 좋은 방법
ex)스크립트를 사용해서 DB백업을 오프라인에서 해야하는 상황
즉, DB를 실행하고 있지 않은 동안에만 백업 할 수 있고, 이를 위해선 서비스를 중지해야하는 상황
백업이 끝나면 백업 프로세스가 성공적으로 끝났는지와 관계없이 프로세스를 다시 시작해야한다면?
1. 서비스를 중지하고 백업하고 예외/특이사항 처리하고 서비스 다시 시작하는 거대 단일 함수 만들기
2. 컨텍스트 관리자를 사용해서 구현하기
- contextlib.contextmanager 데코레이터 예제
해당 함수의 코드를 컨텍스트 관리자로 변환시켜줌
import contextlib #1. 해당 데코레이터를 실행하면 @contextlib.contextmanager def db_handler(): try: stop_database() #2. yield문 앞의 모든 것은 __enter__의 일부처럼 취급,여기서 제너레이터 함수 중지되고 컨텍스트 관리자 진입 yield finally: #3. yield문 뒤의 모든 것은 __exit__의 일부로 볼 수 있음 start_database() with db_handler(): db_backup()
- 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")
데코레이터를 사용하면 로직을 한번만 정의하면 됨
변하지 않는 동일한 로직을 필요한 곳에 원하는 만큼 재사용 가능
- contextlib 기능
안전하다고 확신하는 경우 해당 예외를 무시하는 기능
try/except 블록에서 코드 실행하고 예외를 전달하거나 로그를 남기는 것과 비슷하지만,
suppress메서드를 호출하면 로직에서 자체적으로 처리하고 있는 예외임을 명시
import contextlib #DataConversionException : 입력 데이터가 이미 기대한 것과 같은 포맷이라서 변환할 필요가 없음 with contextlib.suppress(DataConversionException): parse_data(input_json_or_dict)
뭐라는 겨....컴프리헨션과 할당 표현식
nums=[] for i in range(10): nums.append(i) nums=[i for i in range(10)]
개꿀이죠?
프로퍼티, 속성(attribute)와 객체 메서드의 다른 타입들
접근 제어자를 가지는 다른 언어들과 다르게 파이썬은 모든 속성과 함수가 public
_로 시작하는 속성은 private속성을 의미, 외부에서 호출되지 않기를 기대(금지시키는 것은 아님)
__로 시작하면 이름 맹글링이라는 것을 함, 변수이름을 _<class_name>__<attribute-name>으로 변경
프로퍼티
파이썬에서는 setter와 getter 메서드를 더 간결하게 캡슐화가능
내부 데이터를 일관성있고 투명하게 관리하기 위해
ex)좌표값을 처리하는 지리 시스템
위도와 경도는 특정 범위에서만 의미가 있고, 벗어나면 좌표는 존재 불가
즉, 좌표를 나타내는 객체를 생성할 수 있지만 항상 범위 내에 존재하는 지 확인해야함
class Coordinate: def __init__(self, lat:float, long:float)->None: self._latitude=self._longitude=None self.latitude=lat self.longitude=long @property def latitude(self)->float: return self._latitude @latitude.setter def latitude(self, lat_value:float)->None: if lat_value not in range(-90,90+1): raise ValueError(f"떙!") self._latitude=lat_value @property def longitude(self)->float: return self._longitude @latitude.setter def longitude(self, long_value:float)->None: if lat_value not in range(-180,180+1): raise ValueError(f"떙!") self._longitude=long_value
객체의 상태나 내부 데이터에 따라 어떤 계산을 하고 싶은 경우
특정 포맷이나 데이터 타입으로 값을 반환해야 하는 객체가 있는 경우
ex)소수점 이하 4자리까지 좌표값을 반환하기로 결정했다면?
프로퍼티는 명령-쿼리 분리 원칙(CCO8 - command and query separation)을 따르기 위한 좋은 방법
더보기명령-쿼리 분리 원칙?
객체의 메서드가 무언가의 상태를 변경하는 커맨드이거나 무언가의 값을 반환하는 쿼리이거나 둘 중 하나만 수행해야지 둘 다 동시에 수행하면 안된다는 것
걍 객체의 메서드는 한 가지 일만 해야한다는 내용
보다 간결한 구문으로 클래스 만들기
dataclass사용
class Icecream: def __init__(self, name:str, flavor:str): self.name=name self.flavor=flavor @dataclass class Icecream: name : str flavor : str
이터러블 객체
반복 가능한 객체 ex)리스트, 튜플, set, dict 등
- __next__나 __iter__ 이터레이터 메서드 중 하나를 포함하는가?
- 시퀀스이고 __len__과 __getitem__을 모두 가졌는가?
객체를 반복하려고 하면 파이썬은 해당 객체의 iter()함수를 호출
해당 객체에 __iter__메서드가 있는 지 확인 후, __iter__메서드 실행
객체에 __iter__메서드가 정의되어 있지 않으면 __getitem__을 찾고 , 없으면 TypeError 발생시킴
컨테이너 객체
__contains__메서드를 구현한 객체
e in container == container.__contains__(e)
객체의 동적인 속성
__getattr__를 사용하면 객체가 속성에 접근하는 방법을 제어가능
class DA: def __init__(self, a): self.a=a def __getattr__(self, attr): if attr.startswith("fallback_"): name=attr.replace("fallback_","") return f"[fallback resolved] {name}" raise AttributeError(f"{self.__class__.__name__}에는 {attr} 속성이 없음.") d= DA("value") #객체에 정상적으로 존재하는 속성에 접근하여 그 값을 그대로 반환 d.a >>>'value' #객체에 존재하지 않는 fallback_test라는 속성에 접근 d.fallback_test >>>'[fallback resolved] test' #새로운 속성 추가 d.__dict__["fallback_new"]="new value" d.fallback_new >>>'new value' #뭐라는겨 getattr(d,"something","default") >>>'default'
호출형 객체(callable)
객체를 일반 함수처럼 호출하면 __call__메서드가 호출됨
이 때 객체 호출시 사용된 모든 파라미터는 __call__에 그대로 전달
ex)동일한 파라미터 값으로 몇 번이나 호출되는지 카운트하기 위해 __call__메서드를 사용하는 예시
from collections import defaultdict class CallCount: def __init__(self): self._counts = defaultdict(int) def __call__(self, argument): self._counts[argument] += 1 return self._counts[argument] cc=CallCount() cc(1) >>>1 cc(2) >>>1 cc(1) >>>2 cc(1) >>>3 callable(cc) >>>True
매직 메서드 요약
사용 예 매직 메서드 비고 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) 호출형 객체 파이썬에서 유의할 점
변경 가능한(mutable) 파라미터의 기본 값
변경 가능한 객체를 함수의 기본 인자로 사용하면 안됨
def w(user_metadata : dict={"name":"John","age":30}): name=user_metadata.pop("name") age=user_metadata.pop("age") return f"{name} ({age})" w() >>>'John (30)' w({"name":"JYP","age":52}) >>>'JYP (52)' w() >>>에러!KeyError:'name'
KeyError발생하는 이유?
당연하게도 name이랑 age가 제거돼서
어케 바꾸면 될까용?
def w(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})"
내장(built-in) 타입 확장
내장 타입을 확장하는 올바른 방법은 collections모듈을 사용하는 것
calss B(list): def __getitem__(self, index): value = super().__getitem__(index) if index % 2 == 0: prefix="짝수" else: prefix="홀수" return f"[{prefix}] {value}" b=B((0,1,2,3,4,5)) b[0] >>>'[짝수] 0' b[1] >>>'[홀수] 1' "".join(b) >>>에러!TypeError : sequence item 0 : expected str instance, int found
join은 반복 가능한 형태의 원소들을 합치는 함수
int를 합치면 당연히 오류가 나겠지요?
근데 알고보면 얘는 파이썬의 C구현체인 CPython에서만 발생
PyPy에서는 안 생긴다고 함
여튼 오류는 안 좋으니까 고쳐주자
from collections import UserList calss B(UserList): def __getitem__(self, index): value = super().__getitem__(index) if index % 2 == 0: prefix="짝수" else: prefix="홀수" return f"[{prefix}] {value}" b=B((0,1,2,3,4,5)) b[0] >>>'[짝수] 0' b[1] >>>'[홀수] 1' "".join(b) >>>'[짝수] 0[홀수] 1[짝수] 2'
비동기 코드에 대한 간략한 소개
중지 가능한 코드가 있다면 그동안 다른 코드를 실행하자~