27 votes

Conception de descripteurs de propriétés Python: pourquoi copier plutôt que muter?

J'étais en train de regarder comment Python met en œuvre le descripteur de propriété à l'interne. Selon les docs property() est mis en œuvre en termes de descripteur de protocole, de la reproduire ici, pour plus de commodité:

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__)

Ma question est: pourquoi ne sont pas les trois dernières méthodes de mise en œuvre comme suit:

    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

Est-il une raison pour returing de nouvelles instances de la propriété, à l'interne pointant vers fondamentalement les mêmes fonctions get et set?

13voto

MSeifert Points 6307

Commençons par un peu d'histoire, parce que l'origine de la mise en œuvre a été équivalent à celui de votre alternative (équivalent, car property est implémenté en C dans Disponible de sorte que le getter, etc. sont écrits en C pas clair Python").

Cependant il a été rapporté que la question (1620) sur le Python bug tracker en 2007:

Tel que rapporté par Duncan Stand à http://permalink.gmane.org/gmane.comp.python.general/551183 la nouvelle @spam.getter syntaxe modifie la propriété en place, mais il doit créer un nouveau.

Le patch est la première ébauche d'une solution. J'ai à écrire des tests unitaires pour vérifier le patch. Il copie la propriété et en bonus, attrape le __doc__ chaîne de caractères à partir de la lecture si la doc de la chaîne est d'abord venu de la de lecture ainsi.

Malheureusement, le lien ne doit pas aller n'importe où (je ne sais vraiment pas pourquoi on appelle ça un "permalink" ...). Il a été classé comme un bug et changé à la forme actuelle (voir ce patch ou le Github commit (mais c'est une combinaison de plusieurs patchs)). Dans le cas où vous ne voulez pas suivre le lien le changement:

 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);
 }

Et pour setter et deleter. Si vous ne savez pas C les lignes importantes sont:

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

et

return self;

le reste est principalement "Python C API standard". Cependant, ces deux lignes sont équivalentes à votre:

self.fget = fget
return self

Et il a été changé pour:

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

qui, essentiellement, n':

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

Pourquoi était-il changé?

Puisque le lien est en bas je ne sais pas la raison exacte, mais je peux faire des suppositions basées sur les cas de test dans ce 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")

C'est similaire aux exemples les autres réponses déjà fournies. Le problème, c'est que vous voulez être en mesure de modifier le comportement dans une sous-classe, sans affecter la classe parent:

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

Cependant, avec votre propriété, il en résulterait ceci:

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

Cela se produit parce que BaseClass.spam.getter (qui est utilisé en SubClass) modifie et renvoie l' BaseClass.spam de la propriété!

Donc, oui, il avait été changé (très probable), car elle permet de modifier le comportement d'une propriété dans une sous-classe sans changer le comportement de la classe parent.

Une autre raison (?)

Notez qu'il y a une raison supplémentaire, ce qui est un peu idiot, mais en fait vaut la peine de mentionner (à mon avis):

Récapitulons brièvement: Un décorateur est juste sucre syntaxique pour une mission, donc:

@decorator
def decoratee():
    pass

est équivalent à:

def func():
    pass

decoratee = decorator(func)
del func

Le point important ici est que le résultat de la décoratrice est attribué le nom de l'décoré de la fonction. Ainsi, alors que vous utilisez le plus souvent le même "nom de fonction" pour les getter/setter/deleter - vous n'avez pas à!

Par exemple:

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

Dans cet exemple, vous utilisez le descripteur de fichier pour a créer un autre descripteur pour b qui se comporte comme a , sauf qu'il a un setter.

