在Python中,为什么要将属性定义为类属性?

8
我正在阅读《流畅的Python》第19章 “属性的内部机制”,以下几句话让我感到困惑:
“属性始终是类属性,但它们实际上管理类的实例中的属性访问。”
示例代码如下:
class LineItem:

    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight  # <1>
        self.price = price

    def subtotal(self):
        return self.weight * self.price

    @property  # <2>
    def weight(self):  # <3>
        return self.__weight  # <4>

    @weight.setter  # <5>
    def weight(self, value):
        if value > 0:
            self.__weight = value  # <6>
        else:
            raise ValueError('value must be > 0')  # <7>

根据我的以往经验,类属性属于类本身并被所有实例共享。但是在这里,weight属性是一个实例方法,其返回值在不同实例之间是不同的。它怎么能成为类属性呢?难道不是所有类属性对于任何实例都应该是相同的吗?

我想我可能有些误解,所以希望得到正确的解释。谢谢!


4
请注意,方法也是在类上定义的(因此是类属性),但当在实例上调用时,该实例作为参数传递并因此可用于该方法。请详细查看描述器:https://docs.python.org/2/howto/descriptor.html 和 https://docs.python.org/3/reference/datamodel.html#descriptors。 - user2390182
5个回答

3

有一个区别需要注意,当你在类上定义一个@property时,该属性对象会成为类的一个属性。而当你在类的一个实例上定义属性(在__init__方法中),该属性仅存在于该对象中。如果你这样做可能会感到困惑:

>>> dir(LineItem)
['__class__', ..., '__weakref__', 'subtotal', 'weight']

>>> item = LineItem("an item", 3, 1.12)
>>> dir(item)
['__class__', ..., '__weakref__', 'description', 'price', 'subtotal', 'weight']

请注意您的类中有 subtotalweight 两个属性。
值得一提的是,当您定义一个类时,该类下的代码也会被执行。这包括定义变量(它们将变成类的属性)、定义函数和其他任何操作。
>>> import requests

>>> class KindOfJustANamespace:
...     text = requests.get("https://example.com").text
...     while True:
...         break
...     for x in range(2):
...         print(x)
...
0
1
>>> KindOfJustANamespace.text
'<!doctype html>\n<html>\n<head>\n    <title>Example Domain...'

@decorator 只是一种“语法糖”。意味着 @property 与函数相同,即 function = property(function)。这也适用于在类内定义的函数,但现在该函数是类命名空间的一部分。

class TestClass:
    @property
    def foo(self):
        return "foo"
    # ^ is the same as:
    def bar(self):
         return "bar"
    bar = property(bar)

这里有一个关于 Python 中 property 的好解释: https://dev59.com/fmQm5IYBdhLWcg3wyhjk#17330273


2
“从我的以往经验来看,类属性属于类本身,并由所有实例共享。”
“没错。”
“但是,这里的权重(weight)是一个实例方法。”
“不,它是一个属性对象。当你这样做时:”
@decorator
def func():
    return 42

这实际上是语法糖,用于...
def func():
    return 42

func = decorator(func)

IOW,执行了def语句,创建了函数对象,但是它没有绑定到它的名称,而是传递给了decorator可调用对象,并且名称被绑定到decorator()返回的任何内容。在这种情况下,装饰器是property类本身,因此weight属性是一个property实例。您可以通过检查LineItem.weight(将返回property对象本身)来验证这一点。

并且它返回的值在实例之间是不同的。

当然,这很明显。 LineItem.subtotal也是一个类属性(像所有方法一样),但它返回从调用它的实例中获取的值(作为self参数传递给函数)。

它如何有资格成为类属性?这难道不意味着所有类属性对于任何实例都应该相同吗?

类属性对于该类的所有实例都是相同的。是的,对于所有LineItem实例只有一个subtotal函数。

property主要是一种快捷方式,使得一个函数(或两个函数,如果你指定了一个setter)看起来像一个普通的属性,因此当您键入mylinitem.weight时,真正执行的是 LineItem.weight.fget(mylineitem),其中fget是您用@property装饰的getter函数。这背后的机制称为"描述符协议",它也用于将mylineitem.subtotal()转换为LineItem.subtotal(mylineitem)(python函数实现描述符协议以返回“方法”对象,它们本身是函数和当前实例的包装器,并将实例作为第一个参数插入到函数调用中)。
因此,属性是类属性并不奇怪 - 您只需要一个 property 实例来为类的所有实例提供服务。而且,像所有描述符一样,属性必须实际上是类属性才能按预期工作,因为描述符协议仅在类属性上调用(没有“每个实例”计算属性的用例,因为负责“计算”的函数将获得实例作为参数)。

2

通过Simeon Franklin的精彩演讲,我终于理解了描述符和属性概念,以下内容可以被视为他讲义的摘要。感谢他!

要理解属性,首先需要了解描述符,因为属性是由描述符和Python装饰器语法糖实现的。不用担心,这并不难。

什么是描述符:

  • 描述符是实现了至少一个名为__get__()、__set__()和__delete__()方法的任何对象。

描述符可分为两类:

  • 数据描述符实现了__get__()和__set__()。
  • 非数据描述符仅实现了__get__()。

根据Python官方文档所述:

描述符是具有“绑定行为”的对象属性,其属性访问已被描述符协议中的方法覆盖。

那么,什么是描述符协议?简单来说,就是当Python解释器遇到类似obj.attr的属性访问时,它将按一定顺序进行搜索以解析此.attr,并且如果此attr是描述符属性,则此描述符将在特定顺序中优先采取某些措施,并根据描述符协议将此属性访问转换为对此描述符的方法调用,可能会遮盖同名实例属性或类属性。更具体地说,如果attr是数据描述符,则obj.attr将转换为此描述符__get__方法的调用结果;如果attr不是数据描述符并且是实例属性,则将匹配此实例属性;如果attr不在以上情况中且是非数据描述符,则获取该非数据描述符__get__方法的调用结果。有关属性解析规则的完整规则,请单击此处。

现在让我们谈谈属性。如果您查看了Python的描述符文档,您可以找到纯Python版本的属性实现方法:

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

显然,property是一个数据描述符!

@property只是使用了Python的装饰器语法糖。

@property
def attr(self):
    pass

相当于:

attr = property(attr)

因此,正如我在这个问题中发布的那样,attr不再是一个实例方法,而是被装饰器语法糖转换为类属性。它是一个描述符对象属性。

它如何有资格成为类属性?

好的,现在我们解决了这个问题。
然后:

难道所有的类属性都应该对任何实例相同吗?

不!
我从Simeon Franklin的精彩演讲中借用了一个例子。
>>> class MyDescriptor(object):
...     def __get__(self, obj, type):
...         print self, obj, type
...     def __set__(self, obj, val):
...         print "Got %s" % val
...
>>> class MyClass(object):
...     x = MyDescriptor() # Attached at class definition time!
...
>>> obj = MyClass()
>>> obj.x # a function call is hiding here
<...MyDescriptor object ...> <....MyClass object ...> <class '__main__.MyClass'>
>>>
>>> MyClass.x # and here!
<...MyDescriptor object ...> None <class '__main__.MyClass'>
>>>
>>> obj.x = 4 # and here
Got 4

请注意obj.x及其输出。 其输出的第二个元素是<....MyClass object ...>,它是特定实例obj。简单地说,因为此属性访问已转换为__get__方法调用,并且此__get__方法将特定实例参数作为其方法签名descr.__get__(self, obj, type=None)所需,因此它可以根据被调用的实例返回不同的值。
注:我的英文解释可能不够清晰,因此我强烈建议您查看Simeon Franklin的笔记和Python的描述符HowTo。

0

我认为这个例子是错误的,init 应该像这样:

def __init__(self, description, weight, price):
    self.description = description
    self.__weight = weight  # <1> 
    self.__price = price

self.__weight和self.__price是通过属性封装在类中隐藏的内部属性


0

你没有误解。别担心,继续读下去就会清楚了。

同一本书在第20章中解释了由于描述符协议,它们可以成为类属性。文档解释了如何将属性实现为描述符

从例子中可以看出,属性确实是类属性(方法)。 当被调用时,它们会获得一个对实例的引用,并写入/读取其底层的__dict__


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接