Python property descriptor design: why copy rather than mutate?

后端 未结 3 905
离开以前
离开以前 2021-02-03 22:28

I was looking at how Python implements the property descriptor internally. According to the docs property() is implemented in terms of the descriptor protocol, repr

3条回答
  •  有刺的猬
    2021-02-03 23:14

    TL;DR - return self permits child classes to change the behaviour of their parents. See MCVE of the failure below.

    When you create property x in a parent class, that class has an attribute x with a particular setter, getter, and deleter. The first time you say @Parent.x.getter or the like in a child class, you are invoking a method on the parent's x member. If x.getter did not copy the property instance, calling it from the child class would change the parent's getter. That would prevent the parent class from operating the way it was designed to. (Thanks to Martijn Pieters (no surprise) here.)

    And besides, the docs require it:

    A property object has getter, setter, and deleter methods usable as decorators that create a copy of the property ...

    An example, showing the internals:

    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. 
    

    If getter mutated and returned self as you suggested, the child's x would be exactly the parent's x, and both would have been modified.

    However, because the spec requires getter to return a copy, the child's x is a new copy with another_internal_name as fget, and the parent's x is untouched.

    MCVE

    It's a bit long, but shows the behaviour on 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);
    

    And the run:

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

提交回复
热议问题