为什么在Python中使用属性类属性?

时间:2018-09-04 09:07:43

标签: python

我正在阅读 Fluent 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>

根据我以前的经验,类属性属于类本身,并由所有实例共享。但是在这里,权重(即属性)是一个实例方法,因此实例之间的返回值是不同的。如何才能成为类别属性?不是所有实例的所有类属性都应该相同吗?

我认为我误会了一些东西,因此希望得到正确的解释。谢谢!

5 个答案:

答案 0 :(得分:3)

之所以有区别,是因为当您在类上定义@property时,该属性对象成为该类的属性。而当您针对类的实例(在__init__方法中)定义属性时,该属性仅针对该对象存在。如果这样做,可能会造成混淆:

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

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

请注意,subtotal weight都作为类的属性存在。

我认为还值得注意的是,当您定义一个类时,该类下的代码将被执行。这包括定义变量(然后成为类属性),定义函数以及其他任何东西。

>>> 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只是“ syntactic sugar”。如果与@property相同,则在函数上具有function = property(function)的含义。这同样适用于在类内部定义的函数,但是现在该函数是该类名称空间的一部分。

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

https://stackoverflow.com/a/17330273/7220776

中可以找到关于Python中property的很好的解释。

答案 1 :(得分:2)

  

根据我以前的经验,类属性属于类本身,并由所有实例共享。

是的。

  

但是这里的权重属性是一个实例方法

否,它是property对象。当您这样做时:

@decorator
def func():
    return 42

实际上是语法糖

def func():
    return 42

func = decorator(func)

IOW def语句已执行,函数对象已创建,但并没有与其名称绑定,而是传递给了decorator可调用对象,并且该名称已绑定到任何{{1} } 回到。

在这种情况下,装饰器是decorator()类本身,因此property属性是weight实例。您可以通过检查property(这将返回LineItem.weight对象本身)来自己检查。

  

,实例之间的返回值不同。

当然可以,这令人惊讶吗? property也是一个类属性(就像所有方法一样),但是它从被调用的实例返回值(将其作为LineItem.subtotal参数传递给函数)。

  

如何成为类别属性?不是所有实例的所有类属性都应该相同吗?

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

LineItem主要是使一个函数(或指定一对setter的一对函数)看起来像是普通属性的快捷方式,因此当您键入property时,实际执行的是是mylinitem.weight,其中LineItem.weight.fget(mylineitem)是用fget装饰的getter函数。其背后的机制称为"descriptor protocol",它也用于将@property变成mylineitem.subtotal()(Python函数实现描述符协议以返回“方法”对象,这些对象本身就是包装器)函数和当前实例,然后将该实例作为函数调用的第一个参数插入)。

因此,不必奇怪属性是类属性-您只需要一个LineItem.subtotal(mylineitem)实例即可“服务”该类的所有实例-而且,属性-像所有描述符FWIW一样-必须实际上是由于描述符协议仅在类属性上调用,因此可以按预期方式工作(“按实例”计算的属性没有用例,因为负责“计算”的功能会将实例作为参数)。

答案 2 :(得分:0)

您没有误会。不用担心,请继续阅读。在下一章中将变得清楚。

同一本书在第20章中解释说,由于 descriptor协议,它们可以是类属性。该文档介绍了properties are implemented as descriptors的用法。

从示例中可以看到,属性实际上是类属性(方法)。在被调用时,它们获得对该实例的引用,并对其底层__dict__进行写入/读取。

答案 3 :(得分:0)

我认为该示例是错误的, init 应该看起来像这样:

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

self .__ weight和self .__ price是属性隐藏在类中的内部属性

答案 4 :(得分:0)

我终于通过Simeon Franklin's excellent presentation理解了描述符和属性的概念,以下内容可以作为他的讲义的摘要。多亏他!

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

什么是描述符:

  • 描述符是实现至少一种名为__get __(),__set __()和__delete __()的方法的对象。

描述符可以分为两类:

  • 数据描述符同时实现__get __()和__set __()。
  • 非数据描述符仅实现__get __()。

根据python's HowTo

  

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

那么描述符协议是什么?基本上来说,这就是说,当Python解释器遇到obj.attr之类的属性访问时,它将以某种顺序搜索以解决此.attr,并且如果此attr是描述符属性,那么此描述符将按此特定顺序优先处理,并且此属性访问将根据描述符协议转换为对该描述符的方法调用,可能会遮盖同名实例属性或类属性。更具体地说,如果attr是数据描述符,那么obj.attr将被转换为该描述符的__get__方法的调用结果;如果attr不是数据描述符,而是实例属性,则该实例属性将被匹配;如果attr不在上面,并且它是一个非数据描述符,我们将获得此非数据描述符的__get__方法的调用结果。有关属性解析的完整规则,请参见here

现在让我们谈谈财产。如果您查看过Python' descriptor HowTo,则可以找到属性的纯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仅使用python的装饰器语法糖。

@property
def attr(self):
    pass

等效于:

attr = property(attr)

因此,attr不再是我在该问题中发布的实例方法,而是如修饰符所说的那样,由修饰符语法糖转换为 class属性。这是一个描述符对象属性。

  

如何成为类别属性?

好的,我们现在解决了。

然后:

  

不是所有实例的所有类属性都应该相同吗?

不!

我从Simeon Franklin's excellent presentation窃取了一个例子。

>>> 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。