본문 바로가기

Python/자료구조 (Data Structure)

5-2. 디자인 패턴 (Design Pattern): 파이썬 자료구조와 알고리즘

 

디자인 패턴 (design pattern) 은 잘 설계된 구조의 형식적 정의이다. 다양한 디자인 패턴이 있으며, 이들을 사용해 서로 다른 문제를 해결할 수 있다.

데커레이터 패턴

데커레이터 (decorator) 패턴은 @ 표기를 사용해 함수 또는 메서드의 변환을 우아하게 지정해주는 도구다. 데커레이터 패턴은 함수의 객체와 함수를 변경하는 다른 객체의 래핑 (wrapping) 을 혀용한다. 즉 클래스 내의 메서드를 재정의 하지 않고도 자동으로 다른 함수와 래핑 시켜준다. 아래는 구글 파이썬 스타일 가이드의 예제이다.

 

class C(object):
    @my_decorator
    def method(self):
        #메서드...

 

이는 아래와 같은 코드이다.

 

class C(object):
    def method(self):
        #메서드...
    method = my_decorator(method)

 

아래 코드는 데커레이터를 사용하여 리스트에 임의의 값을 넣는 함수를 벤치마킹한다.

 

import random
import time

def benchmark(func):
    def wrapper(*args, **kwargs):
        t = time.perf_counter()
        res = func(*args, **kwargs)
        print("{0} {1}".format(func.__name__, time.perf_counter()-t))
        return res
    return wrapper

@benchmark
## random_tree = benchmark(random_tree)
def random_tree(n):
    temp = [n for n in range (n)]
    for i in range(n+1):
        temp[random.choice(temp)] = random.choice(temp)
    return temp

random_tree(10000)

 

출력:

 

random_tree 0.04088019999999998

 

파이썬에서 일반적으로 사용하는 데커레이터는 @classmethod와 @staticmethod가 있다. 이들은 각각 메서드를 클래스 메서드와 정적 메서드로 변환한다. @classmethod는 첫 번째 인수로 클래스 (cls) 를 사용하고, @staticmethod는 첫 번째 인수에 self 혹은 cls가 없다. 클래스 내 변수에 접근하려면 @classmethod의 첫 번째 인수를 사용할 수 있다.

 

class A(object):
    _hello = True

    def foo(self, x):
        print("foo({0}, {1}) 실행".format(self, x))

    @classmethod
    def class_foo(cls,x):
        print("class_foo({0}, {1}) 실행: {2}".format(cls, x, cls._hello))

    @staticmethod
    def static_foo(x):
        print("static_foo({0}) 실행".format(x))

a = A()
a.foo(1)
a.class_foo(2)
A.class_foo(2)
a.static_foo(3)
A.static_foo(3)

 

출력:

 

foo(<__main__.A object at 0x03CDA400>, 1) 실행
class_foo(<class '__main__.A'>, 2) 실행: True
class_foo(<class '__main__.A'>, 2) 실행: True
static_foo(3) 실행
static_foo(3) 실행

옵저버 패턴

옵저버 패턴 (observer pattern) 은 특정 값을 유지하는 핵심 객체를 갖고, 직렬화된 객체의 복사본을 생성하는 일부 옵저버 (관찰자) 가 있는 경우 유용하다. 즉, 객체의 일대다 (one-to-many) 의존 관계에서 한 객체의 상태가 변경되면, 그 객체에 종속된 모든 객체에 그 내용을 통지하여 자동으로 상태를 갱신하는 방식이다 (마치 provider 또는 subscriber 같은). 옵저버 패턴은 @property 데커레이터를 사용해 구현할 수 있다. 예를 들어 속성 (property) 을 읽기 전용으로 설정하는 것과 같은 속성 접근을 제어할 수 있다. 속성은 접근자 (accessor) 나 getter/setter 메서드 대신 사용된다. 간단한 속성 예제는 아래와 같다.

 

class C:
    def __init__(self, name):
        self._name = name
    @property
    def name(self):
        return self._name
    @name.setter
    def name(self, new_name):
        self._name = "{0} >> {1}".format(self._name, new_name)

c = C("beom")
print(c._name)
print(c.name)
c.name = "seok"
print(c.name)
print(c._name)

 

출력:

 

beom
beom
beom >> seok
beom >> seok

 

파이썬의 옵저버 패턴은 다른 컴퓨터 언어와 조금 다른 방식으로 구현된다. 아래는 셋을 이용한 옵저버 패턴의 구현이다.

 

class Subscriber(object):
    def __init__(self, name):
        self.name = name
    def update(self, message):
        print("{0}, {1}".format(self.name, message))

