기술자 사용법 하우투

저자:  레이몬드 헤팅거(Raymond Hettinger)
연락처:  <python at rcn dot com>
Copyright:  Copyright (c) 2003, 2004 Python Software Foundation. All rights reserved.
한글판 johnsonj 2007.03.06 

목차

요약

기술자를 정의하고, 그 프로토콜을 요약하며, 그리고 어떻게 기술자가 호출되는지 보여준다. 함수, 특성, 정적 메쏘드 그리고 클래스 메쏘드를 포함하여 맞춤 기술자를 조사하고 여러 내장 파이썬 기술자를 조사한다. 순수하게 파이썬 버전으로 그리고 샘플 어플리케이션으로 각각 어떻게 작동하는지 보여준다.

기술자에 관하여 배우면 더 많은 도구모음에 접근할 수 있을 뿐만 아니라 파이썬이 작동하는 법을 더욱 깊게 이해할 수 있고 그 우아한 디자인을 감상할 수 있다.

정의와 소개

일반적으로, 기술자는 "묶기 행위"를 가지는 객체 속성이다. 속성 접근시 기술자 프로토콜의 메쏘드에 의하여 오버라이드 된다. 그런 메쏘드는 __get__, __set__, 그리고 __delete__이다. 이 메쏘드들이 한 객체에 정의되어 있으면 기술자라고 불리운다.

속성 접근에 대한 기본 행위는 객체의 사전에서 그 속성을 얻고 설정하거나 지우는 것이다. 예를 들면, a.x는 탐색 사슬을 가진다. a.__dict__['x']에서 시작하여, 다음 type(a).__dict__['x']으로, 그리고 계속해서 type(a)의 아래 클래스들을 탐색한다. 메타클래스들을 제외하고 말이다. 탐색 값이 객체이고 기술자 메쏘드 중의 하나가 정의되어 있다면, 파이썬은 기본 행위를 오버라이드해서 대신에 그 기술자 메쏘드를 호출할 수도 있다. 우선순위 사슬에서 이런 일이 어디에서 일어날지는 어느 기술자 메쏘드가 정의되어 있는가에 달려 있다. 기술자는 오직 새 스타일의 객체나 클래스에서만 요청된다는 것에 주의하자 (클래스가 object 또는 type으로부터 상속받았다면 새 스타일이다).

기술자는 범용 목적의 강력한 프로토콜이다. 특성과 메쏘드 그리고 정적 메쏘드와 클래스 메쏘드 그리고 super() 뒤에 있는 메커니즘이다. 파이썬 그 자체에서 사용되어 새 스타일의 클래스를 구현한다. 새 스타일의 클래스는 파이썬 2.2 버전에서 도입되었다. 기술자는 밑의 C-코드를 간략하게 하고 일상의 파이썬 프로그램을 위한 새로운 도구 세트를 제공한다.

기술자(Descriptor) 프로토콜

descr.__get__(self, obj, type=None) --> value

descr.__set__(self, obj, value) --> None

descr.__delete__(self, obj) --> None

이게 전부다. 이 메쏘드의 중의 하나만 정의하면 그 객체는 기술자로 간주되며 속성 접근시에 기본 행위를 오버라이드할 수 있다.

객체에 __get____set__이 모두 정의되어 있으면, 그 객체는 데이타 기술자로 간주된다. __get__만 정의된 기술자는 비-데이터 기술자라고 불리운다 (전형적으로 메쏘드에 사용되지만 다른 곳에 사용해도 된다).

데이터 기술자와 비-데이터 기술자는 실체의 사전에 있는 엔트리에 관련하여 오버라이드가 계산되는 방식에서 차이가 있다. 한 실체의 사전에 데이터 기술자와 같은 이름을 가진 엔트리가 존재한다면, 그 데이터 기술자가 우선 순위가 된다. 한 실체의 사전에 비-데이터 기술자와 같은 이름의 엔트리가 존재하면, 사전 엔트리가 우선 순위가 된다.

읽기-전용 데이터 기술자를 만들기 위하여, __get____set____set__에 정의하면 호출될 때 AttributeError가 일어난다. 예외 발생 위치보유자와 함께 __set__ 메쏘드를 정의하면 데이터 기술자로 만들기에 충분하다.

기술자 요청하기

기술자는 그의 메쏘드 이름으로 직접 호출할 수 있다. 예를 들어, d.__get__(obj)와 같이 말이다.

