2000-07-21 16:05:07 -04:00
|
|
|
|
PEP: 213
|
|
|
|
|
Title: Attribute Access Handlers
|
|
|
|
|
Version: $Revision$
|
|
|
|
|
Owner: paul@prescod.net (Paul Prescod)
|
|
|
|
|
Python-Version: 2.0
|
|
|
|
|
Status: Incomplete
|
|
|
|
|
|
|
|
|
|
|
2000-07-21 17:14:26 -04:00
|
|
|
|
Introduction
|
|
|
|
|
|
|
|
|
|
It is possible (and even relatively common) in Python code and
|
|
|
|
|
in extension modules to "trap" when an instance's client code
|
|
|
|
|
attempts to set an attribute and execute code instead. In other
|
|
|
|
|
words it is possible to allow users to use attribute assignment/
|
|
|
|
|
retrieval/deletion syntax even though the underlying implementation
|
|
|
|
|
is doing some computation rather than directly modifying a
|
|
|
|
|
binding.
|
|
|
|
|
|
|
|
|
|
This PEP describes a feature that makes it easier, more efficient
|
|
|
|
|
and safer to implement these handlers for Python instances.
|
|
|
|
|
|
|
|
|
|
Justification
|
|
|
|
|
|
|
|
|
|
Scenario 1:
|
|
|
|
|
|
|
|
|
|
You have a deployed class that works on an attribute named
|
|
|
|
|
"stdout". After a while, you think it would be better to
|
|
|
|
|
check that stdout is really an object with a "write" method
|
|
|
|
|
at the moment of assignment. Rather than change to a
|
|
|
|
|
setstdout method (which would be incompatible with deployed
|
|
|
|
|
code) you would rather trap the assignment and check the
|
|
|
|
|
object's type.
|
|
|
|
|
|
|
|
|
|
Scenario 2:
|
|
|
|
|
|
|
|
|
|
You want to be as compatible as possible with an object
|
|
|
|
|
model that has a concept of attribute assignment. It could
|
|
|
|
|
be the W3C Document Object Model or a particular COM
|
|
|
|
|
interface (e.g. the PowerPoint interface). In that case
|
|
|
|
|
you may well want attributes in the model to show up as
|
|
|
|
|
attributes in the Python interface, even though the
|
|
|
|
|
underlying implementation may not use attributes at all.
|
|
|
|
|
|
|
|
|
|
Scenario 3:
|
|
|
|
|
|
|
|
|
|
A user wants to make an attribute read-only.
|
|
|
|
|
|
|
|
|
|
In short, this feature allows programmers to separate the
|
|
|
|
|
interface of their module from the underlying implementation
|
|
|
|
|
for whatever purpose. Again, this is not a new feature but
|
|
|
|
|
merely a new syntax for an existing convention.
|
|
|
|
|
|
|
|
|
|
Current Solution
|
|
|
|
|
|
|
|
|
|
To make some attributes read-only:
|
|
|
|
|
|
|
|
|
|
class foo:
|
|
|
|
|
def __setattr__( self, name, val ):
|
|
|
|
|
if name=="readonlyattr":
|
|
|
|
|
raise TypeError
|
|
|
|
|
elif name=="readonlyattr2":
|
|
|
|
|
raise TypeError
|
|
|
|
|
...
|
|
|
|
|
else:
|
|
|
|
|
self.__dict__["name"]=val
|
|
|
|
|
|
|
|
|
|
This has the following problems:
|
|
|
|
|
|
|
|
|
|
1. The creator of the method must be intimately aware of whether
|
|
|
|
|
somewhere else in the class hiearchy __setattr__ has also been
|
|
|
|
|
trapped for any particular purpose. If so, she must specifically
|
|
|
|
|
call that method rather than assigning to the dictionary. There
|
|
|
|
|
are many different reasons to overload __setattr__ so there is a
|
|
|
|
|
decent potential for clashes. For instance object database
|
|
|
|
|
implementations often overload setattr for an entirely unrelated
|
|
|
|
|
purpose.
|
|
|
|
|
|
|
|
|
|
2. The string-based switch statement forces all attribute handlers
|
|
|
|
|
to be specified in one place in the code. They may then dispatch
|
|
|
|
|
to task-specific methods (for modularity) but this could cause
|
|
|
|
|
performance problems.
|
|
|
|
|
|
|
|
|
|
3. Logic for the setting, getting and deleting must live in
|
|
|
|
|
__getattr__, __setattr__ and __delattr__. Once again, this can be
|
|
|
|
|
mitigated through an extra level of method call but this is
|
|
|
|
|
inefficient.
|
|
|
|
|
|
|
|
|
|
Proposed Syntax
|
|
|
|
|
|
|
|
|
|
Special methods should declare themselves with declarations of the
|
|
|
|
|
following form:
|
|
|
|
|
|
|
|
|
|
class x:
|
|
|
|
|
def __attr_XXX__(self, op, val ):
|
|
|
|
|
if op=="get":
|
|
|
|
|
return someComputedValue(self.internal)
|
|
|
|
|
elif op=="set":
|
|
|
|
|
self.internal=someComputedValue(val)
|
|
|
|
|
elif op=="del":
|
|
|
|
|
del self.internal
|
|
|
|
|
|
|
|
|
|
Client code looks like this:
|
|
|
|
|
|
|
|
|
|
fooval=x.foo
|
|
|
|
|
x.foo=fooval+5
|
|
|
|
|
del x.foo
|
|
|
|
|
|
|
|
|
|
Semantics
|
|
|
|
|
|
|
|
|
|
Attribute references of all three kinds should call the method.
|
|
|
|
|
The op parameter can be "get"/"set"/"del". Of course this string
|
|
|
|
|
will be interned so the actual checks for the string will be
|
|
|
|
|
very fast.
|
|
|
|
|
|
|
|
|
|
It is disallowed to actually have an attribute named XXX in the
|
|
|
|
|
same instance as a method named __attr_XXX__.
|
|
|
|
|
|
|
|
|
|
An implementation of __attr_XXX__ takes precedence over an
|
|
|
|
|
implementation of __getattr__ based on the principle that
|
|
|
|
|
__getattr__ is supposed to be invoked only after finding an
|
|
|
|
|
appropriate attribute has failed.
|
|
|
|
|
|
|
|
|
|
An implementation of __attr_XXX__ takes precedence over an
|
|
|
|
|
implementation of __setattr__ in order to be consistent. The
|
|
|
|
|
opposite choice seems fairly feasible also, however. The same
|
|
|
|
|
goes for __del_y__.
|
|
|
|
|
|
|
|
|
|
Proposed Implementation
|
|
|
|
|
|
|
|
|
|
There is a new object type called an attribute access handler.
|
|
|
|
|
Objects of this type have the following attributes:
|
|
|
|
|
|
|
|
|
|
name (e.g. XXX, not __attr__XXX__
|
|
|
|
|
method (pointer to a method object
|
|
|
|
|
|
2000-07-21 17:59:29 -04:00
|
|
|
|
In PyClass_New, methods of the appropriate form will be detected and
|
|
|
|
|
converted into objects (just like unbound method objects). These are
|
|
|
|
|
stored in the class __dict__ under the name XXX. The original method
|
|
|
|
|
is stored as an unbound method under its original name.
|
|
|
|
|
|
|
|
|
|
If there are any attribute access handlers in an instance at all,
|
|
|
|
|
a flag is set. Let's call it "I_have_computed_attributes" for
|
|
|
|
|
now. Derived classes inherit the flag from base classes. Instances
|
|
|
|
|
inherit the flag from classes.
|
2000-07-21 17:14:26 -04:00
|
|
|
|
|
|
|
|
|
A get proceeds as usual until just before the object is returned.
|
|
|
|
|
In addition to the current check whether the returned object is a
|
|
|
|
|
method it would also check whether a returned object is an access
|
|
|
|
|
handler. If so, it would invoke the getter method and return
|
|
|
|
|
the value. To remove an attribute access handler you could directly
|
|
|
|
|
fiddle with the dictionary.
|
|
|
|
|
|
|
|
|
|
A set proceeds by checking the "I_have_computed_attributes" flag. If
|
|
|
|
|
it is not set, everything proceeds as it does today. If it is set
|
|
|
|
|
then we must do a dictionary get on the requested object name. If it
|
|
|
|
|
returns an attribute access handler then we call the setter function
|
|
|
|
|
with the value. If it returns any other object then we discard the
|
|
|
|
|
result and continue as we do today. Note that having an attribute
|
|
|
|
|
access handler will mildly affect attribute "setting" performance for
|
|
|
|
|
all sets on a particular instance, but no more so than today, using
|
|
|
|
|
__setattr__. Gets are more efficient than they are today with
|
|
|
|
|
__getattr__.
|
|
|
|
|
|
|
|
|
|
The I_have_computed_attributes flag is intended to eliminate the
|
|
|
|
|
performance degradation of an extra "get" per "set" for objects not
|
|
|
|
|
using this feature. Checking this flag should have miniscule
|
|
|
|
|
performance implications for all objects.
|
|
|
|
|
|
|
|
|
|
The implementation of delete is analogous to the implementation
|
|
|
|
|
of set.
|
|
|
|
|
|
|
|
|
|
Caveats
|
|
|
|
|
|
|
|
|
|
1. You might note that I have not proposed any logic to keep
|
|
|
|
|
the I_have_computed_attributes flag up to date as attributes
|
|
|
|
|
are added and removed from the instance's dictionary. This is
|
|
|
|
|
consistent with current Python. If you add a __setattr__ method
|
|
|
|
|
to an object after it is in use, that method will not behave as
|
|
|
|
|
it would if it were available at "compile" time. The dynamism is
|
|
|
|
|
arguably not worth the extra implementation effort. This snippet
|
|
|
|
|
demonstrates the current behavior:
|
|
|
|
|
|
|
|
|
|
>>> def prn(*args):print args
|
|
|
|
|
>>> class a:
|
|
|
|
|
... __setattr__=prn
|
|
|
|
|
>>> a().foo=5
|
|
|
|
|
(<__main__.a instance at 882890>, 'foo', 5)
|
|
|
|
|
|
|
|
|
|
>>> class b: pass
|
|
|
|
|
>>> bi=b()
|
|
|
|
|
>>> bi.__setattr__=prn
|
|
|
|
|
>>> b.foo=5
|
|
|
|
|
|
|
|
|
|
2. Assignment to __dict__["XXX"] can overwrite the attribute
|
|
|
|
|
access handler for __attr_XXX__. Typically the access handlers will
|
|
|
|
|
store information away in private __XXX variables
|
|
|
|
|
|
|
|
|
|
3. An attribute access handler that attempts to call setattr or getattr
|
|
|
|
|
on the object itself can cause an infinite loop (as with __getattr__)
|
|
|
|
|
Once again, the solution is to use a special (typically private)
|
|
|
|
|
variable such as __XXX.
|
2000-07-21 16:05:07 -04:00
|
|
|
|
|
|
|
|
|
Local Variables:
|
|
|
|
|
mode: indented-text
|
|
|
|
|
indent-tabs-mode: nil
|
|
|
|
|
End:
|