class Publisher(object):
    def __init__(self):
        self.subscribers = set()
    def register(self, who):
        self.subscribers.add(who)
    def unregister(self, who):
        self.subscribers.discard(who)
    def dispatch(self, message):
        for subscriber in self.subscribers:
            subscriber.update(message)

pub = Publisher()

beom = Subscriber('Beom')
ben = Subscriber('Ben')
seok = Subscriber('Seok')

pub.register(beom)
pub.register(ben)
pub.register(seok)

pub.dispatch("점심시간입니다.")
pub.unregister(ben)
pub.dispatch("저녁시간입니다.")

 

출력:

 

Ben, 점심시간입니다.
Seok, 점심시간입니다.
Beom, 점심시간입니다.
Seok, 저녁시간입니다.
Beom, 저녁시간입니다.

 

이번엔 딕셔너리를 이용해 옵저버 패턴을 구현해보자.

 

class SubscriberOne(object):
    def __init__(self, name):
        self.name = name
    def update(self, message):
        print("{0} {1}".format(self.name, message))

class SubscriberTwo(object):
    def __init__(self, name):
        self.name = name
    def receive(self, message):
        print("{0} {1}".format(self.name, message))

class Publisher(object):
    def __init__(self):
        self.subscribers = dict()
    def register(self, who, callback=None):
        if callback is None:
            callback = getattr(who, 'update')
        self.subscribers[who] = callback
    def unregister(self, who):
        del self.subscribers[who]
    def dispatch(self, message):
        for subscriber, callback in self.subscribers.items():
            callback(message)

pub = Publisher()
beom = SubscriberOne('Beom')
seok = SubscriberTwo('Seok')
ben = SubscriberOne('Ben')

pub.register(beom, beom.update)
pub.register(seok, seok.receive)
pub.register(ben)

pub.dispatch('점심시간입니다')
pub.unregister(ben)
pub.dispatch('퇴근시간입니다')

 

출력:

 

Beom 점심시간입니다
Seok 점심시간입니다
Ben 점심시간입니다
Beom 퇴근시간입니다
Seok 퇴근시간입니다

 

위에선 기본값으로 callback 을 update로 지정해주었으니 SubscriberTwo를 register해 줄 땐 SubcriberTwo의 프린트 방식인 receive로 register를 해주었다. 구독자의 형태가 다양해져 이전보다 더 유연하게 코드가 구현되었다.

아래는 이벤트에 따른 (마치 리덕스의 액션 디스패칭과 같은) 옵저버 패턴이다.

 

class Subscriber(object):
    def __init__(self, name):
        self.name = name
    def update(self, message):
        print("{0}, {1}".format(self.name, message))

class Publisher(object):
    def __init__(self, events):
        self.subscribers = {event: dict() for event in events}
    def get_subscribers(self, event):
        return self.subscribers[event]
    def register(self, event, who, callback=None):
        if callback is None:
            callback = getattr(who, 'update')
        self.get_subscribers(event)[who] = callback
    def unregister(self, event, who):
        del self.get_subscribers(event)[who]
    def dispatch(self, event, message):
        for subscriber, callback in self.get_subscribers(event).items():
            callback(message)

pub = Publisher(['점심', '퇴근'])

beom = Subscriber('Beom')
seok = Subscriber('Seok')
ben = Subscriber('Ben')

pub.register('점심', beom, beom.update)
pub.register('퇴근', beom)
pub.register('점심', seok)
pub.register('퇴근', ben)

pub.dispatch('점심', '점심시간입니다.')
pub.dispatch('퇴근', '저녁시간입니다.')

 

출력:

 

Beom, 점심시간입니다.
Seok, 점심시간입니다.
Beom, 저녁시간입니다.
Ben, 저녁시간입니다.

 

싱글턴 패턴

초기화된 객체의 인스턴스를 전역에서 사용하기 위해 싱글턴 (singleton) 패턴을 사용할 수 있다. 이 객체의 인스턴스는 하나만 존재한다. 파이썬에는 private 접근 제한자가 없어 __new__() 클래스 메서드를 갖고 있는 인스턴스만 생성되도록 구현해야 한다. 이는 싱글턴 인스턴스가 생성됐는지 확인한다 (이미 싱글턴 인스턴스가 생성되었는데, 또 다시 생성을 시도했는지 확인한다). 싱글턴 인스턴스가 없다면 슈퍼 클래스를 호출하여 싱글턴 인스턴스를 생성한다.

 

마치며

디자인 패턴에 대해서는 다양한 의견이 있다.

파이콘 스웨덴에서 발표한 파이썬 디자인 패턴에 대한 Design Patterns in Python by Peter Ullrich 도 참고할 만하다.

 

출처

파이썬 자료구조와 알고리즘 - 한빛미디어, 미아 스타인