Movatterモバイル変換


[0]ホーム

URL:


Following system colour schemeSelected dark colour schemeSelected light colour scheme

Python Enhancement Proposals

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

Introduction

This PEP describes an extension to instance attribute lookup andmodification machinery, which allows pure-Python implementationsof many interesting programming models. This PEP tracks thestatus and ownership of this feature. It contains a descriptionof the feature and outlines changes necessary to support thefeature. This PEP summarizes discussions held in mailing listforums, and provides URLs for further information, whereappropriate. The CVS revision history of this file contains thedefinitive historical record.

Background

The semantics for Python instances allow the programmer tocustomize some aspects of attribute lookup and attributemodification, 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 inPython alone, e.g. strict Java Bean-like[2] interfaces and Zopestyle acquisitions[3]. In the latter case, Zope solves this byincluding a C extension called ExtensionClass[5] which modifiesthe standard class semantics, and uses a metaclass hook inPython’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, buttruck-sized loophole in the Python machinery. Third, it can bedifficult for other programmers to use and understand (themetaclass 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 withExtensionClass instances.

Proposals for fixing this problem have often been lumped under therubric of fixing the “class/type dichotomy”; that is, eliminatingthe difference between built-in types and classes[7]. While alaudable goal itself, repairing this rift is not necessary inorder to achieve the types of programming constructs describedabove. This proposal provides an 80% solution with a minimum ofmodification to Python’s class and instance objects. It doesnothing to address the type/class dichotomy.

Proposal

This proposal adds a new special method called__findattr__() withthe following semantics:

  • If defined in a class, it will be called on all instanceattribute resolutions instead of__getattr__() and__setattr__().
  • __findattr__() is never called recursively. That is, when aspecific instance’s__findattr__() is on the call stack, furtherattribute 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 forattribute deletion.
  • When called for getting, it is passed a single argument (notcounting ‘self’): the name of the attribute being accessed.
  • When called for setting, it is called with third argument, whichis 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 theclass at class definition time, they are used, but if they aresubsequently added to a class later they are not.

Key Differences with the Existing Protocol

__findattr__()’s semantics are different from the existingprotocol in key ways:

First,__getattr__() is never called if the attribute is found inthe instance’s__dict__. This is done for efficiency reasons, andbecause otherwise,__setattr__() would have no way to get to theinstance’s attributes.

Second,__setattr__() cannot use “normal” syntax for settinginstance attributes, e.g. “self.name = foo” because that wouldcause recursive calls to__setattr__().

__findattr__() is always called regardless of whether theattribute is in__dict__ or not, and a flag in the instance objectprevents recursive calls to__findattr__(). This gives the classa chance to perform some action for every attribute access. Andbecause it is called for both gets and sets, it is easy to writesimilar policy for all attribute access. Further, efficiency isnot a problem because it is only paid when the extended mechanismis used.

Related Work

PEP 213 describes a different approach to hooking intoattribute access and modification. The semantics proposed inPEP 213can be implemented using the__findattr__() hook describedhere, 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 JavaBean-like interface to objects, where unadorned attribute accessand modification is transparently mapped to a functionalinterface. E.g.

classBean:def__init__(self,x):self.__myfoo=xdef__findattr__(self,name,*args):ifname.startswith('_'):# Private namesifargs:setattr(self,name,args[0])else:returngetattr(self,name)else:# Public namesifargs:name='_set_'+nameelse:name='_get_'+namereturngetattr(self,name)(*args)def_set_foo(self,x):self.__myfoo=xdef_get_foo(self):returnself.__myfoob=Bean(3)printb.foob.foo=9printb.foo

A second, more elaborate example is the implementation of bothimplicit and explicit acquisition in pure Python:

importtypesclassMethodWrapper:def__init__(self,container,method):self.__container=containerself.__method=methoddef__call__(self,*args,**kws):returnself.__method.im_func(self.__container,*args,**kws)classWrapperImplicit:def__init__(self,contained,container):self.__contained=containedself.__container=containerdef__repr__(self):return'<Wrapper: [%s |%s]>'%(self.__container,self.__contained)def__findattr__(self,name,*args):# Some things are our ownifname.startswith('_WrapperImplicit__'):ifargs:returnsetattr(self,name,*args)else:returngetattr(self,name)# setattr stores the name on the contained object directlyifargs:returnsetattr(self.__contained,name,args[0])# Other special namesifname=='aq_parent':returnself.__containerelifname=='aq_self':returnself.__containedelifname=='aq_base':base=self.__containedtry:while1:base=base.aq_selfexceptAttributeError:returnbase# no acquisition for _ namesifname.startswith('_'):returngetattr(self.__contained,name)# Everything else gets wrappedmissing=[]which=self.__containedobj=getattr(which,name,missing)ifobjismissing:which=self.__containerobj=getattr(which,name,missing)ifobjismissing:raiseAttributeError,nameof=getattr(obj,'__of__',missing)ifofisnotmissing:returnof(self)eliftype(obj)==types.MethodType:returnMethodWrapper(self,obj)returnobjclassWrapperExplicit:def__init__(self,contained,container):self.__contained=containedself.__container=containerdef__repr__(self):return'<Wrapper: [%s |%s]>'%(self.__container,self.__contained)def__findattr__(self,name,*args):# Some things are our ownifname.startswith('_WrapperExplicit__'):ifargs:returnsetattr(self,name,*args)else:returngetattr(self,name)# setattr stores the name on the contained object directlyifargs:returnsetattr(self.__contained,name,args[0])# Other special namesifname=='aq_parent':returnself.__containerelifname=='aq_self':returnself.__containedelifname=='aq_base':base=self.__containedtry:while1:base=base.aq_selfexceptAttributeError:returnbaseelifname=='aq_acquire':returnself.aq_acquire# explicit acquisition onlyobj=getattr(self.__contained,name)iftype(obj)==types.MethodType:returnMethodWrapper(self,obj)returnobjdefaq_acquire(self,name):# Everything else gets wrappedmissing=[]which=self.__containedobj=getattr(which,name,missing)ifobjismissing:which=self.__containerobj=getattr(which,name,missing)ifobjismissing:raiseAttributeError,nameof=getattr(obj,'__of__',missing)ifofisnotmissing:returnof(self)eliftype(obj)==types.MethodType:returnMethodWrapper(self,obj)returnobjclassImplicit:def__of__(self,container):returnWrapperImplicit(self,container)def__findattr__(self,name,*args):# ignore setattrsifargs:returnsetattr(self,name,args[0])obj=getattr(self,name)missing=[]of=getattr(obj,'__of__',missing)ifofisnotmissing:returnof(self)returnobjclassExplicit(Implicit):def__of__(self,container):returnWrapperExplicit(self,container)# testsclassC(Implicit):color='red'classA(Implicit):defreport(self):returnself.color# simple implicit acquisitionc=C()a=A()c.a=aassertc.a.report()=='red'd=C()d.color='green'd.a=aassertd.a.report()=='green'try:a.report()exceptAttributeError:passelse:assert0,'AttributeError expected'# special namesassertc.a.aq_parentiscassertc.a.aq_selfisac.a.d=dassertc.a.d.aq_baseisdassertc.aisnota# no acquisition on _ namesclassE(Implicit):_color='purple'classF(Implicit):defreport(self):returnself._colore=E()f=F()e.f=ftry:e.f.report()exceptAttributeError:passelse:assert0,'AttributeError expected'# explicitclassG(Explicit):color='pink'classH(Explicit):defreport(self):returnself.aq_acquire('color')defbarf(self):returnself.colorg=G()h=H()g.h=hassertg.h.report()=='pink'i=G()i.color='cyan'i.h=hasserti.h.report()=='cyan'try:g.i.barf()exceptAttributeError:passelse:assert0,'AttributeError expected'

C++-like access control can also be accomplished, although lesscleanly because of the difficulty of figuring out what method isbeing called from the runtime call stack:

importsysimporttypesPUBLIC=0PROTECTED=1PRIVATE=2try:getframe=sys._getframeexceptImportError:defgetframe(n):try:raiseExceptionexceptException:frame=sys.exc_info()[2].tb_framewhilen>0:frame=frame.f_backifframeisNone:raiseValueError,'call stack is not deep enough'returnframeclassAccessViolation(Exception):passclassAccess: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 timeifobjisnotmissingandtype(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))ifaccessisnotPUBLIC:# Now try to see which method is calling usframe=getframe(0).f_backifframeisNone:raiseAccessViolation# Get the class of the method that's accessing# this attribute, by using the code object cacheifframe.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.ifaccessisPRIVATE:raiseAccessViolationelse:methclass=self.__cache__.get(frame.f_code)ifnotmethclass:raiseAccessViolationifaccessisPRIVATEandmethclassisnotklass:raiseAccessViolationifaccessisPROTECTEDandnotissubclass(methclass,klass):raiseAccessViolation# If we got here, it must be okay to access the attributeifargs:returnsetattr(self,name,*args)returnobj# testsclassA(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=namedefgetfoo(self):returnself._foodefsetfoo(self,newfoo):self._foo=newfoodefgetname(self):returnself._nameA.__access__={'_foo':(PROTECTED,A),'_name':(PRIVATE,A),'__dict__':(PRIVATE,A),'__access__':(PRIVATE,A),}classB(A):defsetfoo(self,newfoo):self._foo=newfoo+3defsetname(self,name):self._name=nameb=B(1)b.getfoo()a=A(1)asserta.getfoo()==1a.setfoo(2)asserta.getfoo()==2try:a._fooexceptAccessViolation:passelse:assert0,'AccessViolation expected'try:a._foo=3exceptAccessViolation:passelse:assert0,'AccessViolation expected'try:a.__dict__['_foo']exceptAccessViolation:passelse:assert0,'AccessViolation expected'b=B()assertb.getfoo()==0b.setfoo(2)assertb.getfoo()==5try:b.setname('B')exceptAccessViolation:passelse:assert0,'AccessViolation expected'assertb.getname()=='A'