대안적으로, 속성 접근시에 기술자가 자동으로 호출되는 것이 더 일반적이다. 예를 들어, obj.ddobj 사전에서 찾는다. d__get__ 메쏘드가 정의되어 있으면, d.__get__(obj)는 앞서 언급한 우선순위에 따라 요청된다.

요청의 세부사항은 obj가 객체인가 클래스인가에 달려 있다. 어느 것이든, 기술자는 오직 새 스타일의 객체와 클래스에만 작동한다. 클래스가 object의 하부클래스라면 새 스타일이다.

객체에 대하여, 작동 메커니즘은 object.__getattribute__에 있다. 이 메쏘드는 b.xtype(b).__dict__['x'].__get__(b, type(b))으로 변환시킨다. 그 구현은 우선순위 사슬을 따라 작동한다. 실체 변수보다 데이터 기술자에게 우선순위가 있고, 비-데이터 기술자보다 실체 변수가 우선순위가 있으며 혹 제공된다면 __getattr__가 순위가 제일 낮다. 완전한 C 구현은 Objects/object.cPyObject_GenericGetAttr()에서 볼 수 있다.

클래스에 대하여, 작동 메커니즘은 type.__getattribute__에 있다. 이 메쏘드는 B.xB.__dict__['x'].__get__(None, B)로 변환시킨다. 순수 파이썬으로는 다음과 같이 보일 것이다:

def __getattribute__(self, key):
    "Emulate type_getattro() in Objects/typeobject.c"
    v = object.__getattribute__(self, key)
    if hasattr(v, '__get__'):
       return v.__get__(None, self)
    return v

기억해야 할 요점은 다음과 같다:

super()가 돌려주는 객체도 기술자를 요청하는 맞춤 __getattribute__ 메쏘드가 있다. super(B, obj).m()를 호출하면 obj.__class__.__mro__를 검색해서 바탕 클래스 A를 찾고 바로 다음에 B 그리고 다음 A.__dict__['m'].__get__(obj, A)을 돌려준다. 기술자가 아니라면, m이 그대로 반환된다. 사전에 없다면, mobject.__getattribute__을 사용하여 검색에 의존한다.

파이썬 2.2에서, m이 데이터 기술자라면 super(B, obj).m()은 오직 __get__만을 요청할 것이다. 파이썬 2.3에서, 예전-스타일의 클래스가 관련되어 있지만 않다면 비-데이터 기술자도 역시 요청된다. 세부 구현정보는 Objects/typeobject.csuper_getattro()에 있으며 순수 파이썬으로는 귀도(Guido)의 튜토리얼에서 볼 수 있다.

상술한 바에 의하면 기술자를 위한 메커니즘은 object, type, 그리고 super 에 대하여 __getattribute__() 메쏘드에 구현된다. 클래스는 object 객체로부터 상속받을 때 또는 비슷한 기능을 제공하는 메타-클래스가 있다면 이 작동메커니즘을 상속받는다. 마찬가지로, 클래스는 __getattribute__()를 오버라이드 하면 기술자 요청을 끌 수 있다.

기술자의 예

다음 코드는 객체들이 데이터 기술자인 클래스를 만든다. get 또는 set 할 때마다 메시지를 인쇄한다. __getattribute__를 오버라이드 하는 것은 모든 속성에 대하여 이렇게 할 수 있는 또다른 접근법이다. 그렇지만, 이 기술자는 선택된 몇 속성만을 관제하는데 유용하다:

class RevealAccess(object):
    """정상적으로 값들을 설정하고 돌려주는 데이터 기술자
       그리고 접근 기록 메시지를 인쇄한다.
    """

    def __init__(self, initval=None, name='var'):
        self.val = initval
        self.name = name

    def __get__(self, obj, objtype):
        print 'Retrieving', self.name
        return self.val

    def __set__(self, obj, val):
        print 'Updating' , self.name
        self.val = val

>>> class MyClass(object):
    x = RevealAccess(10, 'var "x"')
    y = 5

>>> m = MyClass()
>>> m.x
Retrieving var "x"
10
>>> m.x = 20
Updating var "x"
>>> m.x
Retrieving var "x"
20
>>> m.y
5

프로토콜은 단순하지만 놀라운 가능성을 제공한다. 여러 사용 사례는 너무 흔해서 개인 함수 호출 안으로 들어왔다. 특성, 묶인 또는 풀린 메쏘드, 정적 메쏘드 그리고 클래스 메쏘드는 모두 기술자 프로토콜에 기반한다.

특성(Properties)

property()를 호출하면 속성에 접근할 때 함수 호출을 촉발시키는 데이터 기술자를 간결하게 구축할 수 있다. 그의 시그너처는 다음과 같다:

property(fget=None, fset=None, fdel=None, doc=None) -> property attribute

문서에서 관리 속성 x를 사용하는 전형적인 사용법을 볼 수 있다:

class C(object):
    def getx(self): return self.__x
    def setx(self, value): self.__x = value
    def delx(self): del self.__x
    x = property(getx, setx, delx, "I'm the 'x' property.")

기술자 프로토콜의 관점에서 property()가 어떻게 구현되어 있는지 보고 싶다면, 다음은 똑 같은 기능을 순수하게 파이썬으로 만들어 본 것이다:

class Property(object):
    "Objects/descrobject.c의 PyProperty_Type()를 흉내냄"

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self         
        if self.fget is None:
            raise AttributeError, "unreadable attribute"
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError, "can't set attribute"
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError, "can't delete attribute"
        self.fdel(obj)

property() 내장함수는 사용자 인터페이스가 속성 접근을 허용할 때마다 도움을 주며 그에 이은 변화는 메쏘드의 중재를 요구한다.

예를 들면, 스프레트시트 클래스는 Cell('b10').value를 통하여 셀 값에 접근을 허용할 수 있다. 이어서 프로그램에 개선을 하면 셀에 접근할 때마다 다시 계산을 해야 한다; 그렇지만, 프로그래머는 속성에 직접 접근하는 기존의 클라이언트 코드에 영향을 미치고 싶지 않다. 해결책은 그 속성에 대한 접근을 property() 데이터 기술자 안에 감싸는 것이다:

class Cell(object):
    . . .
    def getvalue(self, obj):
        "값을 돌려주기 전에 셀을 재개산한다"
        self.recalc()
        return obj._value
    value = property(getvalue)

함수와 메쏘드

파이썬의 객체 지향적 특징은 함수 기반의 환경 위에 건설되었다. 비-데이터 기술자를 사용하면, 그 둘은 깔끔하게 합병된다.

클래스 사전은 메쏘드를 함수로 저장한다. 클래스 정의에서 메쏘드는 함수를 만드는 일반적인 도구인 deflambda로 정의된다. 일반적인 함수와의 유일한 차이는 첫 인자가 그 실체를 위하여 보존된다는 것이다. 파이썬의 관례대로, 실체 참조는 self라고 불리우지만 this나 기타 다른 이름으로 불리워도 상관없다.

메쏘드 호출을 지원하려면, 함수에 __get__ 메쏘드가 포함되어 있어서 속성 접근 시에 메쏘드를 묶어주어야 한다. 이것은 모든 함수가 비-데이터 기술자라는 뜻이다. 객체 또는 클래스로부터 요청되는지에 따라 묶인 메쏘드 또는 풀린 메쏘드를 돌려준다. 순수 파이썬으로는 다음과 같이 작동한다:

class Function(object):
    . . .
    def __get__(self, obj, objtype=None):
        "Simulate func_descr_get() in Objects/funcobject.c"
        return types.MethodType(self, obj, objtype)

파이썬을 실행하면 함수 기술자가 실제로 어떻게 작동하는지 볼 수 있다:

>>> class D(object):
     def f(self, x):
          return x

>>> d = D()
>>> D.__dict__['f'] # 내부적으로 함수로 저장된다
<function f at 0x00C45070>
>>> D.f             # 클래스로부터 얻으면 풀린 메쏘드가 된다
<unbound method D.f>
>>> d.f             # 실체로부터 얻으면 묶인 메쏘드가 된다
<bound method D.f of <__main__.D object at 0x00B18C90>>

출력결과를 보면 묶인 메쏘드와 풀린 메쏘드는 종류가 다르다. 구현은 언급한대로 되어 있지만, Objects/classobject.cPyMethod_Type의 실제 C 구현은 im_self 필드가 설정되어 있는지 아니면 NULL(C로는 None과 동등)인지에 따라 두 가지로 다르게 표현되는 객체이다.

마찬가지로, 메쏘드 객체를 호출하는 효과는 im_self 필드에 달려있다. 설정되어 있다면 (묶여 있다는 뜻), (im_func 필드에 저장된) 첫 인자가 그 실체에 설정되어 원래 함수가 예상대로 호출된다. 풀려 있다면, 모든 인자들이 그대로 원래 함수에 건네진다. instancemethod_call() 의 실제 C 구현은 약간 더 복잡할 뿐이다. 여기에 약간의 유형 점검이 포함되어 있을 뿐이다.