C'est un peu bizarre d'exemple et n'est probablement pas utilisé très souvent (tous les). Mais même si c'est plutôt étrange et (pour moi) pas très bon style, doivent illustrer que juste parce que vous utilisez property_name.setter (ou getter/deleter) qu'il doit être lié à l' property_name. Il pourrait être lié à un nom! Et je ne m'attends pas à se propager à l'origine de la propriété (bien que je ne suis pas vraiment sûr de ce que je m'attends à voir ici).

Résumé

  • Disponible en fait utilisé la fonction "modifier et renvoyer self" approche en getter, setter et deleter une fois.
  • Il a été modifié à cause d'un rapport de bug.
  • Il s'est comporté "buggy" lorsqu'il est utilisé avec une sous-classe qui a remplacé une propriété de la classe parent.
  • Plus généralement: les Décorateurs ne peut pas l'influence de ce nom, ils seront liés donc l'hypothèse que c'est toujours valable pour return self dans un décorateur peut être contestable (pour un usage général, décorateur).

8voto

cxw Points 872

TL;DR - return self permet à l'enfant des classes de modifier le comportement de leurs parents. Voir MCVE de l'échec ci-dessous.

Lorsque vous créez des biens x , un parent de la classe, cette classe possède un attribut x avec un setter, fonceur, et la deleter. La première fois que vous dites @Parent.x.getter ou dans une classe enfant, vous êtes l'invocation d'une méthode sur le parent de l' x membre. Si x.getter n'ont pas de copie de l' property exemple, l'appel de l' enfant de la classe serait de changer le parent de lecture. Qui empêcherait la classe parent d'exploitation de la façon dont il a été conçu pour. (Merci à Martijn Pieters (pas de surprise) ici.)

Et d'ailleurs, les docs exigent:

Un objet de propriété a getter, setter, et deleter méthodes utilisables comme des décorateurs que de créer une copie de la propriété ...

Un exemple, pour montrer la structure interne:

class P:
    ## @property  --- inner workings shown below, marked "##"
    def x(self):
        return self.__x
    x = property(x)                             ## what @property does

    ## @x.setter
    def some_internal_name(self, x):
        self.__x = x
    x = x.setter(some_internal_name)            ## what @x.setter does

class C(P):
    ## @P.x.getter   # x is defined in parent P, so you have to specify P.x
    def another_internal_name(self):
        return 42

    # Remember, P.x is defined in the parent.  
    # If P.x.getter changes self, the parent's P.x changes.
    x = P.x.getter(another_internal_name)         ## what @P.x.getter does
    # Now an x exists in the child as well as in the parent. 

Si getter muté et est retourné self comme vous l'avez suggéré, l'enfant, x serait exactement de la société mère x, et tous deux auraient été modifiés.

Cependant, parce que la spécification exige getter de retour une copie, l'enfant, x est une copie avec another_internal_name comme fget, et le parent x est intacte.

MCVE

C'est un peu long, mais montre le comportement sur Py 2.7.14.

class OopsProperty(object):
    "Shows what happens if getter()/setter()/deleter() don't copy"

    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)

    ########## getter/setter/deleter modified as the OP suggested
    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

class OopsParent(object):   # Uses OopsProperty() instead of property()
    def __init__(self):
        self.__x = 0

    @OopsProperty
    def x(self):
        print("OopsParent.x getter")
        return self.__x

    @x.setter
    def x(self, x):
        print("OopsParent.x setter")
        self.__x = x

class OopsChild(OopsParent):
    @OopsParent.x.getter                 # changes OopsParent.x!
    def x(self):
        print("OopsChild.x getter")
        return 42;

parent = OopsParent()
print("OopsParent x is",parent.x);

child = OopsChild()
print("OopsChild x is",child.x);

class Parent(object):   # Same thing, but using property()
    def __init__(self):
        self.__x = 0

    @property
    def x(self):
        print("Parent.x getter")
        return self.__x

    @x.setter
    def x(self, x):
        print("Parent.x setter")
        self.__x = x

class Child(Parent):
    @Parent.x.getter
    def x(self):
        print("Child.x getter")
        return 42;

parent = Parent()
print("Parent x is",parent.x);

child = Child()
print("Child x is",child.x);

Et l'exécuter:

$ python foo.py
OopsChild.x getter              <-- Oops!  parent.x called the child's getter
('OopsParent x is', 42)         <-- Oops!
OopsChild.x getter
('OopsChild x is', 42)
Parent.x getter                 <-- Using property(), it's OK
('Parent x is', 0)              <-- What we expected from the parent class
Child.x getter
('Child x is', 42)

7voto

pawamoy Points 1019

Ainsi, vous pouvez utiliser les propriétés de l'héritage?

Juste une tentative de répondre en donnant un exemple:

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

Si la mise en œuvre de la façon dont vous l'écrivez, puis l' Base.value.setter y aurait également le double, qui n'est pas voulu. Nous voulons une nouvelle marque setter, de ne pas modifier la base de l'un.

EDIT: comme l'a souligné @wim, dans ce cas particulier, non seulement il serait de modifier la base de setter, mais nous aurions aussi jusqu'à la fin avec la récursivité d'erreur. En effet, l'enfant setter pourrait appeler celle de base, qui serait modifié de façon à s'appeler lui-même avec Base.value.fset dans une récursivité sans fin.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X