Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 231 – __findattr__()

Author:
Barry Warsaw <barry at python.org>
Status:
Rejected
Type:
Standards Track
Created:
30-Nov-2000
Python-Version:
2.1
Post-History:


Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

소개

이 PEP는 인스턴스 속성 조회 및 수정 메커니즘에 대한 확장을 설명하며, 이를 통해 여러 흥미로운 프로그래밍 모델을 순수 Python으로 구현할 수 있습니다. 이 PEP는 이 기능의 상태와 소유권을 추적합니다. 이 문서는 해당 기능에 대한 설명을 담고 있으며, 이를 지원하는 데 필요한 변경 사항을 개략적으로 설명합니다. 이 PEP는 메일링 리스트 포럼에서 있었던 논의를 요약하며, 필요한 경우 추가 정보를 위한 URL을 제공합니다. 이 파일의 CVS 리비전 기록에는 확정적인 이력 기록이 담겨 있습니다.

배경

Python 인스턴스의 시맨틱스는 프로그래머가 특수 메서드 __getattr__()__setattr__() [1]을 통해 속성 조회와 속성 수정의 일부 측면을 커스터마이즈할 수 있도록 허용합니다.

그러나 이 메서드들이 부과하는 특정 제약 때문에, 엄격한 Java Bean류 [2] 인터페이스와 Zope 스타일 acquisition [3] 같이 Python만으로는 작성할 수 없는 유용한 프로그래밍 기법들이 존재합니다. 후자의 경우, Zope는 ExtensionClass [5]라는 C 확장을 포함시켜 표준 클래스 시맨틱스를 수정하고, “Don Beaudry Hook” 또는 “Don Beaudry Hack” [6]이라고도 불리는 Python 클래스 모델의 메타클래스 훅을 사용함으로써 이 문제를 해결합니다.

Zope의 접근 방식은 동작하기는 하지만, 몇 가지 단점이 있습니다. 첫째, C 확장이 필요합니다. 둘째, Python 메커니즘에서 매우 난해하지만 트럭이 지나갈 만큼 큰 허점을 이용합니다. 셋째, 다른 프로그래머가 사용하고 이해하기 어려울 수 있습니다(그 메타클래스는 머리가 폭발할 정도라는 것으로 잘 알려져 있습니다). 그리고 넷째, ExtensionClass 인스턴스는 “진짜” Python 인스턴스가 아니기 때문에, Python 런타임 시스템의 일부 측면이 ExtensionClass 인스턴스에서는 동작하지 않습니다.

이 문제를 해결하기 위한 제안들은 흔히 “클래스/타입 이분법(class/type dichotomy)”을 바로잡는다는 명목 아래 뭉뚱그려져 왔습니다. 즉, 내장 타입과 클래스 사이의 차이를 없애는 것입니다 [7]. 그 자체로는 칭찬할 만한 목표이지만, 이 균열을 바로잡는 것이 위에서 설명한 종류의 프로그래밍 구성체들을 구현하기 위해 반드시 필요한 것은 아닙니다. 이 제안은 Python의 클래스 및 인스턴스 객체에 최소한의 수정만 가하면서도 80% 수준의 해법을 제공합니다. 이 제안은 타입/클래스 이분법 문제를 해결하는 데는 아무런 기여도 하지 않습니다.

제안

이 제안은 다음과 같은 의미론을 갖는 __findattr__()이라는 새로운 특수 메서드를 추가합니다.

  • 클래스에 정의된 경우, 이는 모든 인스턴스 속성 결정에서 __getattr__()__setattr__() 대신 호출됩니다.
  • __findattr__()은 결코 재귀적으로 호출되지 않습니다. 즉, 특정 인스턴스의 __findattr__()이 호출 스택에 있는 동안에는, 그 인스턴스에 대한 이후의 속성 접근은 표준 __getattr__()__setattr__() 메서드를 사용합니다.
  • __findattr__()은 속성 접근(‘getting’)과 속성 수정(‘setting’) 모두에 대해 호출됩니다. 속성 삭제에 대해서는 호출되지 않습니다.
  • getting을 위해 호출될 때는 인자 하나(‘self’는 제외)를 전달받는데, 바로 접근 중인 속성의 이름입니다.
  • setting을 위해 호출될 때는 세 번째 인자와 함께 호출되는데, 이는 속성에 설정할 값입니다.
  • __findattr__() 메서드는 __getattr__()__setattr__()과 동일한 캐싱 의미론을 가집니다. 즉, 클래스 정의 시점에 클래스 안에 존재하면 사용되지만, 이후에 클래스에 추가되면 사용되지 않습니다.

