PEP 231 – __findattr__()
- 作者:
- Barry Warsaw <barry at python.org>
- 状态:
- 已拒绝
- 类型:
- 标准轨迹
- 创建:
- 2000年11月30日
- Python 版本:
- 2.1
- 历史记录:
简介
本 PEP 描述了对实例属性查找和修改机制的扩展,它允许使用纯 Python 实现许多有趣的编程模型。此 PEP 跟踪此功能的状态和所有权。它包含对该功能的描述,并概述了支持该功能所需的更改。此 PEP 总结了在邮件列表论坛中进行的讨论,并在适当的情况下提供了更多信息的 URL。此文件的 CVS 修订历史包含权威的历史记录。
背景
Python 实例的语义允许程序员通过特殊方法 __getattr__()
和 __setattr__()
[1]来自定义属性查找和属性修改的某些方面。
但是,由于这些方法施加的某些限制,有一些有用的编程技巧无法仅用 Python 编写,例如严格的类似 Java Bean 的 [2] 接口和 Zope 样式的获取 [3]。在后一种情况下,Zope 通过包含一个名为 ExtensionClass 的 C 扩展 [5] 来解决此问题,该扩展修改了标准类语义,并在 Python 的类模型中使用了一个元类钩子,该钩子也被称为“Don Beaudry 钩子”或“Don Beaudry 技巧” [6]。
虽然 Zope 的方法有效,但它有几个缺点。首先,它需要一个 C 扩展。其次,它利用了 Python 机制中一个非常神秘但庞大的漏洞。第三,其他程序员可能难以使用和理解(元类具有众所周知的令人崩溃的特性)。第四,由于 ExtensionClass 实例不是“真正的”Python 实例,因此 Python 运行时系统的一些方面不适用于 ExtensionClass 实例。
解决此问题的提案通常被归类为修复“类/类型二分法”;也就是说,消除内置类型和类之间的差异 [7]。虽然本身是一个值得称赞的目标,但修复这种裂痕对于实现上述类型的编程结构并不是必要的。本提案提供了一个 80% 的解决方案,对 Python 的类和实例对象进行最少的修改。它不会解决类型/类二分法。
提案
本提案添加了一个名为 __findattr__()
的新特殊方法,其语义如下:
- 如果在类中定义,它将在所有实例属性解析中被调用,而不是
__getattr__()
和__setattr__()
。 __findattr__()
永远不会被递归调用。也就是说,当特定实例的__findattr__()
位于调用栈上时,该实例的进一步属性访问将使用标准的__getattr__()
和__setattr__()
方法。__findattr__()
用于属性访问(“获取”)和属性修改(“设置”)。它不适用于属性删除。- 当用于获取时,它传递一个参数(不包括“self”):被访问的属性的名称。
- 当用于设置时,它被调用时带有第三个参数,即要设置为属性的值。
__findattr__()
方法具有与__getattr__()
和__setattr__()
相同的缓存语义;即,如果它们在类定义时存在于类中,则使用它们,但如果随后在类中添加它们,则不会使用它们。
与现有协议的关键差异
__findattr__()
的语义在关键方面与现有协议不同
首先,如果在实例的 __dict__
中找到属性,则永远不会调用 __getattr__()
。这样做是为了提高效率,因为否则,__setattr__()
将无法访问实例的属性。
其次,__setattr__()
不能使用“正常”语法设置实例属性,例如“self.name = foo”,因为这会导致对 __setattr__()
的递归调用。
__findattr__()
始终被调用,无论属性是否在 __dict__
中,并且实例对象中的一个标志可以防止对 __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__
来实现。
版权
本文档已置于公有领域。
来源: https://github.com/python/peps/blob/main/peps/pep-0231.rst
上次修改: 2023-09-09 17:39:29 GMT