관리 메뉴

HAMA 블로그

파이썬 마이크로 실전 패턴 [번역] 본문

Python

파이썬 마이크로 실전 패턴 [번역]

[하마] 이승현 (wowlsh93@gmail.com) 2017. 6. 20. 11:00


파이썬 마이크로 실전 패턴  [번역]


(역주: Gof 의 디자인패턴은 개발하는 중간 중간 자주 사용되지는 않는 중급 규모의 설계의 구조를 담당한다면. 이 마이크로 패턴은 더 작고 개발하면서 매일 만날 수 있는 아주 익숙한 문제들의 모음이다. 이디엄과 겹치기도 한다.) 

일반적인 디자인패턴은 다음 링크(한글 번역) 를 참고하라 -> Pattern in Python 

May 09, 2013


이 글은 블라디미르 켈 레시프 (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)
...
view rawgistfile1.py hosted with ❤ by GitHub
  • 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)
view rawgistfile1.py hosted with ❤ by GitHub
  • 여러 방식의 생성자를 만들기 위해 클래스 메소드를 사용 할 수 있다.

    • 예) 카테시안 좌표와 극좌표 
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)
view rawgistfile1.py hosted with ❤ by GitHub


메소드 오브젝트(Method objects)

  • 많은 인자들과 임시변수들을 공유하는 굉장히 긴 라인을 가진 메소드에서의 코딩방법은?
def send_task(self, task, job, obligation):
...
processed = ...
...
copied = ...
...
executed = ...
100 more lines
view rawgistfile1.py hosted with ❤ by GitHub
  • 많은 수의 작은 메소드들을 만들어서 해결 될 수는 없다. (더 많은 코드를 사용해야할 듯) (역주: ??) 
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()
...
view rawgistfile1.py hosted with ❤ by GitHub


역자 추가 : 참고

* 오리지널
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): ...
view rawgistfile1.py hosted with ❤ by GitHub


Debug printing method

  • __str__는 사용자를 위해 사용하자.
    • e.g. print(point)
  • __repr__ 는 디버깅을 위해 사용하자.


Method comment

  • 작은 메소드는 주석보다 효과적이 될 수 있다.
if self.flags & 0b1000: # Am I visible?
...
# 더 좋습니다.
...
@property
def is_visible(self):
return self.flags & 0b1000
if self.is_visible:
...
view rawgistfile1.py hosted with ❤ by GitHub


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
view rawgistfile1.py hosted with ❤ by GitHub


Intention revealing message

  • 구현이 너무 간단하여 의도를 알리기 쉽지 않을 때 어떻게 할까?
  • 동일한 일을 하는 메소드를 이용하라 ( 더 읽기 쉽게 ) 
class ParagraphEditor:
...
def highlight(self, rectangle):
self.reverse(rectangle)
# 더 낫죠
class ParagraphEditor:
...
highlight = reverse # More readable, more composable
view rawgistfile1.py hosted with ❤ by GitHub


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))
view rawgistfile1.py hosted with ❤ by GitHub
  • 서브클래스를 만들 때도 상속되니 편리하다.


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
view rawgistfile1.py hosted with ❤ by GitHub


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
...
view rawgistfile1.py hosted with ❤ by GitHub


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)
view rawgistfile1.py hosted with ❤ by GitHub


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)
view rawgistfile1.py hosted with ❤ by GitHub


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)
view rawgistfile1.py hosted with ❤ by GitHub

  •  isinstance() 체크용으로 사용


Hashing method

class Book:
...
def __hash__(self):
return hash(self.author) ^ hash(self.other)
view rawgistfile1.py hosted with ❤ by GitHub


Sorted collection

class Book:
...
def __lt__(self, other):
return (self.author, self.title) < (other.author, other.title)
view rawgistfile1.py hosted with ❤ by GitHub


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)
view rawgistfile1.py hosted with ❤ by GitHub


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()
view rawgistfile1.py hosted with ❤ by GitHub


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()
view rawgistfile1.py hosted with ❤ by GitHub


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
view rawgistfile1.py hosted with ❤ by GitHub
  • 암시적인 것보단 명시적인게 낫다.
  • None이라도 리턴하라.


더 읽어볼꺼리

  • Smalltalk Best Practice Patterns   (김창준: 마이크로 패턴. 개발자의 탈무드. 감동의 연속)

    • 스몰토크에 관한것이 아니다. Python, Ruby 등 다른언어에도 통찰력을 줄것이다.



Comments