理解__get__和__set__以及Python描述符

时间:2010-09-26 16:55:43

标签: python descriptor

尝试来了解Python的描述符是什么以及它们对什么有用。但是,我没有成功。我理解它们是如何工作的,但这是我的疑惑。请考虑以下代码:

class Celsius(object):
    def __init__(self, value=0.0):
        self.value = float(value)
    def __get__(self, instance, owner):
        return self.value
    def __set__(self, instance, value):
        self.value = float(value)


class Temperature(object):
    celsius = Celsius()
  1. 为什么我需要描述符类?

  2. 这里的instanceowner是什么? (在__get__)。这些参数的目的是什么?

  3. 我如何致电/使用此示例?

7 个答案:

答案 0 :(得分:126)

描述符是Python的property类型的实现方式。描述符只是实现__get____set__等,然后在其定义中添加到另一个类中(如上所述,使用Temperature类)。例如:

temp=Temperature()
temp.celsius #calls celsius.__get__

访问您将描述符分配给(上例中的celsius)的属性调用适当的描述符方法。

instance中的{p> __get__是该类的实例(如上所述,__get__将收到temp,而owner是具有描述符的类(所以这将是Temperature)。

您需要使用描述符类来封装为其提供支持的逻辑。这样,如果描述符用于缓存一些昂贵的操作(例如),它可以将值存储在自身而不是它的类上。

可以找到关于描述符的文章here

编辑:正如jchl在评论中指出的那样,如果您只是尝试Temperature.celsiusinstance将是None

答案 1 :(得分:95)

  

为什么我需要描述符类?

它让您可以更好地控制属性的工作方式。例如,如果您习惯于使用Java中的getter和setter,那么Python就是这样做的。一个优点是它向用户看起来就像一个属性(语法没有变化)。因此,您可以从普通属性开始,然后,当您需要做一些奇特的事情时,切换到描述符。

属性只是一个可变值。描述符允许您在读取或设置(或删除)值时执行任意代码。因此,您可以想象使用它将属性映射到数据库中的字段,例如 - 一种ORM。

另一种用途可能是拒绝通过在__set__中抛出异常来接受新值 - 有效地使“属性”只读。

  

这里的instanceowner是什么? (在__get__)。这些参数的目的是什么?

这是非常微妙的(我之所以在这里写一个新答案的原因 - 我发现这个问题同时想知道同样的事情并没有找到现有的答案那么棒。)

描述符是在类上定义的,但通常是从实例调用的。当它从一个实例调用时,instanceowner都会被设置(你可以从owner找出instance,这样看起来有点毫无意义。但是当从一个类中调用时,只设置了owner - 这就是它存在的原因。

只有__get__才需要它,因为它是唯一可以在类上调用的。{1}}。如果设置类值,则设置描述符本身。同样删除。这就是为什么那里不需要owner的原因。

  

我如何致电/使用此示例?

嗯,这是使用类似课程的一个很酷的技巧:

class Celsius:

    def __get__(self, instance, owner):
        return 5 * (instance.fahrenheit - 32) / 9

    def __set__(self, instance, value):
        instance.fahrenheit = 32 + 9 * value / 5


class Temperature:

    celsius = Celsius()

    def __init__(self, initial_f):
        self.fahrenheit = initial_f


t = Temperature(212)
print(t.celsius)
t.celsius = 0
print(t.fahrenheit)

(我正在使用Python 3;对于python 2,您需要确保这些分区为/ 5.0/ 9.0)。这给了:

100.0
32.0

现在还有其他的,可以说是更好的方法来在python中实现相同的效果(例如,如果摄氏是一个属性,这是相同的基本机制,但将所有源放在Temperature类中),但这表明可以做什么...

答案 2 :(得分:55)

  

我试图了解Python的描述符是什么以及它们对什么有用。

描述符是具有以下任何特殊方法的类属性(如属性或方法):

  • __get__(非数据描述符方法,例如关于方法/函数)
  • __set__(数据描述符方法,例如在属性实例上)
  • __delete__(数据描述符方法)

这些描述符对象可以用作其他对象类定义的属性。 (也就是说,它们位于类对象的__dict__中。)

描述符对象可用于以编程方式管理正常表达式,赋值,甚至删除中的虚线查找(例如foo.descriptor)的结果。

函数/方法,绑定方法,propertyclassmethodstaticmethod都使用这些特殊方法来控制通过虚线查找访问它们的方式。

数据描述符,如property,可以允许基于对象的更简单状态对属性进行延迟评估,允许实例使用比预先计算每个可能属性更少的内存。

__slots__创建的另一个数据描述符member_descriptor允许类将数据存储在可变的类似于元组的数据结构中,而不是更灵活但占用空间{{}},从而节省了内存。 1}}。

