파이썬 클린코드를 읽으며 정리한 내용입니다.
my_numbers = (4, 5, 3, 9)
my_numbers[-1]
>> 9
my_numbers[-3]
>> 5
my_numbers = (1, 1, 2, 3, 5, 8, 13, 21)
my_numbers[2:5]
>>> (2, 3, 5)
my_numbers[:100]
>>> (1, 1, 2, 3, 5, 8, 13, 21)
my_numbers[-100:]
>>> (1, 1, 2, 3, 5, 8, 13, 21)
my_numbers[:3]
>>> (1, 1, 2)
my_numbers[3:]
>>> (3, 5, 8, 13, 21)
my_numbers[:]
>>> (1, 1, 2, 3, 5, 8, 13, 21)
# 1번째부터 7번째 원소 전까지 2칸 간격으로 조회
my_numbers[1:7:2]
>>> (1, 3, 8)
interval = slice(1, 7, 2)
my_numbers[interval]
>>> (1, 3, 8)
# 시작, 중지, 간격 중 하나를 지정하지 않은 경우 None으로 간주
interval = slice(None, 3)
my_numbers[interval] == my_numbers[:3]
>>> True
__getitem__
이라는 매직 메서드 덕분에 동작한다.__getitem__
은 myobject[key]와 같은 형태를 사용할 때 호출되는 메서드로, key에 해당하는 대괄호 안의 값을 파라미터로 전달한다.__getitem__
과 __len__
을 모두 구현하는 객체이므로 반복이 가능하다.__getitem__
을 사용자정의 클래스에 구현하려는 경우, 몇 가지를 고려해야 한다.클래스가 표준 라이브러리 객체를 감싸는 래퍼인 경우
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.UserList
부모 클래스를 상속한다.래퍼도 아니고 내장 객체를 사용하지도 않은 경우
class Connector:
def __init__(self, source):
self.source = source
self._timeout = 60
conn = Connector("postgresql://localhost")
conn.source
>>> 'postgresql://localhost'
conn._timeout
>>> 60
class Connector:
def __init__(self, source):
self.source = source
self.__timeout = 60
def connect(self):
print(f"connecting with {self.__timeout}s")
conn = Connector('postgresql://localhost')
conn.connect()
>>> connecting with 60s
conn.__timeout
>>>
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-6-d082cdc68a78> in <module>
----> 1 conn.__timeout
AttributeError: 'Connector' object has no attribute '__timeout'
_<class-name>__<attribute-name>
형태의 다른 이름을 만든다. 이것을 이름 맹글링(name mangling)이라 한다.# 프로퍼티를 이용한 이메일 validation 예시
import re
EMAIL_FORMAT = re.compile(r"[^@]+@[^@]+[^@]+")
def is_valid_email(potentially_valid_email: str):
return re.match(EMAIL_FORMAT, potentially_valid_email) is not None
class User:
def __init__(self, username):
self.username = username
self._email = None
@property
def email(self):
return self._email
@email.setter
def email(self, new_email):
if not is_valid_email(new_email):
raise ValueError(f"유효한 이메일이 아니므로 {new_email}값을 사용할 수 없음")
self._email = new_email
__next__
나 __iter__
이터레이터 메서드 중 하나를 포함하는지__len__
과 __getitem__
를 모두 가졌는지__iter__
함수가 있는지 확인한다. 있으면 __iter__
실행__iter__
에서 반환된 이터레이터의 next()함수 호출__next__
함수 호출# 일정 기간의 날짜를 하루 간격으로 반복하는 객체 예시
from datetime import timedelta
class DateRangeIterable:
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
from datetime import date
for day in DateRangeIterable(date(2020, 1, 1), date(2020, 1, 5)):
print(day)
>>>
2020-01-01
2020-01-02
2020-01-03
2020-01-04
__iter__
가 호출될 때마다 새로운 이터레이터를 만들 수 있지만, 새로운 DateRangeIterable 인스턴스가 필요하다.__iter__
에서 제너레이터를 사용할 수도 있다.class DateRangeContainerIterable:
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)
r1 = DateRangeContainerIterable(date(2020, 1, 1), date(2020, 1, 5))
", ".join(map(str, r1))
>>> '2020-01-01, 2020-01-02, 2020-01-03, 2020-01-04'
max(r1)
>>> datetime.date(2020, 1, 4)
__iter__
를 호출하고, __iter__
는 제너레이터를 생성한다.__iter__
가 정의되어 있지 않으면 __getitem__
을 찾고 없으면 TypeError를 발생시킨다.class DateRangeSequence:
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)
s1 = DateRangeSequence(date(2020, 1, 1), date(2020, 1, 5))
for day in s1:
print(day)
>>>
2020-01-01
2020-01-02
2020-01-03
2020-01-04
s1[0]
>>> datetime.date(2020, 1, 1)
__container__
메서드를 구현한 객체__container__
메서드는 일반적으로 Boolean 값을 반환element in container
는 container.__contains__(element)
와 같이 해석된다.# 2차원 게임 지도에서 특정 위치에 표시를 하는 예시
def mark_coordinate(grid, coord):
if 0 <= coord.x < grid.width and 0 <= coord.y < grid.height:
grid[coord] = 'MARKED'
class Boundaries:
def __init__(self, width, height):
self.width = width
self.height = height
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
__getattr__
매직 메서드를 사용해 객체에서 속성을 얻는 방법을 제어할 수 있다.__getattribute__
호출__getattr__
이라는 추가 메서드 호출__getattr__
이라는 메서드를 이용해 반환 값을 제어하고, 심지어 새로운 속성을 만들수도 있다.class DynamicAttributes:
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__}에는 {attr} 속성이 없음")
dyn = DynamicAttributes("value")
dyn.attribute
>>> 'value'
# __getattr__이 호출되어 값을 반환한다.
dyn.fallback_test
>>> '[fallback resolved] test'
# __dict__를 직접 조작하면 __getattr__이 호출되지 않는다.
dyn.__dict__["fallback_new"] = "new_value"
dyn.fallback_new
>>> 'new_value'
# 값을 검색할 수 없는 경우 AttributeError가 발생하며, 기본값을 반환한다.
getattr(dyn, "something", "default")
>>> 'default'
__call__
을 사용하면 객체를 일반 함수처럼 호출할 수 있다.object(*args, **kwargs)
와 같은 구문을 object.__call__(*args, **kwargs)
로 변환한다.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
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})"
wrong_user_display()
>>> 'John (30)'
# 한번더 호출하면 작동하지 않는다.
wrong_user_display()
---------------------------------------------------------------------------
>>>
KeyError Traceback (most recent call last)
<ipython-input-38-468f5f73dd28> in <module>
----> 1 wrong_user_display()
<ipython-input-36-32436123dbd4> in wrong_user_display(user_metadata)
1 def wrong_user_display(user_metadata: dict = {"name": "John", "age": 30}):
----> 2 name = user_metadata.pop("name")
3 age = user_metadata.pop("age")
4 return f"{name} ({age})"
KeyError: 'name'
class BadList(list):
def __getitem__(self, index):
value = super().__getitem__(index)
if index % 2 == 0:
prefix = "짝수"
else:
prefix = "홀수"
return f"[{prefix}] {value}"
bl = BadList((0, 1, 2, 3, 4, 5))
bl[0]
>>> '[짝수] 0'
"".join(bl)
---------------------------------------------------------------------------
>>>
TypeError Traceback (most recent call last)
<ipython-input-42-4256097aa2ac> in <module>
----> 1 "".join(bl)
TypeError: sequence item 0: expected str instance, int found
__getitem__
을 호출하지 않는다.from collections import UserList
class GoodList(UserList):
def __getitem__(self, index):
value = super().__getitem__(index)
if index % 2 == 0:
prefix = "짝수"
else:
prefix = "홀수"
return f"[{prefix}] {value}"
gl = GoodList((0, 1, 2, 3, 4, 5))
gl[0]
>>> '[짝수] 0'
"".join(gl)
>>> '[짝수] 0[홀수] 1[짝수] 2[홀수] 3[짝수] 4[홀수] 5'