파이썬 클린코드를 읽으며 정리한 내용입니다.
팩토리
싱글턴과 공유상태
# git 저장소에서 최신 태그의 코드를 가져오는 객체 예시
class GitFetcher:
_current_tag = None
def __init__(self, tag):
self.current_tag = tag
@property
def current_tag(self):
if self._current_tag is None:
raise AttributeError("tag가 초기화되지 않음")
return self._current_tag
@current_tag.setter
def current_tag(self, new_tag):
self.__class__._current_tag = new_tag
def pull(self):
print("%s에서 풀" % self.current_tag)
return self.current_tag
f1 = GitFetcher(0.1)
f2 = GitFetcher(0.2)
f1.current_tag = 0.3
f2.pull()
>>>
0.3에서 풀
0.3
class SharedAttribute:
def __init__(self, initial_value=None):
self.value = initial_value
self._name = None
def __get__(self, instance, owner):
if instance is None:
return self
if self.value is None:
raise AttributeError(f"{self._name} was never set")
return self.value
def __set__(self, instance, new_value):
self.value = new_value
def __set_name__(self, owner, name):
self._name = name
class GitFetcher:
current_tag = SharedAttribute()
current_branch = SharedAttribute()
def __init__(self, tag, branch=None):
self.current_tag = tag
self.current_branch = branch
def pull(self):
print("%s에서 풀" % self.current_tag)
return self.current_tag
class BaseFetcher:
def __init__(self, source):
self.source = source
class TagFetcher(BaseFetcher):
_attributes = {}
def __init__(self, source):
self.__dict__ = self.__class__._attributes
super().__init__(source)
def pull(self):
print("%s 태그에서 풀" % self.source)
return f"Tag = {self.source}"
class BranchFetcher(BaseFetcher):
_attributes = {}
def __init__(self, source):
self.__dict__ = self.__class__._attributes
super().__init__(source)
def pull(self):
print("%s 브랜치에서 풀" % self.source)
return f"Branch = {self.source}"
class SharedAllMixin:
def __init__(self, *args, **kwargs):
try:
self.__class__._attributes
except AttributeError:
self.__class__._attributes = {}
self.__dict__ = self.__class__._attributes
super().__init__(*args, **kwargs)
class BaseFetcher:
def __init__(self, source):
self.source = source
class TagFetcher(SharedAllMixin, BaseFetcher):
def pull(self):
print("%s 태그에서 풀" % self.source)
return f"Tag = {self.source}"
class BranchFetcher(SharedAllMixin, BaseFetcher):
def pull(self):
print("%s 브랜치에서 풀" % self.source)
return f"Branch = {self.source}"
어댑터 패턴
컴포지트(composite)
from typing import Iterable, Union
# 상품 클래스와, 상품 여러개로 구성된 번들 클래스의 예시
class Product:
def __init__(self, name, price):
self._name = name
self._price = price
@property
def price(self):
return self._price
class ProductBundle:
def __init__(self, name, perc_discount, *products: Iterable[Union[Product, "ProductBundle"]]) -> None:
self._name = name
self._perc_discount = perc_discount
self._products = products
@property
def price(self):
total = sum(p.price for p in self._products)
return total * (1 - self._perc_discount)
데코레이터 패턴
# 전달된 파라미터를 사용해 쿼리에 사용할 수 있는 사전 형태의 객체 반환
class DictQuery:
def __init__(self, **kwargs):
self._raw_query = kwargs
def render(self) -> dict:
return self._raw_query
class QueryEnhancer:
def __init__(self, query: DictQuery):
self.decorated = query
def render(self):
return self.decorated.render()
class RemoveEmpty(QueryEnhancer):
def render(self):
original = super().render()
return {k: v for k, v in original.items() if v}
class CaseInsensitive(QueryEnhancer):
def render(self):
original = super().render()
return {k: v.lower() for k, v in original.items()}
original = DictQuery(key="value", empty="", none=None, upper="UPPER", title="Title")
new_query = CaseInsensitive(RemoveEmpty(original))
original.render()
>>> {'key': 'value', 'empty': '', 'none': None, 'upper': 'UPPER', 'title': 'Title'}
new_query.render()
>>> {'key': 'value', 'upper': 'upper', 'title': 'title'}
from typing import Callable, Dict
class QueryEnhancer:
def __init__(self, query: DictQuery, *decorators: Iterable[Callable[[Dict[str, str]], Dict[str, str]]]) -> None:
self._decorated = query
self._decorators = decorators
def render(self):
current_result = self._decorated.render()
for deco in self._decorators:
current_result = deco(current_result)
return current_result
def remove_empty(original):
return {k: v for k, v in original.items() if v}
def case_insensitive(original):
return {k: v.lower() for k, v in original.items()}
query = DictQuery(foo="bar", empty="", none=None, upper="UPPER", title="Title")
QueryEnhancer(query, remove_empty, case_insensitive).render()
>>> {'foo': 'bar', 'upper': 'upper', 'title': 'title'}
파사드
__init__.py
를 이용해 사용자에게 노출해야 하는 임포트 가능한 외부용 레이아웃과 내부용 레이아웃을 구분하는 경우가 있다. 이렇게 하면 사용자에게 단일 진입점을 제공하며, 패키지의 나머지 파일들을 마음껏 리팩토링하거나 재정렬할 수 있다.책임 연쇄 패턴
import re
class Event:
pattern = None
def __init__(self, next_event=None):
self.successor = next_event
def process(self, logline: str):
if self.can_process(logline):
return self._process(logline)
if self.successor is not None:
return self.successor.process(logline)
def _process(self, logline: str) -> dict:
parsed_data = self._parse_data(logline)
return {
"type": self.__class__.__name__,
"id": parsed_data["id"],
"value": parsed_data["value"]
}
@classmethod
def can_process(cls, logline: str) -> bool:
return cls.pattern.match(logline) is not None
@classmethod
def _parse_data(cls, logline: str) -> dict:
return cls.pattern.match(logline).groupdict()
class LoginEvent(Event):
pattern = re.compile(r"(?P<id>\\d+):\\s+login\\s+(?P<value>\\S+)")
class LogoutEvent(Event):
pattern = re.compile(r"(?P<id>\\d+):\\s+logout\\s+(?P<value>\\S+)")
chain = LogoutEvent(LoginEvent())
chain.process("567: login user")
>>> {'type': 'LoginEvent', 'id': '567', 'value': 'user'}
# 원래 코드
class Event:
def __init__(self, raw_data):
self.raw_data = raw_data
# 객체를 만들지 않고 클래스의 메소드 사용
# 인스턴스 속성을 변화시키지 않는 경우
@staticmethod
def meets_condition(event_data: dict):
return False
class UnknownEvent(Event):
"""데이터만으로 식별할 수 없는 이벤트"""
class LoginEvent(Event):
"""로그인 사용자에 의한 이벤트"""
@staticmethod
def meets_condition(event_data: dict):
return(
event_data["before"]["session"] == 0
and event_data["after"]["session"] == 1
)
class LogoutEvent(Event):
"""로그아웃 사용자에 의한 이벤트"""
@staticmethod
def meets_condition(event_data: dict):
return(
event_data["before"]["session"] == 1
and event_data["after"]["session"] == 0
)
class SystemMonitor:
"""시스템에서 발생한 이벤트 분류"""
def __init__(self, event_data):
self.event_data = event_data
def identify_event(self):
for event_cls in Event.__subclasses__():
try:
if event_cls.meets_condition(self.event_data):
return event_cls(self.event_data)
except KeyError:
continue
return UnknownEvent(self.event_data)
템플릿 메서드 패턴
커맨드
__call__()
매직 메서드를 구현하여 호출 가능한 객체를 생성할 수 있다. 따라서 일단 객체를 초기화하고 나중에 호출할 수 있다.__call__()
을 사용할 수도 있고, 사용자 정의 메서드를 사용할 수도 있다.상태 패턴
import abc
class InvalidTransitionError(Exception):
"""도달 불가능한 상태에서 전이할 때 발생하는 예외"""
class MergeRequestState(abc.ABC):
def __init__(self, merge_request):
self._merge_request = merge_request
@abc.abstractmethod
def open(self):
pass
@abc.abstractmethod
def close(self):
pass
@abc.abstractmethod
def merge(self):
pass
def __str__(self):
return self.__class__.__name__
class Open(MergeRequestState):
def open(self):
self._merge_request.approvals = 0
def close(self):
self._merge_request.approvals = 0
self._merge_request.state = Closed
def merge(self):
print("%s 머지" % self._merge_request)
print("%s 브랜치 삭제" % self._merge_request.source_branch)
self._merge_request.state = Merged
class Closed(MergeRequestState):
def open(self):
print("종료된 머지 리퀘스트 %s 재오픈" % self._merge_request)
self._merge_request.state = Open
def close(self):
pass
def merge(self):
raise InvalidTransitionError("종료된 요청을 머지할 수 없음")
class Merged(MergeRequestState):
def open(self):
raise InvalidTransitionError("이미 머지 완료됨")
def close(self):
raise InvalidTransitionError("이미 머지 완료됨")
def merge(self):
pass
class MergeRequest:
def __init__(self, source_branch: str, target_branch: str) -> None:
self.source_branch = source_branch
self.target_branch = target_branch
self._state = None
self.approvals = 0
self.state = Open
@property
def state(self):
return self._state
@state.setter
def state(self, new_state_cls):
self._state = new_state_cls(self)
def open(self):
return self.state.open()
def close(self):
return self.state.close()
def merge(self):
return self.state.merge()
def __str__(self):
return f"{self.target_branch}: {self.source_branch}"
_state
속성을 가지며 해당 속성을 통해 최종 MergeRequest 상태를 알 수 있다.mr = MergeRequest("develop", "master")
mr.open()
mr.approvals
>>> 0
mr.approvals = 3
mr.close()
mr.approvals
>>> 0
mr.open()
>>> 종료된 머지 리퀘스트 master: develop 재오픈
mr.merge()
>>>
master: develop 머지
develop 브랜치 삭제
mr.close()
>>>
InvalidTransitionErrorTraceback (most recent call last)
<ipython-input-17-7333a1258b38> in <module>
----> 1 mr.close()
<ipython-input-6-f01e7f739763> in close(self)
77
78 def close(self):
---> 79 return self.state.close()
80
81 def merge(self):
<ipython-input-6-f01e7f739763> in close(self)
52
53 def close(self):
---> 54 raise InvalidTransitionError("이미 머지 완료됨")
55
56 def merge(self):
InvalidTransitionError: 이미 머지 완료됨
self.state.open()
과 같은 형태로 호출되게 된다. 이런 반복적인 코드는 __getattr__()
매직 메서드를 사용해 제거할 수 있다.class MergeRequest:
def __init__(self, source_branch: str, target_branch: str) -> None:
self.source_branch = source_branch
self.target_branch = target_branch
# 타입 어노테이션을 이용해 어디에서 인터페이스의 정의를 찾을 수 있는지 알려준다.
self._state: MergeRequestState
self.approvals = 0
self.state = Open
@property
def state(self):
return self._state
@state.setter
def state(self, new_state_cls):
self._state = new_state_cls(self)
@property
def status(self):
return str(self.state)
def __getattr__(self, method):
return getattr(self.state, method)
def __str__(self):
return f"{self.target_branch}: {self.source_branch}"