Permalink
Cannot retrieve contributors at this time
650 lines (520 sloc)
19.5 KB
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| PEP: 231 | |
| Title: __findattr__() | |
| Version: $Revision$ | |
| Last-Modified: $Date$ | |
| Author: barry@python.org (Barry Warsaw) | |
| Status: Rejected | |
| Type: Standards Track | |
| Content-Type: text/x-rst | |
| Created: 30-Nov-2000 | |
| Python-Version: 2.1 | |
| Post-History: | |
| Introduction | |
| ============ | |
| This PEP describes an extension to instance attribute lookup and | |
| modification machinery, which allows pure-Python implementations | |
| of many interesting programming models. This PEP tracks the | |
| status and ownership of this feature. It contains a description | |
| of the feature and outlines changes necessary to support the | |
| feature. This PEP summarizes discussions held in mailing list | |
| forums, and provides URLs for further information, where | |
| appropriate. The CVS revision history of this file contains the | |
| definitive historical record. | |
| Background | |
| ========== | |
| The semantics for Python instances allow the programmer to | |
| customize some aspects of attribute lookup and attribute | |
| modification, through the special methods ``__getattr__()`` and | |
| ``__setattr__()`` [1]_. | |
| However, because of certain restrictions imposed by these methods, | |
| there are useful programming techniques that can not be written in | |
| Python alone, e.g. strict Java Bean-like [2]_ interfaces and Zope | |
| style acquisitions [3]_. In the latter case, Zope solves this by | |
| including a C extension called ExtensionClass [5]_ which modifies | |
| the standard class semantics, and uses a metaclass hook in | |
| Python's class model called alternatively the "Don Beaudry Hook" | |
| or "Don Beaudry Hack" [6]_. | |
| While Zope's approach works, it has several disadvantages. First, | |
| it requires a C extension. Second it employs a very arcane, but | |
| truck-sized loophole in the Python machinery. Third, it can be | |
| difficult for other programmers to use and understand (the | |
| metaclass has well-known brain exploding properties). And fourth, | |
| because ExtensionClass instances aren't "real" Python instances, | |
| some aspects of the Python runtime system don't work with | |
| ExtensionClass instances. | |
| Proposals for fixing this problem have often been lumped under the | |
| rubric of fixing the "class/type dichotomy"; that is, eliminating | |
| the difference between built-in types and classes [7]_. While a | |
| laudable goal itself, repairing this rift is not necessary in | |
| order to achieve the types of programming constructs described | |
| above. This proposal provides an 80% solution with a minimum of | |
| modification to Python's class and instance objects. It does | |
| nothing to address the type/class dichotomy. | |
| Proposal | |
| ======== | |
| This proposal adds a new special method called ``__findattr__()`` with | |
| the following semantics: | |
| * If defined in a class, it will be called on all instance | |
| attribute resolutions instead of ``__getattr__()`` and | |
| ``__setattr__()``. | |
| * ``__findattr__()`` is never called recursively. That is, when a | |
| specific instance's ``__findattr__()`` is on the call stack, further | |
| attribute accesses for that instance will use the standard | |
| ``__getattr__()`` and ``__setattr__()`` methods. | |
| * ``__findattr__()`` is called for both attribute access ('getting') | |
| and attribute modification ('setting'). It is not called for | |
| attribute deletion. | |
| * When called for getting, it is passed a single argument (not | |
| counting 'self'): the name of the attribute being accessed. | |
| * When called for setting, it is called with third argument, which | |
| is the value to set the attribute to. | |
| * ``__findattr__()`` methods have the same caching semantics as | |
| ``__getattr__()`` and ``__setattr__()``; i.e. if they are present in the | |
| class at class definition time, they are used, but if they are | |
| subsequently added to a class later they are not. | |
| Key Differences with the Existing Protocol | |
| ========================================== | |
| ``__findattr__()``'s semantics are different from the existing | |
| protocol in key ways: | |
| First, ``__getattr__()`` is never called if the attribute is found in | |
| the instance's ``__dict__``. This is done for efficiency reasons, and | |
| because otherwise, ``__setattr__()`` would have no way to get to the | |
| instance's attributes. | |
| Second, ``__setattr__()`` cannot use "normal" syntax for setting | |
| instance attributes, e.g. "self.name = foo" because that would | |
| cause recursive calls to ``__setattr__()``. | |
| ``__findattr__()`` is always called regardless of whether the | |
| attribute is in ``__dict__`` or not, and a flag in the instance object | |
| prevents recursive calls to ``__findattr__()``. This gives the class | |
| a chance to perform some action for every attribute access. And | |
| because it is called for both gets and sets, it is easy to write | |
| similar policy for all attribute access. Further, efficiency is | |
| not a problem because it is only paid when the extended mechanism | |
| is used. | |
| Related Work | |
| ============ | |
| :pep:`213` describes a different approach to hooking into | |
| attribute access and modification. The semantics proposed in :pep:`213` | |
| can be implemented using the ``__findattr__()`` hook described | |
| here, with one caveat. The current reference implementation of | |
| ``__findattr__()`` does not support hooking on attribute deletion. | |
| This could be added if it's found desirable. See example below. | |
| Examples | |
| ======== | |
| One programming style that this proposal allows is a Java | |
| Bean-like interface to objects, where unadorned attribute access | |
| and modification is transparently mapped to a functional | |
| interface. E.g. | |
| :: | |
| 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 | |
| A second, more elaborate example is the implementation of both | |
| implicit and explicit acquisition in pure 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++-like access control can also be accomplished, although less | |
| cleanly because of the difficulty of figuring out what method is | |
| being called from the runtime call stack:: | |
| 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' | |
| Here's an implementation of the attribute hook described in PEP | |
| 213 (except that hooking on attribute deletion isn't supported by | |
| the current reference implementation). | |
| :: | |
| 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 | |
| Reference Implementation | |
| ======================== | |
| The reference implementation, as a patch to the Python core, can be | |
| found at this URL: | |
| http://sourceforge.net/patch/?func=detailpatch&patch_id=102613&group_id=5470 | |
| References | |
| ========== | |
| .. [1] http://docs.python.org/reference/datamodel.html#customizing-attribute-access | |
| .. [2] http://www.javasoft.com/products/javabeans/ | |
| .. [3] http://www.digicool.com/releases/ExtensionClass/Acquisition.html | |
| .. [5] http://www.digicool.com/releases/ExtensionClass | |
| .. [6] http://www.python.org/doc/essays/metaclasses/ | |
| .. [7] http://www.foretec.com/python/workshops/1998-11/dd-ascher-sum.html | |
| .. [8] http://docs.python.org/howto/regex.html | |
| Rejection | |
| ========= | |
| There are serious problems with the recursion-protection feature. | |
| As described here it's not thread-safe, and a thread-safe solution | |
| has other problems. In general, it's not clear how helpful the | |
| recursion-protection feature is; it makes it hard to write code | |
| that needs to be callable inside ``__findattr__`` as well as outside | |
| it. But without the recursion-protection, it's hard to implement | |
| ``__findattr__`` at all (since ``__findattr__`` would invoke itself | |
| recursively for every attribute it tries to access). There seems | |
| to be no good solution here. | |
| It's also dubious how useful it is to support ``__findattr__`` both | |
| for getting and for setting attributes -- ``__setattr__`` gets called | |
| in all cases already. | |
| The examples can all be implemented using ``__getattr__`` if care is | |
| taken not to store instance variables under their own names. | |
| Copyright | |
| ========= | |
| This document has been placed in the Public Domain. | |
| .. | |
| Local Variables: | |
| mode: indented-text | |
| indent-tabs-mode: nil | |
| End: |