정적 메쏘드와 클래스 메쏘드

비-데이터 기술자는 함수를 메쏘드로 묶는 일반 패턴에 변화를 주는 간단한 메커니즘을 제공한다.

요약하면, 함수는 __get__ 메쏘드를 가지고 있어서 속성 접근시에 메쏘드로 변환이 가능하다. 비-데이터 기술자는 obj.f(*args) 호출을 f(obj, *args)로 변환한다. klass.f(*args)를 호출하면 f(*args)가 된다.

다음 차트는 묶기와 그의 두가지 유용한 변형들을 요약해 놓았다:

변형   객체에서 호출됨 클래스에서 호출됨
기술자 함수 f(obj, *args) f(*args)
정적메쏘드 f(*args) f(*args)
클래스 메쏘드 f(type(obj), *args) f(klass, *args)

정적 메쏘드는 밑에 깔린 함수를 그대로 돌려준다. c.f이나 C.f를 호출하면 object.__getattribute__(c, "f") 또는 object.__getattribute__(C, "f")를 직접 찾아본 것과 똑 같다. 결과적으로, 함수는 객체나 클래스에서 똑 같이 접근이 가능해진다.

정적 메쏘드에 대한 훌륭한 후보는 self 변수를 참조하지 않는 메쏘드이다.

예를 들면, 통계 꾸러미에는 실험 데이터를 위한 그릇 클래스가 담길 수 있다. 이 그릇 클래스는 데이터에 의존하여 평균, 중위값, 중앙값, 그리고 기타 기술적 통계를 계산하기 위한 일반 메쏘드를 제공한다. 그렇지만, 개념적으로 관련이 있지만 데이터에 의존하지는 않는 유용한 함수가 있을 수 있다. 예를 들면, erf(x)는 통계 작업에 필수적이지만 직접적으로 특정 데이터 세트에 의존하는 않는 간편한 변환 루틴이다. 객체 또는 클래스에서 호출할 수 있다: s.erf(1.5) --> .9332 또는 Sample.erf(1.5) --> .9332과 같이 말이다.

정적메쏘드는 밑에 깔린 함수를 그대로 돌려주기 때문에, 예제 호출은 재미없다:

>>> class E(object):
     def f(x):
          print x
     f = staticmethod(f)

>>> print E.f(3)
3
>>> print E().f(3)
3

비-데이터 기술자 프로토콜을 사용한, 순수한 파이썬 버전의 staticmethod()는 다음과 같이 보일 것이다:

class StaticMethod(object):
 "Emulate PyStaticMethod_Type() in Objects/funcobject.c"

 def __init__(self, f):
      self.f = f

 def __get__(self, obj, objtype=None):
      return self.f

정적 메쏘드와는 다르게, 클래스 메쏘드는 클래스 호출을 인자 리스트 앞에 붙인 다음 그 함수를 호출한다. 이 형태는 호출자가 객체인가 클래스인가에 따라 다르다:

>>> class E(object):
     def f(klass, x):
          return klass.__name__, x
     f = classmethod(f)

>>> print E.f(3)
('E', 3)
>>> print E().f(3)
('E', 3)

이 행위는 그 함수가 클래스 참조만 가지고 밑에 깔린 데이터에 관해서는 신경쓸 필요가 없을 때 유용하다. 클래스 메쏘드의 한가지 사용예는 대안적인 클래스 구성자를 만드는 것이다. 파이썬 2.3에서, dict.fromkeys() 클래스 메쏘드는 키 리스트로부터 새로운 사전을 만든다. 순수하게 파이썬으로는 다음과 동등하다:

class Dict:
    . . .
    def fromkeys(klass, iterable, value=None):
        "Emulate dict_fromkeys() in Objects/dictobject.c"
        d = klass()
        for key in iterable:
            d[key] = value
        return d
    fromkeys = classmethod(fromkeys)

이제 유일키로 구성된 새로운 사전은 다음과 같이 구성이 가능하다:

>>> Dict.fromkeys('abracadabra')
{'a': None, 'r': None, 'b': None, 'c': None, 'd': None}

비-데이터 기술자 프로토콜을 사용한, 순수 파이썬 버전의 classmethod()는 다음과 같을 것이다:

class ClassMethod(object):
     "Emulate PyClassMethod_Type() in Objects/funcobject.c"

     def __init__(self, f):
          self.f = f

     def __get__(self, obj, klass=None):
          if klass is None:
               klass = type(obj)
          def newfunc(*args):
               return self.f(klass, *args)
          return newfunc