非数据描述符,通常是实例,类和静态方法,从它们的非数据描述符方法{{1}获取它们的隐式第一个参数(通常分别命名为__dict__cls) }}

Python的大多数用户只需要学习简单的用法,而不需要进一步学习或理解描述符的实现。

深度:什么是描述符?

描述符是具有以下任何方法(self__get____get__)的对象,旨在通过点查找使用,就像它是典型属性一样一个实例。对于所有者对象__set__,对象为__delete__

  • obj_instance调用
    descriptor返回obj_instance.descriptor 这就是所有方法和属性descriptor.__get__(self, obj_instance, owner_class)的工作方式。

  • value调用
    get返回obj_instance.descriptor = value 这是属性descriptor.__set__(self, obj_instance, value)的工作方式。

  • None调用
    setter返回del obj_instance.descriptor 这是属性descriptor.__delete__(self, obj_instance)的工作方式。

None是其实例,其类包含描述符对象的实例。 deleter描述符的实例(可能只是obj_instance类的一个

要使用代码定义它,如果对象的属性集与任何所需属性相交,则对象是描述符:

self

Data Descriptor有一个obj_instance和/或def has_descriptor_attrs(obj): return set(['__get__', '__set__', '__delete__']).intersection(dir(obj)) def is_descriptor(obj): """obj can be instance of descriptor or the descriptor class""" return bool(has_descriptor_attrs(obj)) 非数据描述符既没有__set__也没有__delete__

__set__

内置描述符对象示例:

  • __delete__
  • def has_data_descriptor_attrs(obj): return set(['__set__', '__delete__']) & set(dir(obj)) def is_data_descriptor(obj): return bool(has_data_descriptor_attrs(obj))
  • classmethod
  • 一般的功能

非数据描述符

我们可以看到staticmethodproperty是非数据描述符:

classmethod

两者都只有staticmethod方法:

>>> is_descriptor(classmethod), is_data_descriptor(classmethod)
(True, False)
>>> is_descriptor(staticmethod), is_data_descriptor(staticmethod)
(True, False)

请注意,所有函数都是非数据描述符:

__get__

数据描述符,>>> has_descriptor_attrs(classmethod), has_descriptor_attrs(staticmethod) (set(['__get__']), set(['__get__']))

但是,>>> def foo(): pass ... >>> is_descriptor(foo), is_data_descriptor(foo) (True, False) 是数据描述符:

property

虚线查询订单

这些是重要的distinctions,因为它们会影响虚线查找的查找顺序。

property
  1. 首先,上面查看该属性是否是实例类的数据描述符,
  2. 如果没有,它会查看该属性是否在>>> is_data_descriptor(property) True >>> has_descriptor_attrs(property) set(['__set__', '__get__', '__delete__']) ' obj_instance.attribute 中,然后
  3. 它最终回归到非数据描述符。
  4. 此查找顺序的结果是函数/方法之类的非数据描述符可以是overridden by instances

    回顾和后续步骤

    我们已经了解到,描述符是包含obj_instance__dict____get__任何内容的对象。这些描述符对象可以用作其他对象类定义的属性。现在我们将以代码为例来了解它们的使用方法。

    问题代码分析

    这是您的代码,然后是您的问题和答案:

    __set__
      
        
    1. 为什么我需要描述符类?
    2.   

    您的描述符确保您始终拥有此__delete__类属性的浮点数,并且您无法使用class Celsius(object): def __init__(self, value=0.0): self.value = float(value) def __get__(self, instance, owner): return self.value def __set__(self, instance, value): self.value = float(value) class Temperature(object): celsius = Celsius() 删除该属性:

    Temperature

    否则,您的描述符会忽略所有者的所有者类和实例,而是在描述符中存储状态。您可以使用简单的类属性轻松地在所有实例之间共享状态(只要您始终将其设置为类的浮点数并且永远不会删除它,或者对代码的用户这样做感到满意):

    del

    这会让你获得与你的例子完全相同的行为(请参阅下面对问题3的回答),但使用内置的Pythons(>>> t1 = Temperature() >>> del t1.celsius Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: __delete__ ),并且会被认为更加惯用:

    class Temperature(object):
        celsius = 0.0
    
      
        
    1. 这里的实例和所有者是什么? (在获取)。这些参数的目的是什么?
    2.   

    property是调用描述符的所有者的实例。所有者是使用描述符对象来管理对数据点的访问的类。有关更具描述性的变量名称,请参阅本答案第一段旁边定义描述符的特殊方法的描述。

      
        
    1. 我如何致电/使用此示例?
    2.   

    这是一个示范:

    class Temperature(object):
        _celsius = 0.0
        @property
        def celsius(self):
            return type(self)._celsius
        @celsius.setter
        def celsius(self, value):
            type(self)._celsius = float(value)
    

    您无法删除该属性:

    instance

    而且您无法指定一个无法转换为浮动的变量:

    >>> t1 = Temperature()
    >>> t1.celsius
    0.0
    >>> t1.celsius = 1
    >>> 
    >>> t1.celsius
    1.0
    >>> t2 = Temperature()
    >>> t2.celsius
    1.0
    

    否则,您所拥有的是所有实例的全局状态,通过分配给任何实例来管理。

    大多数有经验的Python程序员实现这一结果的预期方式是使用>>> del t2.celsius Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: __delete__ 装饰器,它使用相同的描述符,但将行为带入所有者类的实现(再次,如上所述):

    >>> t1.celsius = '0x02'
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 7, in __set__
    ValueError: invalid literal for float(): 0x02
    

    与原始代码完全相同的预期行为:

    property

    结论

    我们已经介绍了定义描述符的属性,数据描述符和非数据描述符之间的区别,使用它们的内置对象以及有关使用的特定问题。

    再说一遍,你会如何使用这个问题的例子?我希望你不会。我希望你从我的第一个建议(一个简单的类属性)开始,如果你认为有必要,继续第二个建议(属性装饰器)。

答案 3 :(得分:6)

  

为什么我需要描述符类?

受Buciano Ramalho的 Fluent Python 的启发

想象你有这样的课程

class LineItem:
     price = 10.9
     weight = 2.1
     def __init__(self, name, price, weight):
          self.name = name
          self.price = price
          self.weight = weight

item = LineItem("apple", 2.9, 2.1)
item.price = -0.9  # it's price is negative, you need to refund to your customer even you delivered the apple :(
item.weight = -0.8 # negative weight, it doesn't make sense

我们应该验证重量和价格以避免为它们分配负数,如果我们使用描述符作为代理,我们可以编写更少的代码

class Quantity(object):
    __index = 0

    def __init__(self):
        self.__index = self.__class__.__index
        self._storage_name = "quantity#{}".format(self.__index)
        self.__class__.__index += 1

    def __set__(self, instance, value):
        if value > 0:
            setattr(instance, self._storage_name, value)
        else:
           raise ValueError('value should >0')

   def __get__(self, instance, owner):
        return getattr(instance, self._storage_name)

然后像这样定义类LineItem:

class LineItem(object):
     weight = Quantity()
     price = Quantity()

     def __init__(self, name, weight, price):
         self.name = name
         self.weight = weight
         self.price = price

我们可以扩展Quantity类来做更常见的验证

答案 4 :(得分:3)

在详细介绍描述符之前,了解Python中属性查找的工作原理可能很重要。假设该类没有元类,并且使用默认的__getattribute__实现(两者均可用于“自定义”行为)。

在这种情况下,属性查找的最佳说明(在Python 3.x中或在Python 2.x中用于新型类)来自Understanding Python metaclasses (ionel's codelog)。该图像使用:代替“不可自定义的属性查找”。

这表示在foobar的{​​{1}}上查找属性instance

enter image description here

这里有两个重要条件:

  • 如果Class类具有属性名称条目,并且它具有instance__get__
  • 如果__set__的属性名称条目为 no ,但该类只有一个且具有instance

这就是描述符的所在:

  • 数据描述符,它们同时具有__get____get__
  • 非数据描述符,仅包含__set__

在两种情况下,返回的值都将通过__get__进行调用,其中实例作为第一个参数,而类作为第二个参数。

对于类属性查找,查找甚至更加复杂(例如,参见Class attribute lookup (in the above mentioned blog))。

让我们转到您的特定问题:

  

为什么需要描述符类?

在大多数情况下,您不需要编写描述符类!但是,您可能是非常普通的最终用户。例如功能。函数是描述符,这就是将函数__get__作为第一个参数隐式传递的方法。

self

如果您在实例上查询def test_function(self): return self class TestClass(object): def test_method(self): ... ,则会返回“绑定方法”:

test_method

类似地,您也可以通过手动调用函数>>> instance = TestClass() >>> instance.test_method <bound method TestClass.test_method of <__main__.TestClass object at ...>> 来绑定函数(不建议这样做,仅出于说明目的):

__get__

您甚至可以称之为“自绑定方法”:

>>> test_function.__get__(instance, TestClass)
<bound method test_function of <__main__.TestClass object at ...>>

请注意,我没有提供任何参数,该函数确实返回了绑定的实例!

函数是非数据描述符

数据描述符的一些内置示例为>>> test_function.__get__(instance, TestClass)() <__main__.TestClass at ...> 。忽略property的{​​{1}},gettersetter描述符是(来自Descriptor HowTo Guide "Properties"):

deleter

由于它是数据描述符,因此只要您查询property的“名称”就会调用它,并且它只是委托给以class Property(object): 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) property和{{1 }}(如果有)。

标准库中还有其他几个描述符,例如@property@name.setter

描述符的要点很容易(尽管您很少需要它们):用于属性访问的抽象通用代码。 @name.deleter是实例变量访问的抽象,staticmethod提供了方法的抽象,classmethod提供了不需要实例访问的方法的抽象,property提供了抽象的方法用于需要类访问权限而不是实例访问权限的方法(这有点简化了)。

另一个示例是class property

一个有趣的示例(使用Python 3.6中的function)也可以是仅允许特定类型的属性:

staticmethod

然后您可以在类中使用描述符:

classmethod

然后玩一点:

__set_name__

或“懒惰的财产”:

class TypedProperty(object):
    __slots__ = ('_name', '_type')
    def __init__(self, typ):
        self._type = typ

    def __get__(self, instance, klass=None):
        if instance is None:
            return self
        return instance.__dict__[self._name]

    def __set__(self, instance, value):
        if not isinstance(value, self._type):
            raise TypeError(f"Expected class {self._type}, got {type(value)}")
        instance.__dict__[self._name] = value

    def __delete__(self, instance):
        del instance.__dict__[self._name]

    def __set_name__(self, klass, name):
        self._name = name

在某些情况下,将逻辑移到公共描述符中可能是有道理的,但是也可以用其他方法解决它们(但可能需要重复一些代码)。

  

这里的class Test(object): int_prop = TypedProperty(int) >>> t = Test() >>> t.int_prop = 10 >>> t.int_prop 10 >>> t.int_prop = 20.0 TypeError: Expected class <class 'int'>, got <class 'float'> 是什么? (在class LazyProperty(object): __slots__ = ('_fget', '_name') def __init__(self, fget): self._fget = fget def __get__(self, instance, klass=None): if instance is None: return self try: return instance.__dict__[self._name] except KeyError: value = self._fget(instance) instance.__dict__[self._name] = value return value def __set_name__(self, klass, name): self._name = name class Test(object): @LazyProperty def lazy(self): print('calculating') return 10 >>> t = Test() >>> t.lazy calculating 10 >>> t.lazy 10 中)。这些参数的目的是什么?

这取决于您如何查找属性。如果您在实例上查找属性,则:

  • 第二个参数是您在其中查找属性的实例
  • 第三个参数是实例的类

如果您在类上查找属性(假设描述符是在类上定义的):

  • 第二个参数是instance
  • 第三个参数是您在其中查找属性的类

因此,基本上,如果要在进行类级查找时自定义行为,则必须使用第三个参数(因为owner__get__)。

  

我将如何调用/使用此示例?

您的示例基本上是一个属性,该属性仅允许将值转换为None,并且在类的所有实例之间(以及在该类上共享),尽管一个实例只能在对象上使用“读取”访问权限类,否则您将替换描述符实例):

