Python: Inkonsistenz in der Art und Weise, wie Sie die Funktion __setattr__ definieren?

StackOverflow https://stackoverflow.com/questions/6305267

  •  26-10-2019
  •  | 
  •  

Frage

Betrachten Sie diesen Code:

class Foo1(dict):
    def __getattr__(self, key): return self[key]
    def __setattr__(self, key, value): self[key] = value

class Foo2(dict):
    __getattr__ = dict.__getitem__
    __setattr__ = dict.__setitem__

o1 = Foo1()
o1.x = 42
print(o1, o1.x)

o2 = Foo2()
o2.x = 42
print(o2, o2.x)

Ich würde die gleiche Ausgabe erwarten. Mit CPython 2.5, 2.6 (ähnlich in 3.2) bekomme ich jedoch:

({'x': 42}, 42)
({}, 42)

Mit PYPY 1.5.0 erhalte ich die erwartete Ausgabe:

({'x': 42}, 42)
({'x': 42}, 42)

Welches ist die "richtige" Ausgabe? (Oder wie sollte die Ausgabe gemäß der Python -Dokumentation sein?)


Hier ist der Fehlerbericht für CPython.

War es hilfreich?

Lösung

Ich vermute, dass es mit einer Suchoptimierung zu tun hat. Aus dem Quellcode:

 /* speed hack: we could use lookup_maybe, but that would resolve the
       method fully for each attribute lookup for classes with
       __getattr__, even when the attribute is present. So we use
       _PyType_Lookup and create the method only when needed, with
       call_attribute. */
    getattr = _PyType_Lookup(tp, getattr_str);
    if (getattr == NULL) {
        /* No __getattr__ hook: use a simpler dispatcher */
        tp->tp_getattro = slot_tp_getattro;
        return slot_tp_getattro(self, name);
    }

Der schnelle Pfad schaut es nicht nach dem Klassenwörterbuch nach.

Der beste Weg, um die gewünschte Funktionalität zu erhalten, besteht darin, eine Überschreibungsmethode in die Klasse zu platzieren.

class AttrDict(dict):
    """A dictionary with attribute-style access. It maps attribute access to
    the real dictionary.  """
    def __init__(self, *args, **kwargs):
        dict.__init__(self, *args, **kwargs)

    def __repr__(self):
        return "%s(%s)" % (self.__class__.__name__, dict.__repr__(self))

    def __setitem__(self, key, value):
        return super(AttrDict, self).__setitem__(key, value)

    def __getitem__(self, name):
        return super(AttrDict, self).__getitem__(name)

    def __delitem__(self, name):
        return super(AttrDict, self).__delitem__(name)

    __getattr__ = __getitem__
    __setattr__ = __setitem__

     def copy(self):
        return AttrDict(self)

Was ich wie erwartet Werke gefunden habe.

Andere Tipps

Es ist ein bekannter (und vielleicht nicht so gut) dokumentierter Unterschied. Pypy unterscheidet nicht zwischen Funktionen und integrierten Funktionen. In CPython -Funktionen werden in der Klasse (die haben __Get__) als ungebundene Methoden geplant, während gebaute Funktionen nicht (sie sind unterschiedlich).

Unter PYPY sind gebaute Funktionen jedoch genau die gleichen wie Python-Funktionen, sodass der Dolmetscher sie nicht unterscheiden kann und sie als Funktionen auf Python-Ebene behandelt. Ich denke, dies wurde als Implementierungsdetails definiert, obwohl es einige Diskussionen über Python-Dev darüber gab, diesen besonderen Unterschied zu beseitigen.

Prost,
fidjal

Beachte das Folgende:

>>> dict.__getitem__ # it's a 'method'
<method '__getitem__' of 'dict' objects> 
>>> dict.__setitem__ # it's a 'slot wrapper'
<slot wrapper '__setitem__' of 'dict' objects> 

>>> id(dict.__dict__['__getitem__']) == id(dict.__getitem__) # no bounding here
True
>>> id(dict.__dict__['__setitem__']) == id(dict.__setitem__) # or here either
True

>>> d = {}
>>> dict.__setitem__(d, 1, 2) # can be called directly (since not bound)
>>> dict.__getitem__(d, 1)    # same with this
2

Jetzt können wir sie einfach einwickeln (und __getattr__ wird auch ohne das funktionieren):

class Foo1(dict):
    def __getattr__(self, key): return self[key]
    def __setattr__(self, key, value): self[key] = value

class Foo2(dict):
    """
    It seems, 'slot wrappers' are not bound when present in the __dict__ 
    of a class and retrieved from it via instance (or class either).
    But 'methods' are, hence simple assignment works with __setitem__ 
    in your original example.
    """
    __setattr__ = lambda *args: dict.__setitem__(*args)
    __getattr__ = lambda *args: dict.__getitem__(*args) # for uniformity, or 
    #__getattr__ = dict.__getitem__                     # this way, i.e. directly


o1 = Foo1()
o1.x = 42
print(o1, o1.x)

o2 = Foo2()
o2.x = 42
print(o2, o2.x)

Was gibt:

>>>
({'x': 42}, 42)
({'x': 42}, 42)

Der Mechanismus hinter dem fraglichen Verhalten ist (wahrscheinlich bin ich kein Experte) außerhalb der "sauberen" Untergruppe von Python (wie in gründlichen Büchern wie "Lernen Python" oder "Python auf den Punkt" und "Locker" in Python dokumentiert. org) und bezieht sich auf den Teil der Sprache, der durch die Implementierung "as-is" dokumentiert wird (und unterliegt (eher häufige Änderungen).

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top