Here’s an implementation of the attribute hook described in PEP213 (except that hooking on attribute deletion isn’t supported bythe current reference implementation).

classPep213:def__findattr__(self,name,*args):hookname='__attr_%s__'%nameifargs:op='set'else:op='get'# XXX: op = 'del' currently not supportedmissing=[]meth=getattr(self,hookname,missing)ifmethismissing:ifop=='set':returnsetattr(self,name,*args)else:returngetattr(self,name)else:returnmeth(op,*args)defcomputation(i):print'doing computation:',ireturni+3defrev_computation(i):print'doing rev_computation:',ireturni-3classX(Pep213):def__init__(self,foo=0):self.__foo=foodef__attr_foo__(self,op,val=None):ifop=='get':returncomputation(self.__foo)elifop=='set':self.__foo=rev_computation(val)# XXX: 'del' not yet supportedx=X()fooval=x.fooprintfoovalx.foo=fooval+5printx.foo# del x.foo

Reference Implementation

The reference implementation, as a patch to the Python core, can befound 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

Rejection

There are serious problems with the recursion-protection feature.As described here it’s not thread-safe, and a thread-safe solutionhas other problems. In general, it’s not clear how helpful therecursion-protection feature is; it makes it hard to write codethat needs to be callable inside__findattr__ as well as outsideit. But without the recursion-protection, it’s hard to implement__findattr__ at all (since__findattr__ would invoke itselfrecursively for every attribute it tries to access). There seemsto be no good solution here.

It’s also dubious how useful it is to support__findattr__ bothfor getting and for setting attributes –__setattr__ gets calledin all cases already.

The examples can all be implemented using__getattr__ if care istaken not to store instance variables under their own names.

Copyright

This document has been placed in the Public Domain.


Source:https://github.com/python/peps/blob/main/peps/pep-0231.rst

Last modified:2025-02-01 08:55:40 GMT


[8]ページ先頭

©2009-2025 Movatter.jp