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:
번역·라이선스 안내
이 비공식 한국어 번역은 원문 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 인스턴스에서는 동작하지 않습니다.
이 문제를 해결하려는 제안들은 흔히 “클래스/타입 이분법”, 즉 내장 타입과 클래스 사이의 차이를 없애는 문제를 바로잡는다는 범주 아래 한데 묶여 왔습니다 [7]. 그 자체로는 훌륭한 목표이지만, 앞서 설명한 종류의 프로그래밍 구조를 실현하는 데 이 간극을 메우는 일이 반드시 필요한 것은 아닙니다. 이 제안은 Python의 클래스 객체와 인스턴스 객체를 최소한으로만 수정하여 80%의 해결책을 제공합니다. 이 제안은 타입/클래스 이분법 문제를 해결하는 데는 아무런 조치도 하지 않습니다.
제안
이 제안은 다음과 같은 의미론을 갖는 __findattr__()라는 새로운 특수 메서드를 추가합니다.
- 클래스에 정의되어 있으면, 모든 인스턴스 속성 결정 시
__getattr__()와__setattr__()대신 호출됩니다. __findattr__()는 절대 재귀적으로 호출되지 않습니다. 즉, 특정 인스턴스의__findattr__()가 호출 스택에 있는 동안에는 해당 인스턴스에 대한 이후의 속성 접근이 표준__getattr__()및__setattr__()메서드를 사용합니다.__findattr__()는 속성 접근(‘getting’)과 속성 수정(‘setting’) 모두에 대해 호출됩니다. 속성 삭제 시에는 호출되지 않습니다.- getting을 위해 호출될 때는 (‘self’를 제외하고) 인자 하나, 즉 접근하려는 속성의 이름이 전달됩니다.
- setting을 위해 호출될 때는 세 번째 인자와 함께 호출되며, 이 인자는 속성에 설정할 값입니다.
__findattr__()메서드는__getattr__()및__setattr__()와 동일한 캐싱 의미론을 갖습니다. 즉, 클래스 정의 시점에 클래스에 존재하면 사용되지만, 이후에 클래스에 추가되면 사용되지 않습니다.
기존 프로토콜과의 주요 차이점
__findattr__()의 의미론은 다음과 같은 핵심적인 면에서 기존 프로토콜과 다릅니다.
첫째, 속성이 인스턴스의 __dict__에 있으면 __getattr__()는 절대 호출되지 않습니다. 이는 효율성 때문이기도 하고, 그렇지 않으면 __setattr__()가 인스턴스의 속성에 접근할 방법이 없기 때문이기도 합니다.
둘째, __setattr__()는 인스턴스 속성을 설정할 때 “self.name = foo”와 같은 “일반적인” 문법을 사용할 수 없습니다. 그렇게 하면 __setattr__()가 재귀적으로 호출되기 때문입니다.
__findattr__()는 속성이 __dict__에 있는지와 관계없이 항상 호출되며, 인스턴스 객체의 플래그가 __findattr__()의 재귀 호출을 막습니다. 이를 통해 클래스는 모든 속성 접근에 대해 어떤 동작을 수행할 기회를 얻습니다. 또한 읽기와 쓰기 모두에서 호출되므로, 모든 속성 접근에 대해 비슷한 정책을 작성하기 쉽습니다. 게다가 확장 메커니즘을 사용할 때만 비용이 들기 때문에 효율성도 문제가 되지 않습니다.
관련 작업
PEP 213은 속성 접근과 수정에 훅을 거는 다른 접근 방식을 설명합니다. PEP 213에서 제안한 의미 체계는 여기서 설명하는 __findattr__() 훅을 사용하여 구현할 수 있지만, 한 가지 주의할 점이 있습니다. __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으로 구현하는 것입니다:
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에서 설명한 속성 훅의 구현입니다(단, 현재 참조 구현에서는 속성 삭제에 대한 훅을 지원하지 않습니다).
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__를 사용해 구현할 수 있습니다.
Copyright
This document has been placed in the Public Domain.