Python属性描述符设计:为什么要复制而不是变异?

nes*_*dis 28 python inheritance properties mutators

我在看内部Python如何实现属性描述符.根据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__)
Run Code Online (Sandbox Code Playgroud)

我的问题是:为什么实现的最后三种方法不是如下:

    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
Run Code Online (Sandbox Code Playgroud)

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

MSe*_*ert 12

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

然而,据报道,2007年Python bug追踪器上的问题(1620):

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

补丁是修复的第一稿.我要编写单元测试来验证补丁.__doc__如果doc字符串最初来自getter,它会复制属性并作为奖励 从getter获取字符串.

不幸的是链接不会去任何地方(我真的不知道为什么它被称为"永久链接"......).它被归类为bug并更改为当前表单(请参阅此补丁或相应的Github提交(但它是几个补丁的组合)).如果您不想关注该链接,则更改为:

 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);
 }
Run Code Online (Sandbox Code Playgroud)

和类似的setterdeleter.如果您不了解C,重要的是:

((propertyobject *)self)->prop_get = getter;
Run Code Online (Sandbox Code Playgroud)

return self;
Run Code Online (Sandbox Code Playgroud)

其余的主要是"Python C API样板".但是这两行等同于你的:

self.fget = fget
return self
Run Code Online (Sandbox Code Playgroud)

它被改为:

return property_copy(self, getter, NULL, NULL, NULL);
Run Code Online (Sandbox Code Playgroud)

基本上:

return type(self)(fget, self.fset, self.fdel, self.__doc__)
Run Code Online (Sandbox Code Playgroud)

为什么会改变?

由于链接已关闭,我不知道确切的原因,但我可以根据该提交中添加的测试用例进行推测:

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")
Run Code Online (Sandbox Code Playgroud)

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

>>> b = BaseClass()
>>> b.spam
5
Run Code Online (Sandbox Code Playgroud)

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

>>> b = BaseClass()
>>> b.spam
---------------------------------------------------------------------------
PropertyGet                               Traceback (most recent call last)
PropertyGet: 5
Run Code Online (Sandbox Code Playgroud)

发生这种情况是因为BaseClass.spam.getter(用于SubClass)实际修改并返回BaseClass.spam属性!

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

另一个原因 (?)

请注意,还有一个原因,有点傻但实际上值得一提(在我看来):

让我们回顾一下:装饰器只是一个赋值的语法糖,所以:

@decorator
def decoratee():
    pass
Run Code Online (Sandbox Code Playgroud)

相当于:

def func():
    pass

decoratee = decorator(func)
del func
Run Code Online (Sandbox Code Playgroud)

这里重要的一点是装饰器结果被赋予装饰函数名称.因此,虽然您通常对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
Run Code Online (Sandbox Code Playgroud)

在这个例子中,你使用的描述符,a再创建一个描述符b其行为类似a,只是它有一个setter.

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

摘要

  • CPython实际上使用了"修改和返回self"方法getter,setter并且deleter曾经使用过.
  • 由于错误报告,它已被更改.
  • 当与覆盖父类属性的子类一起使用时,它表现为"buggy".
  • 更一般地说:装饰器不能影响它们将被绑定的名称,因此假设它return self在装饰器中始终有效可能是有问题的(对于通用装饰器).

  • +460 用于挖掘历史和那些测试用例。 (2认同)

cxw*_*cxw 8

TL; DR - return self允许子类改变其父母的行为.请参阅以下故障的MCVE.

x在父类中创建属性时,该类具有包含x特定setter,getter和deleter 的属性.您第一次@Parent.x.getter在子类中说出或类似内容时,您正在 x成员上调用方法.如果x.getter没有复制property实例,则从类调用它将更改父级的 getter.这会阻止父类以其设计的方式运行.(感谢的Martijn皮特斯(毫无疑问)在这里.)

此外,文档要求:

属性对象具有可用作装饰器的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. 
Run Code Online (Sandbox Code Playgroud)

如果按照你的建议进行getter变异和返回self,那么孩子x将完全是父母的x,并且两者都会被修改.

但是,因为规范要求getter返回副本,所以子节点是x带有another_internal_nameas 的新副本fget,而父节点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);
Run Code Online (Sandbox Code Playgroud)

并运行:

$ 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)
Run Code Online (Sandbox Code Playgroud)


paw*_*moy 7

那么你可以使用继承属性吗?

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

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
Run Code Online (Sandbox Code Playgroud)

如果以你编写它的方式实现它,那么Base.value.setter也会设置double,这是不想要的.我们想要一个全新的设定器,而不是修改基础设置器.

编辑:正如@wim所指出的,在这种特殊情况下,它不仅会修改基本的setter,而且最终会出现递归错误.实际上,子安装程序会调用基本的一个,它将被修改为Base.value.fset在无休止的递归中调用自己.