【问题标题】:Python property descriptor design: why copy rather than mutate?Python 属性描述符设计:为什么要复制而不是变异?
【发布时间】:2018-08-11 10:01:25
【问题描述】:

我正在研究 Python 如何在内部实现 property descriptor。根据文档,property() 是根据描述符协议实现的,为方便起见,在此复制:

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

我的问题是:为什么最后三个方法没有按如下方式实现:

    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

是否有理由返回新的属性实例,在内部指向基本相同的 get 和 set 函数?

【问题讨论】:

    标签: python inheritance properties mutators


    【解决方案1】:

    所以你可以使用继承的属性?

    只是试图通过举例来回答:

    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
    

    如果它是按照您编写的方式实现的,那么Base.value.setter 也会设置双精度值,这是不想要的。我们想要一个全新的二传手,而不是修改基础二传手。

    编辑:正如@wim 所指出的,在这种特殊情况下,它不仅会修改基本设置器,而且还会导致递归错误。实际上,子设置器会调用基本设置器,它会被修改为使用Base.value.fset 在无休止的递归中调用自身。

    【讨论】:

    • 关闭。我认为,子设置器会破坏递归堆栈。
    【解决方案2】:

    TL;DR - return self 允许子类改变其父类的行为。请参阅下面的失败的 MCVE。

    当您在父类中创建属性x 时,该类有一个属性x,带有特定的setter、getter 和deleter。第一次在子类中说@Parent.x.getter 或类似名称时,您正在调用 x 成员的方法。如果x.getter 没有复制property 实例,从child 类调用它会改变parent 的 getter。这将阻止父类按照设计的方式运行。 (感谢 Martijn Pieters(毫不奇怪)here。)

    此外,docs 需要它:

    属性对象具有 getter、setter 和 deleter 方法,可用作创建属性副本的装饰器...

    一个例子,展示内部结构:

    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. 
    

    如果getter 发生突变并按照您的建议返回self,则孩子的x 将与父母的x 完全相同,并且两者都将被修改。

    但是,由于规范要求getter 返回一个副本,所以子级的x 是一个新的副本,another_internal_namefget,而父级的x 保持不变。

    MCVE

    有点长,但显示了 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);
    

    然后运行:

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

    【讨论】:

      【解决方案3】:

      让我们从一点历史开始,因为最初的实现已经等同于您的替代方案(等同于因为property 是在 CPython 中用 C 实现的,所以 getter 等是用 C 而不是“普通 Python”编写的)。

      然而,早在 2007 年它就被报道为 issue (1620) on the Python bug tracker

      正如 Duncan Booth 在 http://permalink.gmane.org/gmane.comp.python.general/551183新 @spam.getter 语法修改了适当的属性,但它应该创建 一个新的。

      补丁是修复的初稿。我必须编写单元测试 验证补丁。它复制了财产,并作为奖励抓住了 __doc__ 来自 getter 的字符串,如果文档字符串最初来自 吸气剂也是如此。

      不幸的是,链接没有去任何地方(我真的不知道为什么它被称为“永久链接”......)。它被归类为错误并更改为当前形式(请参阅this patch 或相应的Github commit (but it's a combination of several patches))。如果您不想点击链接,更改是:

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

      setterdeleter 也类似。如果您不知道 C,重要的几行是:

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

      return self;
      

      其余的主要是“Python C API 样板”。然而,这两行相当于你的:

      self.fget = fget
      return self
      

      又改成了:

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

      本质上是这样的:

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

      为什么要改?

      由于链接失效我不知道具体原因,但是我可以根据添加的test-cases in that 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")
      

      这与其他答案已经提供的示例类似。问题是您希望能够在不影响父类的情况下更改子类中的行为:

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

      但是对于您的财产,它会导致:

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

      这是因为BaseClass.spam.getter(在SubClass 中使用)实际上修改并返回了BaseClass.spam 属性!

      所以是的,它已经被改变了(很可能),因为它允许修改子类中属性的行为而不改变父类的行为。

      另一个原因(?)

      请注意,还有一个额外的原因,这有点愚蠢但实际上值得一提(在我看来):

      让我们简要回顾一下:装饰器只是分配的语法糖,所以:

      @decorator
      def decoratee():
          pass
      

      相当于:

      def func():
          pass
      
      decoratee = decorator(func)
      del func
      

      这里的重点是装饰器的结果被分配给被装饰函数的名称。因此,虽然您通常对 getter/setter/deleter 使用相同的“函数名称” - 但您不必这样做!

      例如:

      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
      

      在本例中,您使用a 的描述符为b 创建另一个描述符,其行为类似于a,只是它得到了setter

      这是一个相当奇怪的例子,可能不经常(或根本不)使用。但即使它很奇怪并且(对我来说)不是很好的风格 - 它应该说明仅仅因为你使用property_name.setter(或getter/deleter)它必须绑定到property_name。它可以绑定到任何名称!而且我不希望它传播回原始属性(尽管我不确定我会在这里期待什么)。

      总结

      • CPython 实际上在gettersetterdeleter 中使用了一次“修改并返回self”的方法。
      • 已因错误报告而更改。
      • 与覆盖父类属性的子类一起使用时,它的行为“有问题”。
      • 更一般地说:装饰器无法影响它们将绑定的名称,因此在装饰器中它始终对 return self 有效的假设可能是有问题的(对于通用装饰器)。

      【讨论】:

      • +460 用于挖掘历史和那些测试用例。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-10-02
      • 2015-11-18
      • 1970-01-01
      • 1970-01-01
      • 2018-01-20
      • 1970-01-01
      相关资源
      最近更新 更多