PEP 231 – __findattr__()
- Autor:
- Barry Warsaw <barry at python.org>
- Status:
- Abgelehnt
- Typ:
- Standards Track
- Erstellt:
- 30-Nov-2000
- Python-Version:
- 2.1
- Post-History:
Einleitung
Dieses PEP beschreibt eine Erweiterung der Mechanik für die Suche und Modifikation von Instanzattributen, die reine Python-Implementierungen vieler interessanter Programmiermodelle ermöglicht. Dieses PEP verfolgt den Status und die Zuständigkeit für diese Funktion. Es enthält eine Beschreibung der Funktion und skizziert die notwendigen Änderungen zur Unterstützung der Funktion. Dieses PEP fasst Diskussionen in Mailinglisten zusammen und liefert, wo angebracht, URLs für weitere Informationen. Die CVS-Revisionshistorie dieser Datei enthält die definitive historische Aufzeichnung.
Hintergrund
Die Semantik für Python-Instanzen erlaubt es dem Programmierer, einige Aspekte der Attributsuche und Attributmodifikation über die speziellen Methoden __getattr__() und __setattr__() anzupassen [1].
Aufgrund bestimmter Einschränkungen dieser Methoden sind jedoch nützliche Programmiertechniken, die nicht allein in Python geschrieben werden können, z. B. strenge Java Bean-ähnliche [2] Schnittstellen und Zope-Stil-Akquisitionen [3]. Im letzteren Fall löst Zope dies durch die Einbeziehung einer C-Erweiterung namens ExtensionClass [5], die die Standard-Klassen-Semantik modifiziert und einen Metaklassen-Hook im Klassenmodell von Python verwendet, der alternativ als „Don Beaudry Hook“ oder „Don Beaudry Hack“ bezeichnet wird [6].
Obwohl Zopes Ansatz funktioniert, hat er mehrere Nachteile. Erstens erfordert er eine C-Erweiterung. Zweitens verwendet er eine sehr obskure, aber groß angelegte Lücke in der Python-Mechanik. Drittens kann es für andere Programmierer schwierig sein, ihn zu verwenden und zu verstehen (die Metaklasse hat bekannte Gehirn-explodierende Eigenschaften). Und viertens, da ExtensionClass-Instanzen keine „echten“ Python-Instanzen sind, funktionieren einige Aspekte des Python-Laufzeitsystems nicht mit ExtensionClass-Instanzen.
Vorschläge zur Behebung dieses Problems wurden oft unter dem Stichwort „Klassen-/Typen-Dichotomie“ zusammengefasst; d. h. die Eliminierung des Unterschieds zwischen integrierten Typen und Klassen [7]. Obwohl dies selbst ein lobenswertes Ziel ist, ist die Reparatur dieses Risses nicht notwendig, um die oben beschriebenen Programmierkonstrukte zu erreichen. Dieser Vorschlag bietet eine 80%-Lösung mit minimalen Änderungen an den Klassen- und Instanzobjekten von Python. Er befasst sich nicht mit der Typ-/Klassen-Dichotomie.
Vorschlag
Dieser Vorschlag fügt eine neue spezielle Methode namens __findattr__() mit folgender Semantik hinzu:
- Wenn sie in einer Klasse definiert ist, wird sie bei allen Instanzattributsuchen anstelle von
__getattr__()und__setattr__()aufgerufen. __findattr__()wird nie rekursiv aufgerufen. Das heißt, wenn die__findattr__()einer bestimmten Instanz auf dem Aufrufstapel liegt, werden weitere Attributzugriffe für diese Instanz die Standardmethoden__getattr__()und__setattr__()verwenden.__findattr__()wird sowohl für den Attributzugriff („Getting“) als auch für die Attributmodifikation („Setting“) aufgerufen. Er wird nicht für das Löschen von Attributen aufgerufen.- Beim Aufruf zum Abrufen wird ein einzelnes Argument übergeben (abgesehen von „self“): der Name des zugegriffenen Attributs.
- Beim Aufruf zum Setzen wird er mit einem dritten Argument aufgerufen, dem Wert, auf den das Attribut gesetzt werden soll.
__findattr__()-Methoden haben die gleiche Caching-Semantik wie__getattr__()und__setattr__(); d. h. wenn sie zum Zeitpunkt der Klassendefinition in der Klasse vorhanden sind, werden sie verwendet, aber wenn sie später zu einer Klasse hinzugefügt werden, werden sie nicht verwendet.
Wesentliche Unterschiede zum bestehenden Protokoll
__findattr__()s Semantik unterscheidet sich in wichtigen Punkten von der bestehenden Protokollierung.
Erstens wird __getattr__() nie aufgerufen, wenn das Attribut im __dict__ der Instanz gefunden wird. Dies geschieht aus Effizienzgründen und weil andernfalls __setattr__() keine Möglichkeit hätte, auf die Instanzattribute zuzugreifen.
Zweitens kann __setattr__() keine „normale“ Syntax zum Setzen von Instanzattributen verwenden, z. B. „self.name = foo“, da dies rekursive Aufrufe von __setattr__() verursachen würde.
__findattr__() wird immer aufgerufen, unabhängig davon, ob das Attribut im __dict__ vorhanden ist oder nicht, und ein Flag im Instanzobjekt verhindert rekursive Aufrufe von __findattr__(). Dies gibt der Klasse die Möglichkeit, für jeden Attributzugriff eine Aktion durchzuführen. Und da es sowohl für Gets als auch für Sets aufgerufen wird, ist es einfach, eine ähnliche Richtlinie für alle Attributzugriffe zu schreiben. Darüber hinaus ist die Effizienz kein Problem, da sie nur bezahlt wird, wenn der erweiterte Mechanismus verwendet wird.
Beispiele
Ein Programmierstil, den dieser Vorschlag ermöglicht, ist eine Java Bean-ähnliche Schnittstelle zu Objekten, bei der ungekennzeichnete Attributzugriffe und Modifikationen transparent in eine funktionale Schnittstelle abgebildet werden. Z. B.
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
Ein zweites, aufwändigeres Beispiel ist die Implementierung von impliziter und expliziter Akquisition in reinem 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++-ähnliche Zugriffskontrolle kann ebenfalls erreicht werden, wenn auch weniger sauber aufgrund der Schwierigkeit, aus dem Laufzeit-Aufrufstapel zu ermitteln, welche Methode aufgerufen wird.
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'
Hier ist eine Implementierung des Attribut-Hooks, der in PEP 213 beschrieben wird (außer dass das Einhaken bei Attributlöschungen von der aktuellen Referenzimplementierung nicht unterstützt wird).
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
Referenzimplementierung
Die Referenzimplementierung, als Patch für den Python-Kern, finden Sie unter dieser URL
http://sourceforge.net/patch/?func=detailpatch&patch_id=102613&group_id=5470
Referenzen
Ablehnung
Es gibt ernsthafte Probleme mit der Rekursionsschutzfunktion. Wie hier beschrieben ist sie nicht Thread-sicher, und eine Thread-sichere Lösung hat andere Probleme. Im Allgemeinen ist es nicht klar, wie hilfreich die Rekursionsschutzfunktion ist; sie erschwert das Schreiben von Code, der sowohl innerhalb als auch außerhalb von __findattr__ aufrufbar sein muss. Aber ohne den Rekursionsschutz ist es schwierig, __findattr__ überhaupt zu implementieren (da __findattr__ sich rekursiv für jedes Attribut, auf das es zuzugreifen versucht, selbst aufrufen würde). Hier scheint es keine gute Lösung zu geben.
Es ist auch zweifelhaft, wie nützlich es ist, __findattr__ sowohl für das Abrufen als auch für das Setzen von Attributen zu unterstützen – __setattr__ wird bereits in allen Fällen aufgerufen.
Die Beispiele können alle mit __getattr__ implementiert werden, wenn man darauf achtet, Instanzvariablen nicht unter ihren eigenen Namen zu speichern.
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0231.rst
Zuletzt geändert: 2025-02-01 08:55:40 GMT