diff --git a/pep-0000.txt b/pep-0000.txt index bc1bc9d53..57090d2d9 100644 --- a/pep-0000.txt +++ b/pep-0000.txt @@ -39,6 +39,7 @@ Index by Category I 226 pep-0226.txt Python 2.1 Release Schedule Hylton S 227 pep-0227.txt Statically Nested Scopes Hylton S 230 pep-0230.txt Warning Framework van Rossum + S 231 pep-0231.txt __findattr__() Warsaw Pie-in-the-sky PEPs (not ready; may become active yet) @@ -125,6 +126,7 @@ Numerical Index S 228 pep-0228.txt Reworking Python's Numeric Model Zadka S 229 pep-0229.txt Using Distutils to Build Python Kuchling S 230 pep-0230.txt Warning Framework van Rossum + S 231 pep-0231.txt __findattr__() Warsaw Key diff --git a/pep-0231.txt b/pep-0231.txt new file mode 100644 index 000000000..cf81ac773 --- /dev/null +++ b/pep-0231.txt @@ -0,0 +1,551 @@ +PEP: 231 +Title: __findattr__() +Version: $Revision$ +Author: barry@digicool.com (Barry A. Warsaw) +Python-Version: 2.1 +Status: Draft +Created: 30-Nov-2000 +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. + + +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 '' % (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 '' % (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 acquisiton 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' + + + And finally, 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' + + +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://www.python.org/doc/current/ref/attribute-access.html + [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://www.python.org/doc/howto/rexec/rexec.html + + +Copyright + + This document has been placed in the Public Domain. + + + +Local Variables: +mode: indented-text +indent-tabs-mode: nil +End: