inherit - python property example




Design del descrittore di proprietà Python: perché copiare piuttosto che mutare? (2)

Iniziamo con un po 'di storia, perché l'implementazione originale era equivalente alla tua alternativa (equivalente perché la property è implementata in C in CPython, quindi il getter , ecc. Sono scritti in C non "plain Python").

Tuttavia è stato segnalato come problema (1620) sul bug tracker di Python nel 2007:

Come riportato da Duncan Booth su http://permalink.gmane.org/gmane.comp.python.general/551183 la nuova sintassi @ spam.getter modifica la proprietà sul posto ma dovrebbe crearne una nuova.

La patch è la prima bozza di una correzione. Devo scrivere test unitari per verificare la patch. Copia la proprietà e, come bonus, afferra la stringa __doc__ dal getter se la stringa doc proviene inizialmente dal getter.

Sfortunatamente il link non va da nessuna parte (non so davvero perché si chiami "permalink" ...). È stato classificato come bug e modificato nel modulo corrente (vedere questa patch o il commit Github corrispondente (ma è una combinazione di diverse patch) ). Nel caso in cui tu non voglia seguire il link il cambiamento è stato:

 PyObject *
 property_getter(PyObject *self, PyObject *getter)
 {
-   Py_XDECREF(((propertyobject *)self)->prop_get);
-   if (getter == Py_None)
-       getter = NULL;
-   Py_XINCREF(getter);
-   ((propertyobject *)self)->prop_get = getter;
-   Py_INCREF(self);
-   return self;
+   return property_copy(self, getter, NULL, NULL, NULL);
 }

E simile per setter e deleter . Se non conosci C le linee importanti sono:

((propertyobject *)self)->prop_get = getter;

e

return self;

il resto è principalmente "Python C API boilerplate". Tuttavia queste due righe sono equivalenti al tuo:

self.fget = fget
return self

Ed è stato modificato in:

return property_copy(self, getter, NULL, NULL, NULL);

che essenzialmente fa:

return type(self)(fget, self.fset, self.fdel, self.__doc__)

Perché è stato cambiato?

Dato che il collegamento non funziona, non conosco il motivo esatto, tuttavia posso speculare sulla base dei casi di test aggiunti in quel commit :

import unittest

class PropertyBase(Exception):
    pass

class PropertyGet(PropertyBase):
    pass

class PropertySet(PropertyBase):
    pass

class PropertyDel(PropertyBase):
    pass

class BaseClass(object):
    def __init__(self):
        self._spam = 5

    @property
    def spam(self):
        """BaseClass.getter"""
        return self._spam

    @spam.setter
    def spam(self, value):
        self._spam = value

    @spam.deleter
    def spam(self):
        del self._spam

class SubClass(BaseClass):

    @BaseClass.spam.getter
    def spam(self):
        """SubClass.getter"""
        raise PropertyGet(self._spam)

    @spam.setter
    def spam(self, value):
        raise PropertySet(self._spam)

    @spam.deleter
    def spam(self):
        raise PropertyDel(self._spam)

class PropertyTests(unittest.TestCase):
    def test_property_decorator_baseclass(self):
        # see #1620
        base = BaseClass()
        self.assertEqual(base.spam, 5)
        self.assertEqual(base._spam, 5)
        base.spam = 10
        self.assertEqual(base.spam, 10)
        self.assertEqual(base._spam, 10)
        delattr(base, "spam")
        self.assert_(not hasattr(base, "spam"))
        self.assert_(not hasattr(base, "_spam"))
        base.spam = 20
        self.assertEqual(base.spam, 20)
        self.assertEqual(base._spam, 20)
        self.assertEqual(base.__class__.spam.__doc__, "BaseClass.getter")

    def test_property_decorator_subclass(self):
        # see #1620
        sub = SubClass()
        self.assertRaises(PropertyGet, getattr, sub, "spam")
        self.assertRaises(PropertySet, setattr, sub, "spam", None)
        self.assertRaises(PropertyDel, delattr, sub, "spam")
        self.assertEqual(sub.__class__.spam.__doc__, "SubClass.getter")

È simile agli esempi delle altre risposte già fornite. Il problema è che vuoi essere in grado di cambiare il comportamento in una sottoclasse senza influenzare la classe genitore:

>>> b = BaseClass()
>>> b.spam
5

Tuttavia con la tua proprietà si tradurrebbe in questo:

>>> b = BaseClass()
>>> b.spam
---------------------------------------------------------------------------
PropertyGet                               Traceback (most recent call last)
PropertyGet: 5

Ciò accade perché BaseClass.spam.getter (che è utilizzato in SubClass ) in realtà modifica e restituisce la proprietà BaseClass.spam !

Quindi sì, è stato modificato (molto probabilmente) perché consente di modificare il comportamento di una proprietà in una sottoclasse senza modificare il comportamento sulla classe genitore.

Un'altra ragione (?)

Nota che c'è un motivo in più, che è un po 'sciocco, ma in realtà vale la pena menzionarlo (secondo me):

Ricapitoliamo brevemente: un decoratore è solo zucchero sintattico per un incarico, quindi:

@decorator
def decoratee():
    pass

è equivalente a:

def func():
    pass

decoratee = decorator(func)
del func

Il punto importante qui è che il risultato del decoratore è assegnato al nome della funzione decorata . Quindi, mentre generalmente usi lo stesso "nome funzione" per getter / setter / deleter - non devi!

Per esempio:

class Fun(object):
    @property
    def a(self):
        return self._a

    @a.setter
    def b(self, value):
        self._a = value

>>> o = Fun()
>>> o.b = 100
>>> o.a
100
>>> o.b
100
>>> o.a = 100
AttributeError: can't set attribute

In questo esempio si usa il descrittore di a per creare un altro descrittore per b che si comporta come a tranne che ha un setter .

È un esempio piuttosto strano e probabilmente non viene usato molto spesso (o del tutto). Ma anche se è uno stile piuttosto strano e (per me) non molto bello - dovrebbe illustrarlo solo perché si usa property_name.setter (o getter / deleter ) che deve essere associato a property_name . Potrebbe essere associato a qualsiasi nome! E non mi aspetto che si propaghi di nuovo alla proprietà originale (anche se non sono sicuro di cosa mi aspetterei qui).

Sommario

  • CPython ha effettivamente usato l'approccio "modifica e ritorno di self " in getter , setter e deleter una volta.
  • Era stato cambiato a causa di un bug report.
  • Si comportava in modo "buggy" quando usato con una sottoclasse che sovrascriveva una proprietà della classe genitore.
  • Più in generale: i decoratori non possono influenzare a quale nome saranno associati, quindi l'ipotesi che sia sempre valido return self in un decoratore potrebbe essere discutibile (per un decoratore di carattere generale).

Stavo osservando come Python implementa internamente il descrittore di proprietà . Secondo la property() docs property() è implementato in termini di protocollo descrittore, riproducendolo qui per comodità:

class Property(object):
    "Emulate PyProperty_Type() in Objects/descrobject.c"

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)

La mia domanda è: perché gli ultimi tre metodi non sono implementati come segue:

    def getter(self, fget):
        self.fget = fget
        return self

    def setter(self, fset):
        self.fset = fset
        return self

    def deleter(self, fdel):
        self.fdel= fdel
        return self

C'è un motivo per ritirare nuove istanze di proprietà, puntando internamente essenzialmente alle stesse funzioni get e set?


Quindi puoi usare le proprietà con l'ereditarietà?

Solo un tentativo di risposta dando un esempio:

class Base(object):
    def __init__(self):
        self._value = 0

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, val):
        self._value = val


class Child(Base):
    def __init__(self):
        super().__init__()
        self._double = 0

    @Base.value.setter
    def value(self, val):
        Base.value.fset(self, val)
        self._double = val * 2

Se è stato implementato nel modo in cui lo scrivi, il Base.value.setter anche il double, che non è voluto. Vogliamo un setter nuovo di zecca, non modificare quello di base.

EDIT: come sottolineato da @wim, in questo caso particolare, non solo avrebbe modificato il setter di base, ma avremmo anche avuto un errore di ricorsione. In effetti, il setter bambino chiamerebbe quello base, che sarebbe stato modificato per chiamarsi con Base.value.fset in una ricorsione infinita.





mutators