기존 프로토콜과의 주요 차이점

__findattr__()의 의미론은 기존 프로토콜과 몇 가지 핵심적인 면에서 다릅니다.

첫째, __getattr__()는 속성이 인스턴스의 __dict__에서 발견되면 결코 호출되지 않습니다. 이는 효율성을 위한 것이며, 그렇지 않으면 __setattr__()가 인스턴스의 속성에 접근할 방법이 없기 때문입니다.

둘째, __setattr__()는 인스턴스 속성을 설정하는 데 “self.name = foo”와 같은 “일반적인” 구문을 사용할 수 없는데, 이는 __setattr__()에 대한 재귀 호출을 유발하기 때문입니다.

__findattr__()는 속성이 __dict__에 있는지 여부와 관계없이 항상 호출되며, 인스턴스 객체 내의 플래그가 __findattr__()에 대한 재귀 호출을 막습니다. 이는 클래스에게 모든 속성 접근에 대해 어떤 동작을 수행할 기회를 제공합니다. 그리고 get과 set 모두에 대해 호출되기 때문에, 모든 속성 접근에 대해 유사한 정책을 작성하기 쉽습니다. 게다가, 확장된 메커니즘이 사용될 때만 비용이 발생하므로 효율성은 문제가 되지 않습니다.

관련 작업

PEP 213은 속성 접근 및 수정에 개입하는 다른 접근법을 설명합니다. 여기서 설명한 __findattr__() 훅을 사용하면 PEP 213에서 제안된 의미론을 구현할 수 있지만, 한 가지 주의할 점이 있습니다. __findattr__()의 현재 참조 구현은 속성 삭제에 대한 훅을 지원하지 않습니다. 바람직하다고 판단되면 이를 추가할 수 있습니다. 아래 예제를 참조하십시오.

예제

이 제안이 허용하는 하나의 프로그래밍 스타일은 객체에 대한 Java Bean과 유사한 인터페이스로, 꾸밈없는 속성 접근 및 수정이 함수형 인터페이스로 투명하게 매핑됩니다. 예를 들면.

class Bean:
    def __init__(self, x):
        self.__myfoo = x

    def __findattr__(self, name, *args):
        if name.startswith('_'):
            # Private names
            if args: setattr(self, name, args[0])
            else:    return getattr(self, name)
        else:
            # Public names
            if args: name = '_set_' + name
            else:    name = '_get_' + name
            return getattr(self, name)(*args)

    def _set_foo(self, x):
        self.__myfoo = x

    def _get_foo(self):
        return self.__myfoo


b = Bean(3)
print b.foo
b.foo = 9
print b.foo

두 번째, 더 정교한 예제는 순수 Python에서 암묵적 획득(implicit acquisition)과 명시적 획득(explicit acquisition)을 모두 구현한 것입니다.:

import types

class MethodWrapper:
    def __init__(self, container, method):
        self.__container = container
        self.__method = method

    def __call__(self, *args, **kws):
        return self.__method.im_func(self.__container, *args, **kws)


class WrapperImplicit:
    def __init__(self, contained, container):
        self.__contained = contained
        self.__container = container

    def __repr__(self):
        return '<Wrapper: [%s | %s]>' % (self.__container,
                                         self.__contained)

    def __findattr__(self, name, *args):
        # Some things are our own
        if name.startswith('_WrapperImplicit__'):
            if args: return setattr(self, name, *args)
            else:    return getattr(self, name)
        # setattr stores the name on the contained object directly
        if args:
            return setattr(self.__contained, name, args[0])
        # Other special names
        if name == 'aq_parent':
            return self.__container
        elif name == 'aq_self':
            return self.__contained
        elif name == 'aq_base':
            base = self.__contained
            try:
                while 1:
                    base = base.aq_self
            except AttributeError:
                return base
        # no acquisition for _ names
        if name.startswith('_'):
            return getattr(self.__contained, name)
        # Everything else gets wrapped
        missing = []
        which = self.__contained
        obj = getattr(which, name, missing)
        if obj is missing:
            which = self.__container
            obj = getattr(which, name, missing)
            if obj is missing:
                raise AttributeError, name
        of = getattr(obj, '__of__', missing)
        if of is not missing:
            return of(self)
        elif type(obj) == types.MethodType:
            return MethodWrapper(self, obj)
        return obj


class WrapperExplicit:
    def __init__(self, contained, container):
        self.__contained = contained
        self.__container = container

    def __repr__(self):
        return '<Wrapper: [%s | %s]>' % (self.__container,
                                         self.__contained)

    def __findattr__(self, name, *args):
        # Some things are our own
        if name.startswith('_WrapperExplicit__'):
            if args: return setattr(self, name, *args)
            else:    return getattr(self, name)
        # setattr stores the name on the contained object directly
        if args:
            return setattr(self.__contained, name, args[0])
        # Other special names
        if name == 'aq_parent':
            return self.__container
        elif name == 'aq_self':
            return self.__contained
        elif name == 'aq_base':
            base = self.__contained
            try:
                while 1:
                    base = base.aq_self
            except AttributeError:
                return base
        elif name == 'aq_acquire':
            return self.aq_acquire
        # explicit acquisition only
        obj = getattr(self.__contained, name)
        if type(obj) == types.MethodType:
            return MethodWrapper(self, obj)
        return obj

    def aq_acquire(self, name):
        # Everything else gets wrapped
        missing = []
        which = self.__contained
        obj = getattr(which, name, missing)
        if obj is missing:
            which = self.__container
            obj = getattr(which, name, missing)
            if obj is missing:
                raise AttributeError, name
        of = getattr(obj, '__of__', missing)
        if of is not missing:
            return of(self)
        elif type(obj) == types.MethodType:
            return MethodWrapper(self, obj)
        return obj


class Implicit:
    def __of__(self, container):
        return WrapperImplicit(self, container)

    def __findattr__(self, name, *args):
        # ignore setattrs
        if args:
            return setattr(self, name, args[0])
        obj = getattr(self, name)
        missing = []
        of = getattr(obj, '__of__', missing)
        if of is not missing:
            return of(self)
        return obj


class Explicit(Implicit):
    def __of__(self, container):
        return WrapperExplicit(self, container)


# tests
class C(Implicit):
    color = 'red'

class A(Implicit):
    def report(self):
        return self.color

# simple implicit acquisition
c = C()
a = A()
c.a = a
assert c.a.report() == 'red'

d = C()
d.color = 'green'
d.a = a
assert d.a.report() == 'green'

try:
    a.report()
except AttributeError:
    pass
else:
    assert 0, 'AttributeError expected'


# special names
assert c.a.aq_parent is c
assert c.a.aq_self is a

c.a.d = d
assert c.a.d.aq_base is d
assert c.a is not a


# no acquisition on _ names
class E(Implicit):
    _color = 'purple'

class F(Implicit):
    def report(self):
        return self._color

e = E()
f = F()
e.f = f
try:
    e.f.report()
except AttributeError:
    pass
else:
    assert 0, 'AttributeError expected'


# explicit
class G(Explicit):
    color = 'pink'

class H(Explicit):
    def report(self):
        return self.aq_acquire('color')

    def barf(self):
        return self.color

g = G()
h = H()
g.h = h
assert g.h.report() == 'pink'

i = G()
i.color = 'cyan'
i.h = h
assert i.h.report() == 'cyan'

try:
    g.i.barf()
except AttributeError:
    pass
else:
    assert 0, 'AttributeError expected'

C++와 유사한 접근 제어도 구현할 수 있지만, 런타임 호출 스택에서 어떤 메서드가 호출되고 있는지 알아내기가 어려워서 그다지 깔끔하지는 않습니다.:

import sys
import types

PUBLIC = 0
PROTECTED = 1
PRIVATE = 2

try:
    getframe = sys._getframe
except ImportError:
    def getframe(n):
        try: raise Exception
        except Exception:
            frame = sys.exc_info()[2].tb_frame
        while n > 0:
            frame = frame.f_back
            if frame is None:
                raise ValueError, 'call stack is not deep enough'
        return frame


class AccessViolation(Exception):
    pass


class Access:
    def __findattr__(self, name, *args):
        methcache = self.__dict__.setdefault('__cache__', {})
        missing = []
        obj = getattr(self, name, missing)
        # if obj is missing we better be doing a setattr for
        # the first time
        if obj is not missing and type(obj) == types.MethodType:
            # Digusting hack because there's no way to
            # dynamically figure out what the method being
            # called is from the stack frame.
            methcache[obj.im_func.func_code] = obj.im_class
        #
        # What's the access permissions for this name?
        access, klass = getattr(self, '__access__', {}).get(
            name, (PUBLIC, 0))
        if access is not PUBLIC:
            # Now try to see which method is calling us
            frame = getframe(0).f_back
            if frame is None:
                raise AccessViolation
            # Get the class of the method that's accessing
            # this attribute, by using the code object cache
            if frame.f_code.co_name == '__init__':
                # There aren't entries in the cache for ctors,
                # because the calling mechanism doesn't go
                # through __findattr__().  Are there other
                # methods that might have the same behavior?
                # Since we can't know who's __init__ we're in,
                # for now we'll assume that only protected and
                # public attrs can be accessed.
                if access is PRIVATE:
                    raise AccessViolation
            else:
                methclass = self.__cache__.get(frame.f_code)
                if not methclass:
                    raise AccessViolation
                if access is PRIVATE and methclass is not klass:
                    raise AccessViolation
                if access is PROTECTED and not issubclass(methclass,
                                                          klass):
                    raise AccessViolation
        # If we got here, it must be okay to access the attribute
        if args:
            return setattr(self, name, *args)
        return obj

# tests
class A(Access):
    def __init__(self, foo=0, name='A'):
        self._foo = foo
        # can't set private names in __init__
        self.__initprivate(name)

    def __initprivate(self, name):
        self._name = name

    def getfoo(self):
        return self._foo

    def setfoo(self, newfoo):
        self._foo = newfoo

    def getname(self):
        return self._name

A.__access__ = {'_foo'      : (PROTECTED, A),
                '_name'     : (PRIVATE, A),
                '__dict__'  : (PRIVATE, A),
                '__access__': (PRIVATE, A),
                }

class B(A):
    def setfoo(self, newfoo):
        self._foo = newfoo + 3

    def setname(self, name):
        self._name = name

b = B(1)
b.getfoo()

a = A(1)
assert a.getfoo() == 1
a.setfoo(2)
assert a.getfoo() == 2

try:
    a._foo
except AccessViolation:
    pass
else:
    assert 0, 'AccessViolation expected'

try:
    a._foo = 3
except AccessViolation:
    pass
else:
    assert 0, 'AccessViolation expected'

try:
    a.__dict__['_foo']
except AccessViolation:
    pass
else:
    assert 0, 'AccessViolation expected'


b = B()
assert b.getfoo() == 0
b.setfoo(2)
assert b.getfoo() == 5
try:
    b.setname('B')
except AccessViolation:
    pass
else:
    assert 0, 'AccessViolation expected'

assert b.getname() == 'A'

다음은 PEP 213에서 설명한 어트리뷰트 훅(attribute hook)의 구현입니다(다만 어트리뷰트 삭제에 대한 훅은 현재의 참조 구현에서 지원되지 않습니다).

class Pep213:
    def __findattr__(self, name, *args):
        hookname = '__attr_%s__' % name
        if args:
            op = 'set'
        else:
            op = 'get'
        # XXX: op = 'del' currently not supported
        missing = []
        meth = getattr(self, hookname, missing)
        if meth is missing:
            if op == 'set':
                return setattr(self, name, *args)
            else:
                return getattr(self, name)
        else:
            return meth(op, *args)


def computation(i):
    print 'doing computation:', i
    return i + 3


def rev_computation(i):
    print 'doing rev_computation:', i
    return i - 3


class X(Pep213):
    def __init__(self, foo=0):
        self.__foo = foo

    def __attr_foo__(self, op, val=None):
        if op == 'get':
            return computation(self.__foo)
        elif op == 'set':
            self.__foo = rev_computation(val)
        # XXX: 'del' not yet supported

x = X()
fooval = x.foo
print fooval
x.foo = fooval + 5
print x.foo
# del x.foo

참조 구현

Python 코어에 대한 패치 형태의 참조 구현은 다음 URL에서 찾을 수 있습니다:

http://sourceforge.net/patch/?func=detailpatch&patch_id=102613&group_id=5470

참고 자료

거부

재귀 방지 기능에는 심각한 문제가 있습니다. 여기서 설명한 대로 이 기능은 스레드 안전하지 않으며, 스레드 안전한 해결책에는 또 다른 문제가 있습니다. 일반적으로 재귀 방지 기능이 얼마나 유용한지는 명확하지 않습니다. 이 기능은 __findattr__ 내부에서뿐만 아니라 외부에서도 호출 가능 객체여야 하는 코드를 작성하기 어렵게 만듭니다. 하지만 재귀 방지 기능이 없으면 __findattr__를 구현하는 것 자체가 어렵습니다(__findattr__가 접근을 시도하는 모든 속성에 대해 자기 자신을 재귀적으로 호출하게 되기 때문입니다). 여기에는 좋은 해결책이 없어 보입니다.

속성을 가져올 때와 설정할 때 모두에 대해 __findattr__를 지원하는 것이 얼마나 유용한지도 의문입니다. __setattr__는 이미 모든 경우에 호출됩니다.

인스턴스 변수를 자신의 이름으로 저장하지 않도록 주의하면 이 예제들은 모두 __getattr__를 사용해 구현할 수 있습니다.