instance

这就是为什么描述符通常使用第二个参数(None)来存储值以避免共享的原因。但是,在某些情况下,可能需要在实例之间共享一个值(尽管目前我无法想到一个方案)。但是,对于温度类的摄氏温度特性几乎没有任何意义……除了纯粹作为学术练习之外。

答案 5 :(得分:0)

我尝试了安德鲁库克回答的代码(根据建议进行了细微更改)。 (我正在运行python 2.7)。

代码:

#!/usr/bin/env python
class Celsius:
    def __get__(self, instance, owner): return 9 * (instance.fahrenheit + 32) / 5.0
    def __set__(self, instance, value): instance.fahrenheit = 32 + 5 * value / 9.0

class Temperature:
    def __init__(self, initial_f): self.fahrenheit = initial_f
    celsius = Celsius()

if __name__ == "__main__":

    t = Temperature(212)
    print(t.celsius)
    t.celsius = 0
    print(t.fahrenheit)

结果:

C:\Users\gkuhn\Desktop>python test2.py
<__main__.Celsius instance at 0x02E95A80>
212

使用3之前的Python,确保你从object中继承子类,这将使描述符正常工作,因为 get 魔法对旧样式类不起作用。

答案 6 :(得分:0)

您会看到https://docs.python.org/3/howto/descriptor.html#properties

library(janitor); library(dplyr)
df %>%
  tabyl(Wealth, choice) %>%
  adorn_percentages("row")

# Wealth         1         2         3
#      5 0.0000000 0.7500000 0.2500000
#      8 0.5000000 0.0000000 0.5000000
#     10 0.3333333 0.3333333 0.3333333