일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |
- docker
- test-helper
- 백준
- 딥러닝
- 그리디
- 머신러닝
- Object detection
- ubuntu
- 3단계
- STL
- 모두를 위한 딥러닝 강좌 시즌1
- pytorch
- 1단계
- 구현
- 프로그래머스
- AWS
- 실전알고리즘
- 이것이 코딩테스트다 with 파이썬
- 2단계
- ssd
- cs
- 자료구조 및 실습
- 전산기초
- 파이썬
- Python
- MySQL
- CS231n
- SWEA
- 코드수행
- C++
- Today
- Total
곰퓨타의 SW 이야기
CH 2. 파이썬스러운 코드 본문
파이썬스럽다 (pythonic)
관용구 : 특정 작업을 수행하기 위해 코드를 작성하는 특별한 방법이다
파이썬에서 관용구를 따른 것을 파이썬스럽다(Pythonic)이라 한다.
1. 인덱스와 슬라이스
첫번쩨 요소의 인덱스는 0부터 시작한다.
파이썬은 음수 인덱스를 통해 끝에서부터 접근이 가능하다.
>>> my_numbers = (4,5,3,9)
>>> my_numbers[-1]
9
>>> my_numbers[-3]
5
하나의 요소를 얻는 것 외에도 slice를 사용하여 특정 구간의 요소를 구할 수 있다.
[x:y] --> x, x+1, ... y-1까지의 요소 (x번째 이상부터 y번째 미만까지)
x, y 파라미터가 없는 경우에는 튜플의 복사본을 만든다.
>>> my_numbers = (1,1,2,3,5,8,13,21)
>>> my_numbers[2:5]
(2,3,5)
>>> my_numbers[:3]
(1,1,2)
>>> my_numbers[3;]
(3,5,8,13,21)
>>> my_numbers[::] # my_numbers[:]도 마찬가지로 복사본을 만든다.
(1,1,2,3,5,8,13,21)
>>> my_numbers[1:7:2] # 1번과 7번 미만의 인덱스 사이에 있는 요소를 두 칸씩 점프
(1,3,8)
slice함수 내장 객체
>>> interval = slice(1,7,2)
>>> my_numbers[interval]
(1,3,8)
>>> interval = slice(None, 3)
>>> my_numbers[interval] == my_numbers[:3]
True
튜플, 문자열, 리스트의 특정 요소를 가져오려고 한다면 for 루프를 돌며 수작업으로 요소를 선택하지 말고 이와 같은 방법을 사용하는 것이 좋다.
자체 시퀀스 생성
이는 __getitem__이라는 매직 매서드(매직매서드는 파이썬에서 특수한 동작을 수행하기 위해 예약한 메서드로 이중 밑줄로 둘러싸여 있다.) 이는 myobject[key]와 같은 형태를 사용할 때 호출되는 메서드로 key에 해당하는 대괄호 안의 값을 파라미터로 전달한다.
업무 도메인에서 사용하는 사용자 정의 클래스에 __getitem__을 구현하려는 경우 파이썬스러운 접근 방식을 따르기 위해 몇 가지를 고려해야 한다.
클래스가 표준 라이브러리 객체를 감싸는 래퍼(wrapper)인 경우 기본 객체에 가능한 많은 동작을 위임할 수 있다. 즉 클래스가 리스트의 래퍼인 경우 리스트의 동일한 메서드를 호출하여 호환성을 유지할 수 있다.
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. 컨텍스트 관리자
- __enter__와 __exit__ 두 개의 매직 매서드로 구성된다.
- with 문은 enter 메서드를 호출하고 이 메서드가 무엇을 반환하든 as 이후에 지정된 변수에 할당된다.
- 해당 블록에 대한 마지막 문장이 끝나면 컨텍스트가 종료되며 exit 메서드를 호출한다
- 블록 내에 예외 또는 오류가 있는 경우에도 exit 메서드가 여전히 호출되며, 예외는 파라미터로 확인 가능하다.
- True를 반환하면 잠재적으로 발생한 예외를 호출자에게 전파하지 않고 정지함을 의미하나 좋지 않은 습관이다.
import contextlib
run = print
def stop_database():
run("systemctl stop postgresql.service")
def start_database():
run("systemctl start postgresql.service")
class DBHandler:
def __enter__(self):
stop_database()
return self
def __exit__(self, exc_type, ex_value, ex_traceback):
start_database()
def db_backup():
run("pg_dump database")
def main():
with DBHandler():
db_backup()
main()
컨텍스트 관리자 구현
- contextlib 모듈을 활용한 컨텍스트 관리자 구현 가능
- contextlib.contextmanager 데코레이터를 적용하면 해당 함수의 코드를 컨텍스트 관리자로 변환한다.
- contextlib.ContextDecorator를 사용하면 with 문 없이 완전히 독립적인 실행이 가능하다.
- 컨텍스트 관리자 내부에서 사용하고자 하는 객체를 얻을 수 없다.
- e.g ) with offline_back() as bp: 처럼 사용할 수 없다.
- with contextlib.suppress(DataConversionException)은 로직 자체적으로 처리하고 있음을 예외임을 명시하고 실패하지 않도록 한다.
import contextlib
@contextlib.contextmanager
def db_handler():
try:
stop_database() # yield 전은 __enter__를 의미한다.
yield # yeild는 제네레이터임을 의미한다. yield 뒤에는 반환 값을 작성
finally:
start_database() # finally 구문은 __exit__를 의미한다.
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")
def main():
with DBHandler():
db_backup()
with db_handler():
db_backup()
offline_backup()
if __name__ == "__main__":
main()
3. comprehension 과 할당 표현식
import re
from typing import Iterable, Set
ARN_REGEX = re.compile(r"arn:aws:[a-z0-9\-]*:[a-z0-9\-]*:(?P<account_id>\d+):.*")
def collect_account_ids_from_arns(arns: Iterable[str]) -> Set[str]:
"""Given several ARNs in the form
arn:partition:service:region:account-id:resource-id
Collect the unique account IDs found on those strings, and return them.
"""
collected_account_ids = set()
for arn in arns:
matched = re.match(ARN_REGEX, arn)
if matched is not None:
account_id = matched.groupdict()["account_id"]
collected_account_ids.add(account_id)
return collected_account_ids
def collect_account_ids_from_arns2(arns: Iterable[str]) -> Set[str]:
matched_arns = filter(None, (re.match(ARN_REGEX, arn) for arn in arns))
return {m.groupdict()["account_id"] for m in matched_arns}
def collect_account_ids_from_arns3(arns: Iterable[str]) -> Set[str]:
return {
matched.groupdict()["account_id"]
for arn in arns
if (matched := re.match(ARN_REGEX, arn)) is not None
}
4. 프로퍼티, 속성 (attribute)와 객체 메서드의 다른 타입들
- 파이썬 객체의 모든 프로퍼티와 함수는 public 이다.
- 따라서 엄격한 경계사항은 없지만 밑줄로 시작하는 속성은 해당 객체에 대해 private를 의미하며 외부에서 호출하지 않기를 기대한다.
파이썬에서의 밑줄
- 이중 밑줄은 여러 번 확장되는 클래스의 메서드를 이름 충돌 없이 오버라이드하기 위해 만들어진다.
- 이중 밑줄이 아닌 하나의 밑줄을 사용하는 파이썬의 관습을 지킬 것
class Connector:
def __init__(self, source):
self.source = source
self._timeout = 60 # 하나의 밑줄
conn = Connector("postgresql://localhost")
conn._timeout = 70
print(conn.__dict__)
# {'source': 'postgresql://localhost', '_timeout': 70}
class Connector:
def __init__(self, source):
self.source = source
self.__timeout = 60 # 두 개의 밑줄
conn = Connector("postgresql://localhost")
conn.__timeout # "접근할 수 없다"가 아닌 "존재하지 않는다"는 오류 발생
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-8-bd7c24031309> in <module>
6 conn = Connector("postgresql://localhost")
7
----> 8 conn.__timeout # "접근할 수 없다"가 아닌 "존재하지 않는다"는 오류 발생
AttributeError: 'Connector' object has no attribute '__timeout'
conn._Connector__timeout = 80 # __함수에 대한 접근 방법
print(conn.__dict__)
{'source': 'postgresql://localhost', '_Connector__timeout': 80}
프로퍼티
- 객체의 어떤 속성에 대한 접근을 제어하려는 경우 사용한다. (잘못된 정보의 입력 제한 등)
- getter, setter 의 역할
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 -90 <= lat_value <= 90:
self._latitude = lat_value
else:
raise ValueError(f"{lat_value} is an invalid value for latitude")
@property
def longitude(self) -> float:
return self._longitude
@longitude.setter
def longitude(self, long_value: float) -> None:
if -180 <= long_value <= 180:
self._longitude = long_value
else:
raise ValueError(f"{long_value} is an invalid value for longitude")
이터러블 객체
- 반복 가능한지 확인하기 위해 파이썬은 고수준에서 다음 두 가지를 차례로 검사한다.
- 객체가 __next__ 나 __iter__이터레이터 메소드 중 하나를 포함하는지 여부
- 객체가 시퀀스이고 __len__과 __getitem__을 모두 가졌는지 여부
이터러블 객체 만들기
- 객체를 반복하려고 하면 파이썬은 해당 객체의 iter() 함수 호출
- for문은 StopIteration 예외가 발생할 떄까지 next()를 호출하는 것과 같다.
- 한 번 실행하면 끝의 날짜에 도달한 상태이므로 이후에 호출하면 계속 StopIteration 예외가 발생하는 문제
- 제너레이터를 사용하면 이런 문제를 해결할 수 있다. --> 컨테이너 이터러블
시퀀스 만들기
- 이터러블은 메모리를 적게 사용하지만 n번째 요소를 얻기 위한 시간복잡도는 O(n) 이다.
- 시퀀스로 구현하면 더 많은 메모리가 사용되지만 (모든 것을 보관해야 하므로) 특정 요소를 가져오기 위한 인덱싱 시간 복잡도는 O(1)로 상수에 가능하다.
from datetime import timedelta
class DateRangeIterable:
"""An iterable that contains its own iterator object."""
def __init__(self, start_date, end_date):
self.start_date = start_date
self.end_date = end_date
self._present_day = start_date
def __iter__(self):
return self
def __next__(self):
if self._present_day >= self.end_date:
raise StopIteration()
today = self._present_day
self._present_day += timedelta(days=1)
return today
class DateRangeContainerIterable:
"""An range that builds its iteration through a generator."""
def __init__(self, start_date, end_date):
self.start_date = start_date
self.end_date = end_date
def __iter__(self):
current_day = self.start_date
while current_day < self.end_date:
yield current_day
current_day += timedelta(days=1)
class DateRangeSequence:
"""An range created by wrapping a sequence."""
def __init__(self, start_date, end_date):
self.start_date = start_date
self.end_date = end_date
self._range = self._create_range()
def _create_range(self):
days = []
current_day = self.start_date
while current_day < self.end_date:
days.append(current_day)
current_day += timedelta(days=1)
return days
def __getitem__(self, day_no):
return self._range[day_no]
def __len__(self):
return len(self._range)
컨테이너 객체
- 컨테이너는 __contain__메서드를 구현한 객체이다
- 일반적으로 Boolean 을 반환하며 in 키워드가 발견될 때 호출된다.
- element in container ==> container.contains(element)
def mark_coordinate(grid, coord):
if 0 <= coord.x < grid.width and 0 <= coord.y < grid.height:
grid[coord] = 1
if coord in grid:
grid[coord] = 1
class Boundaries:
def __init__(self, width, height):
self.width = width
self.height = heigh
def __contains__(self, coord):
x, y = coord
return 0 <= x < self.width and 0 <= y < self.height
class Grid:
def __init__(self, width, height):
self.width = width
self.height = height
self.limits = Boundaries(width, height)
def __contains__(self, coord):
return coord in self.limits
객체의 동적인 속성
- 파이썬은 객체를 호출하면 객체 사전에서 __getattribute__를 호출한다.
- 객체에 찾고 있는 속성이 없는 경우 객체 이름을 파라미터로 getattr 이라는 추가 메서드가 호출된다.
class DynamicAttributes:
"""
>>> dyn = DynamicAttributes("value")
>>> dyn.attribute
'value'
>>> dyn.fallback_test
'[fallback resolved] test'
>>> dyn.__dict__["fallback_new"] = "new value"
>>> dyn.fallback_new
'new value'
>>> getattr(dyn, "something", "default")
'default'
"""
def __init__(self, attribute):
self.attribute = attribute
def __getattr__(self, attr):
if attr.startswith("fallback_"):
name = attr.replace("fallback_", "")
return f"[fallback resolved] {name}"
raise AttributeError(
f"{self.__class__.__name__} has no attribute {attr}"
)
호출형(callable) 객체
- __call__을 사용하면 객체를 일반 함수처럼 호출할 수 있다.
- 여기에 전달된 모든 파라미터는 call 메서드에 그대로 전달된다.
from collections import defaultdict
class CallCount:
"""
>>> cc = CallCount()
>>> cc(1)
1
>>> cc(2)
1
>>> cc(1)
2
>>> cc(1)
3
>>> cc("something")
1
>>> callable(cc)
True
"""
def __init__(self):
self._counts = defaultdict(int)
def __call__(self, argument):
self._counts[argument] += 1
return self._counts[argument]
5. 파이썬에서 유의할 점
변경 가능한 파라미터의 기본 값
- 변경 가능한 객체를 함수의 기본 인자로 사용하면 안 된다.
from collections import UserList
def wrong_user_display(user_metadata: dict = {"name": "John", "age": 30}):
name = user_metadata.pop("name")
age = user_metadata.pop("age")
return f"{name} ({age})"
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})"
내장(build-in) 타입 확장
- 리스트, 문자열, 사전과 같은 내장 타입을 확장하는 올바른 방법은 collections 모듈을 사용하는 것
- 내장 함수에서 override가 동작하지 않는다.
class BadList(list):
def __getitem__(self, index):
value = super().__getitem__(index)
if index % 2 == 0:
prefix = "even"
else:
prefix = "odd"
return f"[{prefix}] {value}"
class GoodList(UserList):
def __getitem__(self, index):
value = super().__getitem__(index)
if index % 2 == 0:
prefix = "even"
else:
prefix = "odd"
return f"[{prefix}] {value}"
Reference
https://github.com/PacktPublishing/Clean-Code-in-Python-Second-Edition/tree/main/book/src/ch02
https://github.com/dream2globe/CleanCodeInPython/blob/master/Ch2_PythonicCode.ipynb
'TIL > 클린코드' 카테고리의 다른 글
CH 6. 디스크립터로 더 멋진 객체 만들기 (0) | 2023.06.26 |
---|---|
CH 5. 데코레이터를 사용한 코드 개선 (0) | 2023.05.16 |
CH 4. SOLID 원칙 (0) | 2023.05.15 |
CH 3. 좋은 코드의 일반적인 특징 (0) | 2023.05.07 |
CH1. 코드 포매팅과 도구 (0) | 2023.04.26 |