이 글은 블라디미르 켈 레시프 (Vladimir Keleshev)의 "Python Best Practice Patterns"라는 제목으로, 2013 년 5 월 2 일 덴마크 Python Meetup에서 발표되었습니다. 원본 동영상은 here (약 38 분 길이)입니다.
참고 : 일부 코드 예제는 독자 의견을 기반으로 원래의 프레젠테이션에서 수정되었습니다
참고 : Keleshev는 비디오에서 지적했듯이 코드 예제는 Python3 용으로 작성되어있습니다. object으로 부터 명시적으로 하위 클래스를 만들지 않았습니다. (역주: object 를 상속받는것은 python 2.x 버전의 new 스타일입니다. python 3.x 에서는 old -style 과 new - style 의 구분이 없어지고 모두 new-style 로 행동하므로 object 를 명시적으로 써 넣지 않아도 되게 되었지만 더 널리 퍼지기 전 까지는 오해를 방지하고자 작성해주는것을 권장하기도 합니다)
컴포즈드 메소드(Composed method)
하나의 일을 수행하는 메소드로 프로그램을 나눈다.
- 메소드안의 모든 연산을 동일한 추상레벨로 유지하라.
적은 라인 수를 가진 많은 메소드로 만들라.
추상레벨을 다르게 만들라: 비트맵, 파일 시스템 연산, 사운드 플레이 등
| # Better |
| class Boiler: |
| def safety_check(self): |
| if any([self.temperature > MAX_TEMPERATURE, |
| self.pressure > MAX_PRESSURE]): |
| if not self.shutdown(): |
| self.alarm() |
|
|
| def alarm(self): |
| with open(BUZZER_MP3_FILE) as f: |
| play_sound(f.read()) |
|
|
| @property |
| def pressure(self): |
| pressure_psi = abb_f100.register / F100_FACTOR |
| return psi_to_pascal(pressure_psi) |
| ... |
safety_check
은 단지 온도와 압력만 다룬다.alarm
은 파일과 사운드만 다룬다.pressure
은 비트(bits) 와 그것들을 변환하는것을 다룬다
생성메소드(Constructor method)
| # At initiation `point` is not well-formed |
| point = Point() |
| point.x = 12 |
| point.y = 5 |
|
|
| # Better |
| point = Point(x=12, y=5) |
| class Point: |
| def __init__(self, x, y): |
| self.x, self.y = x, y |
|
|
| @classmethod |
| def polar(cls, r, theta): |
| return cls(r * cos(theta), |
| r * sin(theta)) |
|
|
| point = Point.polar(r=13, theta=22.6) |
메소드 오브젝트(Method objects)
- 많은 인자들과 임시변수들을 공유하는 굉장히 긴 라인을 가진 메소드에서의 코딩방법은?
| def send_task(self, task, job, obligation): |
| ... |
| processed = ... |
| ... |
| copied = ... |
| ... |
| executed = ... |
| 100 more lines |
- 많은 수의 작은 메소드들을 만들어서 해결 될 수는 없다. (더 많은 코드를 사용해야할 듯) (역주: ??)
| class TaskSender: |
| def __init__(self, task, job ,obligation): |
| self.task = task |
| self.job = job |
| self.obligation = obligation |
| self.processed = [] |
| self.copied = [] |
| self.executed = [] |
|
|
| def __call__(self): |
| self.prepare() |
| self.process() |
| self.execute() |
| ... |
* 오리지널
Extract Method 를 사용할 수 없을만큼 꼬인 코드가 있다고 하자.
class Order {
//...
public double price() {
double primaryBasePrice;
double secondaryBasePrice;
double tertiaryBasePrice;
// long computation.
//...
}
}
* method object 로 변경
로컬변수는 멤버변수가 되었으며, 메소드 위주의 객체를 만들어서 다양한 메소드로 분리 할 수 있게 되었다.
class Order {
//...
public double price() {
return new PriceCalculator(this).compute();
}
}
class PriceCalculator {
private double primaryBasePrice;
private double secondaryBasePrice;
private double tertiaryBasePrice;
public PriceCalculator(Order order) {
// copy relevant information from order object.
//...
}
public double compute() {
// long computation.
//...
}
}
메소드 주변에서 실행하기(python 에서는 컨텍스트 매니저)
- 함께 해야 될 액션 쌍을 어떻게 표현 할 것인가?
| f = open('file.txt', 'w') |
| f.write('hi') |
| f.close() |
|
|
| # Better |
| with open('file.txt', 'w') as f: |
| f.write('hi') |
|
|
| with pytest.raises(ValueError): |
| int('hi') |
|
|
| with SomeProtocol(host, port) as protocol: |
| protocol.send(['get', signal]) |
| result = protocol.receive() |
|
|
| class SomeProtocol: |
| def __init__(self, host, port): |
| self.host, self.port = host, port |
|
|
| def __enter__(self): |
| self._client = socket() |
| self._client.connect((self.host, |
| self.port)) |
|
|
| def __exit__(self, exception, value, traceback): |
| self._client.close() |
|
|
| def send(self, payload): ... |
|
|
| def receive(self): ... |
Debug printing method
__str__
는 사용자를 위해 사용하자.__repr__
는 디버깅을 위해 사용하자.
Method comment
- 작은 메소드는 주석보다 효과적이 될 수 있다.
| if self.flags & 0b1000: # Am I visible? |
| ... |
|
|
| # 더 좋습니다. |
| ... |
| @property |
| def is_visible(self): |
| return self.flags & 0b1000 |
|
|
| if self.is_visible: |
| ... |
Choosing message
| # 좋지 않아요. |
| if type(entry) is Film: |
| responsible = entry.producer |
| else: |
| responsible = entry.author |
|
|
| # Shouldn't use type() or isinstance() in conditional --> smelly |
|
|
| # 좋습니다. |
| class Film: |
| ... |
| @property |
| def responsible(self): |
| return self.producer |
|
|
| entry.responsible |
Intention revealing message
- 구현이 너무 간단하여 의도를 알리기 쉽지 않을 때 어떻게 할까?
- 동일한 일을 하는 메소드를 이용하라 ( 더 읽기 쉽게 )
| class ParagraphEditor: |
| ... |
| def highlight(self, rectangle): |
| self.reverse(rectangle) |
|
|
| # 더 낫죠 |
| class ParagraphEditor: |
| ... |
| highlight = reverse # More readable, more composable |
Constant method (constant class variable)
| _DEFAULT_PORT = 1234 |
|
|
| class SomeProtocol: |
| ... |
| def __enter__(self): |
| self._client = socket() |
| self._client.connect( |
| (self.host, |
| self.port or _DEFAULT_PORT) |
| ) |
| return self |
|
|
| # If you want to subclass SomeProtocol, you would have to overwrite every method! |
|
|
| # 더 낫죠 |
| class SomeProtocol: |
| _default_port = 1234 |
| ... |
| def __enter__(self): |
| self._client = socket() |
| self._client.connect( |
| (self.host, |
| self.port or self._default_port)) |
Direct and indirect variable access
- Direct
- getters 와setters 가 필요 없다.
| class Point: |
| def __init__(self, x, y): |
| self.x, self.y = x, y |
|
|
| # Sometimes need more flexibility --> use properties |
|
|
| class Point: |
| def __init__(self, x, y): |
| self._x, self._y = x, y |
|
|
| @property |
| def x(self): |
| return self._x |
|
|
| @x.setter |
| def x(self, value): |
| self._x = value |
Enumeration (iteration) method
| # 나뻐요. 컬렉션의 타입을 바꿀 수 없습니다. |
|
|
| # e.g. can't change employees from a list to a set |
| class Department: |
| def __init__(self, *employees): |
| self.employees = employees |
|
|
| for employee in department.employees: |
| ... |
| |
| # 더 낫다 |
| class Department: |
| def __init__(self, *employees): |
| self._employees = employees |
|
|
| def __iter__(self): |
| return iter(self._employees) |
|
|
| for employee in department: # More readable, more composable |
| ... |
| |
Temporary variable
| # Meh |
| class Rectangle: |
| def bottom_right(self): |
| return Point(self.left + self.width, |
| self.top + self.height) |
|
|
| # 가독성을 위해 임시변수를 사용하는것이 더 낫다. |
| class Rectangle: |
| ... |
| def bottom_right(self): |
| right = self.left + self.width |
| bottom = self.top + self.height |
| return Point(right, bottom) |
| |
Sets
- for 루프를 대신하는 sets 을 종종 사용 할 수 있다.
| item in a_set |
| item not in a_set |
|
|
| # a_set <= other |
| a_set.is_subset(other) |
|
|
| # a_set | other |
| a_set.union(other) |
|
|
| # a_set & other |
| a_set.intersection(other) |
|
|
| # a_set - other |
| a_set.difference(other) |
Equality method
| obj == obj2 |
| obj1 is obj2 |
|
|
| class Book: |
| ... |
| def __eq__(self, other): |
| if not isinstance(other, self.__class__): |
| return NotImplemented |
| return (self.author == other.author and |
| self.title == other.title) |
Hashing method
| class Book: |
| ... |
| def __hash__(self): |
| return hash(self.author) ^ hash(self.other) |
Sorted collection
| class Book: |
| ... |
| def __lt__(self, other): |
| return (self.author, self.title) < (other.author, other.title) |
Concatenation
| class Config: |
| def __init__(self, **entries): |
| self.entries = entries |
|
|
| def __add__(self, other): |
| entries = (self.entries.items() + |
| other.entries.items()) |
| return Config(**entries) |
| default_config = Config(color=False, port=8080) |
| config = default_config + Config(color=True) |
Simple enumeration parameter
- 적절한 반복문에 사용되는 변수를 만들기 힘들 때 그냥 each 를 사용하라.
| # 어색함 |
| for options_shortcut in self.options_shortcuts: |
| options_shortcut.this() |
| options_shortcut.that() |
|
|
| # 좋습니다. |
| for each in self.options_shortcuts: |
| each.this() |
| each.that() |
Cascades
리턴 값이 없는 메소드를 작성하는 대신에, self 를 리턴해보라.
| # Instead of this |
| self.release_water() |
| self.shutdown() |
| self.alarm() |
|
|
| class Reactor: |
| ... |
| def release_water(self): |
| self.valve.open() |
| return self |
|
|
| self.release_water().shutdown().alarm() |
Interesting return value
| # Meh |
| def match(self, items): |
| for each in items: |
| if each.match(self): |
| return each |
| # Is this supposed to reach the end? Is this a bug? |
|
|
| # Better |
| def match(self, items): |
| for each in items: |
| if each.match(self): |
| return each |
| return None |
- 암시적인 것보단 명시적인게 낫다.
None
이라도 리턴하라.
더 읽어볼꺼리