理解 __get__ 和 __set__ 以及 Python 描述符

发布于 2024-09-25 10:38:39 字数 546 浏览 10 评论 0 原文

试图了解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. 这里的实例所有者是什么? (在__get__中)。这些参数的用途是什么?

  3. 我如何调用/使用这个例子?

I am trying to understand what Python's descriptors are and what they are useful for. I understand how they work, but here are my doubts. Consider the following code:

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. Why do I need the descriptor class?

  2. What is instance and owner here? (in __get__). What is the purpose of these parameters?

  3. How would I call/use this example?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

我是男神闪亮亮 2024-10-02 10:38:39

描述符是 Python 的 property 类型的实现方式。描述符只需实现__get____set__等,然后添加到其定义中的另一个类中(就像上面对Temperature类所做的那样)。例如:

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

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

__get__ 中的 instance 是类的实例(因此上面的 __get__ 将接收 temp,而 Owner 是具有描述符的类(因此它是 Temperature)。

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

官方Python文档包含一个关于描述符的文章,更详细地介绍了它们的工作原理,包括几个示例:

编辑:正如 jchl 在评论中指出的,如果您只是尝试 Temperature.celsius,实例将为

The descriptor is how Python's property type is implemented. A descriptor simply implements __get__, __set__, etc. and is then added to another class in its definition (as you did above with the Temperature class). For example:

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

Accessing the property you assigned the descriptor to (celsius in the above example) calls the appropriate descriptor method.

instance in __get__ is the instance of the class (so above, __get__ would receive temp, while owner is the class with the descriptor (so it would be Temperature).

You need to use a descriptor class to encapsulate the logic that powers it. That way, if the descriptor is used to cache some expensive operation (for example), it could store the value on itself and not its class.

The official Python documentation includes an article about descriptors that walks through how they work in more detail, including several examples.

EDIT: As jchl pointed out in the comments, if you simply try Temperature.celsius, instance will be None.

追风人 2024-10-02 10:38:39

为什么我需要描述符类?

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

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

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

这里的实例所有者是什么? (在__get__中)。这些参数的目的是什么?

这是非常微妙的(也是我在这里写一个新答案的原因 - 我在想知道同样的事情时发现了这个问题,但没有发现现有的答案那么好)。

描述符在类上定义,但通常从实例调用。当从实例调用它时,instanceowner 都会被设置(并且您可以从 instance 计算出 owner,因此似乎有点毫无意义)。但是当从类中调用时,只有 owner 被设置 - 这就是它存在的原因。

只有 __get__ 才需要它,因为它是唯一可以在类上调用的函数。如果设置类值,则设置描述符本身。删除也是如此。这就是为什么那里不需要 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 中实现相同的效果(例如,如果 celsius 是一个属性,这是相同的基本机制,但将所有源放在Temperature类中),但这显示了可以做什么...

Why do I need the descriptor class?

It gives you extra control over how attributes work. If you're used to getters and setters in Java, for example, then it's Python's way of doing that. One advantage is that it looks to users just like an attribute (there's no change in syntax). So you can start with an ordinary attribute and then, when you need to do something fancy, switch to a descriptor.

An attribute is just a mutable value. A descriptor lets you execute arbitrary code when reading or setting (or deleting) a value. So you could imagine using it to map an attribute to a field in a database, for example – a kind of ORM.

Another use might be refusing to accept a new value by throwing an exception in __set__ – effectively making the "attribute" read only.

What is instance and owner here? (in __get__). What is the purpose of these parameters?

This is pretty subtle (and the reason I am writing a new answer here - I found this question while wondering the same thing and didn't find the existing answer that great).

A descriptor is defined on a class, but is typically called from an instance. When it's called from an instance both instance and owner are set (and you can work out owner from instance so it seems kinda pointless). But when called from a class, only owner is set – which is why it's there.

This is only needed for __get__ because it's the only one that can be called on a class. If you set the class value you set the descriptor itself. Similarly for deletion. Which is why the owner isn't needed there.

How would I call/use this example?

Well, here's a cool trick using similar classes:

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)

(I'm using Python 3; for python 2 you need to make sure those divisions are / 5.0 and / 9.0). That gives:

100.0
32.0

Now there are other, arguably better ways to achieve the same effect in python (e.g. if celsius were a property, which is the same basic mechanism but places all the source inside the Temperature class), but that shows what can be done...

余罪 2024-10-02 10:38:39

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

描述符是类命名空间中管理实例属性(如槽、属性或方法)的对象。例如:

class HasDescriptors:
    __slots__ = 'a_slot' # creates a descriptor
    
    def a_method(self):  # creates a descriptor
        "a regular method"
    
    @staticmethod        # creates a descriptor
    def a_static_method():
        "a static method"
    
    @classmethod         # creates a descriptor
    def a_class_method(cls):
        "a class method"
    
    @property            # creates a descriptor
    def a_property(self):
        "a property"

# even a regular function:
def a_function(some_obj_or_self):      # creates a descriptor
    "create a function suitable for monkey patching"

HasDescriptors.a_function = a_function     # (but we usually don't do this)

迂腐地讲,描述符是具有以下特殊方法的对象,这些方法可能称为“描述符方法”:

  • __get__:例如,非数据描述符方法在方法/函数上
  • __set__:数据描述符方法,例如在属性实例或槽上
  • __delete__:数据描述符方法,再次由属性或槽使用

这些描述符对象是属性在其他对象类命名空间中。也就是说,它们位于类对象的 __dict__ 中。

描述符对象以编程方式管理普通表达式、赋值或删除中的点查找(例如foo.descriptor)的结果。

函数/方法、绑定方法、属性、类方法和静态方法都使用这些特殊方法来控制如何通过点查找来访问它们。

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

另一个数据描述符,由__slots__创建的member_descriptor ,通过让类将数据存储在可变的类似元组的数据结构中,而不是更灵活但占用空间的 __dict__ 中,可以节省内存(并加快查找速度)。

非数据描述符、实例和类方法从其非数据描述符方法 __get__ 获取其隐式第一个参数(通常分别命名为 selfcls - 这就是静态方法知道没有隐式第一个参数的方式。

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

但了解描述符的工作原理可以让人们对自己掌握 Python 更有信心。

深入:什么是描述符?

描述符是具有以下任何方法(__get____set____delete__)的对象,旨在通过点式查找用作如果它是一个实例的典型属性。对于具有 descriptor 对象的所有者对象 obj_instance

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

  • obj_instance.descriptor = value 调用
    descriptor.__set__(self, obj_instance, value) 返回None
    这就是属性上的 setter 的工作原理。

  • del obj_instance.descriptor 调用
    descriptor.__delete__(self, obj_instance) 返回 None
    这就是属性上的删除器的工作原理。

obj_instance 是其类包含描述符对象实例的实例。 self描述符的实例(可能只是 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))

A 数据描述符有一个__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
  • staticmethod
  • property
  • 一般

非数据描述

函数我们可以看到 classmethod 和 < code>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

然而,property 是一个数据描述符:

>>> is_data_descriptor(property)
True
>>> has_descriptor_attrs(property)
set(['__set__', '__get__', '__delete__'])

点式查找顺序

这些很重要区别,因为它们影响点式查找的查找顺序。

obj_instance.attribute
  1. 首先,上面查看该属性是否是实例类上的数据描述符,
  2. 如果不是,则查看该属性是否在 obj_instance__dict__,然后
  3. 它最终回到非数据描述符。

这种查找顺序的结果是像函数/方法这样的非数据描述符可以被实例覆盖

回顾和后续步骤

我们已经了解到,描述符是具有 __get____set____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()
  1. 为什么需要描述符类?

您的描述符确保 Temperature 此类属性始终具有浮点数,并且您无法使用 del 删除该属性:

>>> t1 = Temperature()
>>> del t1.celsius
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: __delete__

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

class Temperature(object):
    celsius = 0.0

:与您的示例的行为相同(请参阅下面对问题 3 的回复),但使用 Python 内置函数(property),并且会被认为更惯用:

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
    

    你不能删除该属性:

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

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

    大多数经验丰富的 Python 程序员实现此结果的预期方法是使用属性装饰器,它在底层使用相同的描述符,但将行为引入所有者类的实现中(再次,如上面所定义):

    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)
    

    它与原始代码段具有完全相同的预期行为:

    >>> t1 = Temperature()
    >>> t2 = Temperature()
    >>> t1.celsius
    0.0
    >>> t1.celsius = 1.0
    >>> t2.celsius
    1.0
    >>> del t1.celsius
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: can't delete attribute
    >>> t1.celsius = '0x02'
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 8, in celsius
    ValueError: invalid literal for float(): 0x02
    

    结论

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

    那么,您将如何使用问题的示例?我希望你不会。我希望您从我的第一个建议(一个简单的类属性)开始,如果您认为有必要,请继续讨论第二个建议(属性装饰器)。

    I am trying to understand what Python's descriptors are and what they can be useful for.

    Descriptors are objects in a class namespace that manage instance attributes (like slots, properties, or methods). For example:

    class HasDescriptors:
        __slots__ = 'a_slot' # creates a descriptor
        
        def a_method(self):  # creates a descriptor
            "a regular method"
        
        @staticmethod        # creates a descriptor
        def a_static_method():
            "a static method"
        
        @classmethod         # creates a descriptor
        def a_class_method(cls):
            "a class method"
        
        @property            # creates a descriptor
        def a_property(self):
            "a property"
    
    # even a regular function:
    def a_function(some_obj_or_self):      # creates a descriptor
        "create a function suitable for monkey patching"
    
    HasDescriptors.a_function = a_function     # (but we usually don't do this)
    

    Pedantically, descriptors are objects with any of the following special methods, which may be known as "descriptor methods":

    • __get__: non-data descriptor method, for example on a method/function
    • __set__: data descriptor method, for example on a property instance or slot
    • __delete__: data descriptor method, again used by properties or slots

    These descriptor objects are attributes in other object class namespaces. That is, they live in the __dict__ of the class object.

    Descriptor objects programmatically manage the results of a dotted lookup (e.g. foo.descriptor) in a normal expression, an assignment, or a deletion.

    Functions/methods, bound methods, property, classmethod, and staticmethod all use these special methods to control how they are accessed via the dotted lookup.

    A data descriptor, like property, can allow for lazy evaluation of attributes based on a simpler state of the object, allowing instances to use less memory than if you precomputed each possible attribute.

    Another data descriptor, a member_descriptor created by __slots__, allows memory savings (and faster lookups) by having the class store data in a mutable tuple-like datastructure instead of the more flexible but space-consuming __dict__.

    Non-data descriptors, instance and class methods, get their implicit first arguments (usually named self and cls, respectively) from their non-data descriptor method, __get__ - and this is how static methods know not to have an implicit first argument.

    Most users of Python need to learn only the high-level usage of descriptors, and have no need to learn or understand the implementation of descriptors further.

    But understanding how descriptors work can give one greater confidence in one's mastery of Python.

    In Depth: What Are Descriptors?

    A descriptor is an object with any of the following methods (__get__, __set__, or __delete__), intended to be used via dotted-lookup as if it were a typical attribute of an instance. For an owner-object, obj_instance, with a descriptor object:

    • obj_instance.descriptor invokes
      descriptor.__get__(self, obj_instance, owner_class) returning a value
      This is how all methods and the get on a property work.

    • obj_instance.descriptor = value invokes
      descriptor.__set__(self, obj_instance, value) returning None
      This is how the setter on a property works.

    • del obj_instance.descriptor invokes
      descriptor.__delete__(self, obj_instance) returning None
      This is how the deleter on a property works.

    obj_instance is the instance whose class contains the descriptor object's instance. self is the instance of the descriptor (probably just one for the class of the obj_instance)

    To define this with code, an object is a descriptor if the set of its attributes intersects with any of the required attributes:

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

    A Data Descriptor has a __set__ and/or __delete__.
    A Non-Data-Descriptor has neither __set__ nor __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))
    

    Builtin Descriptor Object Examples:

    • classmethod
    • staticmethod
    • property
    • functions in general

    Non-Data Descriptors

    We can see that classmethod and staticmethod are Non-Data-Descriptors:

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

    Both only have the __get__ method:

    >>> has_descriptor_attrs(classmethod), has_descriptor_attrs(staticmethod)
    (set(['__get__']), set(['__get__']))
    

    Note that all functions are also Non-Data-Descriptors:

    >>> def foo(): pass
    ... 
    >>> is_descriptor(foo), is_data_descriptor(foo)
    (True, False)
    

    Data Descriptor, property

    However, property is a Data-Descriptor:

    >>> is_data_descriptor(property)
    True
    >>> has_descriptor_attrs(property)
    set(['__set__', '__get__', '__delete__'])
    

    Dotted Lookup Order

    These are important distinctions, as they affect the lookup order for a dotted lookup.

    obj_instance.attribute
    
    1. First the above looks to see if the attribute is a Data-Descriptor on the class of the instance,
    2. If not, it looks to see if the attribute is in the obj_instance's __dict__, then
    3. it finally falls back to a Non-Data-Descriptor.

    The consequence of this lookup order is that Non-Data-Descriptors like functions/methods can be overridden by instances.

    Recap and Next Steps

    We have learned that descriptors are objects with any of __get__, __set__, or __delete__. These descriptor objects can be used as attributes on other object class definitions. Now we will look at how they are used, using your code as an example.


    Analysis of Code from the Question

    Here's your code, followed by your questions and answers to each:

    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. Why do I need the descriptor class?

    Your descriptor ensures you always have a float for this class attribute of Temperature, and that you can't use del to delete the attribute:

    >>> t1 = Temperature()
    >>> del t1.celsius
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: __delete__
    

    Otherwise, your descriptors ignore the owner-class and instances of the owner, instead, storing state in the descriptor. You could just as easily share state across all instances with a simple class attribute (so long as you always set it as a float to the class and never delete it, or are comfortable with users of your code doing so):

    class Temperature(object):
        celsius = 0.0
    

    This gets you exactly the same behavior as your example (see response to question 3 below), but uses a Pythons builtin (property), and would be considered more idiomatic:

    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)
    
    1. What is instance and owner here? (in get). What is the purpose of these parameters?

    instance is the instance of the owner that is calling the descriptor. The owner is the class in which the descriptor object is used to manage access to the data point. See the descriptions of the special methods that define descriptors next to the first paragraph of this answer for more descriptive variable names.

    1. How would I call/use this example?

    Here's a demonstration:

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

    You can't delete the attribute:

    >>> del t2.celsius
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: __delete__
    

    And you can't assign a variable that can't be converted to a float:

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

    Otherwise, what you have here is a global state for all instances, that is managed by assigning to any instance.

    The expected way that most experienced Python programmers would accomplish this outcome would be to use the property decorator, which makes use of the same descriptors under the hood, but brings the behavior into the implementation of the owner class (again, as defined above):

    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)
    

    Which has the exact same expected behavior of the original piece of code:

    >>> t1 = Temperature()
    >>> t2 = Temperature()
    >>> t1.celsius
    0.0
    >>> t1.celsius = 1.0
    >>> t2.celsius
    1.0
    >>> del t1.celsius
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: can't delete attribute
    >>> t1.celsius = '0x02'
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 8, in celsius
    ValueError: invalid literal for float(): 0x02
    

    Conclusion

    We've covered the attributes that define descriptors, the difference between data- and non-data-descriptors, builtin objects that use them, and specific questions about use.

    So again, how would you use the question's example? I hope you wouldn't. I hope you would start with my first suggestion (a simple class attribute) and move on to the second suggestion (the property decorator) if you feel it is necessary.

    朕就是辣么酷 2024-10-02 10:38:39

    在深入了解描述符的详细信息之前,了解 Python 中的属性查找如何工作可能很重要。这假设该类没有元类,并且它使用 __getattribute__ 的默认实现(两者都可以用于“自定义”行为)。

    在这种情况下,属性查找(在 Python 3.x 中或 Python 2.x 中的新式类)的最佳说明来自 了解 Python 元类(ionel 的代码日志)。该图像使用 : 代替“不可自定义的属性查找”。

    这表示在 Class实例 上查找属性 foobar

    在此处输入图像描述

    这里有两个条件很重要:

    • 如果instance 的类有一个属性名称条目,它有 __get____set__
    • 如果实例没有属性名称条目,但类有一个并且具有__get__

    这就是描述符的用武之地:

    • 数据描述符,它同时具有__get____set__
    • 非数据描述符,仅具有__get__

    在这两种情况下,返回值都会通过 __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 ...>
    

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

    函数是非数据描述符

    数据描述符的一些内置示例是属性。忽略 gettersetterdeleterproperty 描述符是(来自 描述符操作指南“属性”):

    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)
    

    由于它是一个数据描述符,因此每当您查找 < 的“名称”时都会调用它code>property 并且它只是委托给用 @property@name.setter@name.deleter 修饰的函数(如果存在)。

    标准库中还有其他几个描述符,例如staticmethodclassmethod

    描述符的要点很简单(尽管您很少需要它们):用于属性访问的抽象公共代码。 property 是实例变量访问的抽象,function 提供方法的抽象,staticmethod 提供不需要实例访问的方法的抽象classmethod 为需要类访问而不是实例访问的方法提供了抽象(这有点简化)。

    另一个例子是类属性

    一个有趣的例子(使用 Python 3.6 中的 __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'>
    

    或者“懒惰” property”:

    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
    

    在这些情况下,将逻辑移动到公共描述符中可能是有意义的,但是也可以通过其他方式解决它们(但可能需要重复一些代码)。

    这里的实例所有者是什么? (在__get__中)。这些参数的目的是什么?

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

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

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

    • 第二个参数是 None
    • 第三个参数是您查找属性的类

    所以基本上,如果您想在进行类级别查找时自定义行为,则第三个参数是必需的 - (因为实例None)。

    我如何调用/使用这个例子?

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

    >>> t1 = Temperature()
    >>> t2 = Temperature()
    
    >>> t1.celsius = 20   # setting it on one instance
    >>> t2.celsius        # looking it up on another instance
    20.0
    
    >>> Temperature.celsius  # looking it up on the class
    20.0
    

    这就是描述符通常使用第二个参数(实例)来存储值以避免共享它的原因。然而,在某些情况下,可能需要在实例之间共享值(尽管我目前无法想到场景)。然而,对于温度类别中的摄氏属性来说,它实际上没有任何意义……除非作为纯粹的学术练习。

    Before going into the details of descriptors it may be important to know how attribute lookup in Python works. This assumes that the class has no metaclass and that it uses the default implementation of __getattribute__ (both can be used to "customize" the behavior).

    The best illustration of attribute lookup (in Python 3.x or for new-style classes in Python 2.x) in this case is from Understanding Python metaclasses (ionel's codelog). The image uses : as substitute for "non-customizable attribute lookup".

    This represents the lookup of an attribute foobar on an instance of Class:

    enter image description here

    Two conditions are important here:

    • If the class of instance has an entry for the attribute name and it has __get__ and __set__.
    • If the instance has no entry for the attribute name but the class has one and it has __get__.

    That's where descriptors come into it:

    • Data descriptors which have both __get__ and __set__.
    • Non-data descriptors which only have __get__.

    In both cases the returned value goes through __get__ called with the instance as first argument and the class as second argument.

    The lookup is even more complicated for class attribute lookup (see for example Class attribute lookup (in the above mentioned blog)).

    Let's move to your specific questions:

    Why do I need the descriptor class?

    In most cases you don't need to write descriptor classes! However you're probably a very regular end user. For example functions. Functions are descriptors, that's how functions can be used as methods with self implicitly passed as first argument.

    def test_function(self):
        return self
    
    class TestClass(object):
        def test_method(self):
            ...
    

    If you look up test_method on an instance you'll get back a "bound method":

    >>> instance = TestClass()
    >>> instance.test_method
    <bound method TestClass.test_method of <__main__.TestClass object at ...>>
    

    Similarly you could also bind a function by invoking its __get__ method manually (not really recommended, just for illustrative purposes):

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

    You can even call this "self-bound method":

    >>> test_function.__get__(instance, TestClass)()
    <__main__.TestClass at ...>
    

    Note that I did not provide any arguments and the function did return the instance I had bound!

    Functions are Non-data descriptors!

    Some built-in examples of a data-descriptor would be property. Neglecting getter, setter, and deleter the property descriptor is (from Descriptor HowTo Guide "Properties"):

    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)
    

    Since it's a data descriptor it's invoked whenever you look up the "name" of the property and it simply delegates to the functions decorated with @property, @name.setter, and @name.deleter (if present).

    There are several other descriptors in the standard library, for example staticmethod, classmethod.

    The point of descriptors is easy (although you rarely need them): Abstract common code for attribute access. property is an abstraction for instance variable access, function provides an abstraction for methods, staticmethod provides an abstraction for methods that don't need instance access and classmethod provides an abstraction for methods that need class access rather than instance access (this is a bit simplified).

    Another example would be a class property.

    One fun example (using __set_name__ from Python 3.6) could also be a property that only allows a specific type:

    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
    

    Then you can use the descriptor in a class:

    class Test(object):
        int_prop = TypedProperty(int)
    

    And playing a bit with it:

    >>> t = Test()
    >>> t.int_prop = 10
    >>> t.int_prop
    10
    
    >>> t.int_prop = 20.0
    TypeError: Expected class <class 'int'>, got <class 'float'>
    

    Or a "lazy property":

    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
    

    These are cases where moving the logic into a common descriptor might make sense, however one could also solve them (but maybe with repeating some code) with other means.

    What is instance and owner here? (in __get__). What is the purpose of these parameters?

    It depends on how you look up the attribute. If you look up the attribute on an instance then:

    • the second argument is the instance on which you look up the attribute
    • the third argument is the class of the instance

    In case you look up the attribute on the class (assuming the descriptor is defined on the class):

    • the second argument is None
    • the third argument is the class where you look up the attribute

    So basically the third argument is necessary if you want to customize the behavior when you do class-level look-up (because the instance is None).

    How would I call/use this example?

    Your example is basically a property that only allows values that can be converted to float and that is shared between all instances of the class (and on the class - although one can only use "read" access on the class otherwise you would replace the descriptor instance):

    >>> t1 = Temperature()
    >>> t2 = Temperature()
    
    >>> t1.celsius = 20   # setting it on one instance
    >>> t2.celsius        # looking it up on another instance
    20.0
    
    >>> Temperature.celsius  # looking it up on the class
    20.0
    

    That's why descriptors generally use the second argument (instance) to store the value to avoid sharing it. However in some cases sharing a value between instances might be desired (although I cannot think of a scenario at this moment). However it makes practically no sense for a celsius property on a temperature class... except maybe as purely academic exercise.

    源来凯始玺欢你 2024-10-02 10:38:39

    为什么我需要描述符类?

    受到 Buciano Ramalho

    Imaging 的 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 类来执行更常见的验证

    Why do I need the descriptor class?

    Inspired by Fluent Python by Buciano Ramalho

    Imaging you have a class like this

    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
    

    We should validate the weight and price in avoid to assign them a negative number, we can write less code if we use descriptor as a proxy as this

    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)
    

    then define class LineItem like this:

    class LineItem(object):
         weight = Quantity()
         price = Quantity()
    
         def __init__(self, name, weight, price):
             self.name = name
             self.weight = weight
             self.price = price
    

    and we can extend the Quantity class to do more common validating

    神妖 2024-10-02 10:38:39

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

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

    You'd see https://docs.python.org/3/howto/descriptor.html#properties

    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__)
    
    黎歌 2024-10-02 10:38:39

    易于理解(带有示例)__get__ & 的解释__设置__ & __call__ 在类中,什么是Owner、Instance

    在深入研究之前需要先了解一下:

    1. __get__ __set__ 被称为类的描述符工作/保存它们的内部属性,即:__name__(类/所有者类的名称),变量-__dict__等。稍后我将解释什么是所有者。
    2. 设计中使用了描述符更常见的是,例如,使用装饰器(将事物抽象出来)。您可以认为它更常用于软件架构设计中,以减少冗余并提高可读性(似乎很讽刺)。因此遵守 SOLID 和 DRY 原则。
    3. 如果您设计的软件不应该遵守 SOLID 和 DRY 原则,您可能不需要它们,但理解它们总是明智的。

    1. 考虑这段代码:

    class Method:
        def __init__(self, name):
            self.name = name
        def __call__(self, instance, arg1, arg2):
            print(f"{self.name}: {instance} called with {arg1} and {arg2}")
    
    
    class MyClass:
        method = Method("Internal call")
    
    instance = MyClass()
    
    
    instance.method("first", "second")
    
    # Prints:TypeError: __call__() missing 1 required positional argument: 'arg2'
    
    

    因此,当调用 instance.method("first", "second") 时,会从 Method 类调用 __call__ 方法(call 方法使一个像函数一样可调用的类对象 - 每当类实例被调用时 __call__ 就会被实例化),并分配以下参数:instance: "first", arg1: "second",并且最后一个arg2被省略,这会打印出错误:TypeError: __call__()missing 1 requiredpositional argument: 'arg2'

    2.如何解决它?

    • 由于__call__采用instance作为第一个参数(instance、arg1、arg2),但是instance是什么?

    • Instance 是调用描述符类(Method)的主类(MyClass)的实例。那么,instance = MyClass()实例,那么谁是所有者?持有描述符类的类 - MyClass,但是,我们的描述符类 (Method) 中没有方法将其识别为实例。这就是我们需要 __get__ 方法的地方。再次考虑下面的代码:

    
    
    from types import MethodType
    class Method:
        def __init__(self, name):
            self.name = name
        def __call__(self, instance, arg1, arg2):
            print(f"{self.name}: {instance} called with {arg1} and {arg2}")
        def __set__(self, instance, value):
            self.value = value
            instance.__dict__["method"] = value
        def __get__(self, instance, owner):
            if instance is None:
                return self
            print (instance, owner)
            return MethodType(self, instance)   
    
    
    class MyClass:
        method = Method("Internal call")
    
    instance = MyClass()
    
    
    instance.method("first", "second") 
    # Prints: Internal call: <__main__.MyClass object at 0x7fb7dd989690> called with first and second
    

    根据文档,暂时忘记 set

    __get__ “调用以获取所有者类的属性(类属性访问)或该类的实例(实例属性访问)。”

    如果您这样做: instance.method.__get__(instance)

    Prints:<__main__.MyClass object at 0x7fb7dd9eab90> <类'__main__.MyClass'>

    这意味着实例:MyClass 的对象,即instance
    并且 OwnerMyClass 本身

    3. __set__ 说明:

    __set__ 用于设置类中的一些值 >__dict__ 对象(假设使用命令行)。设置 set 内部值的命令是: instance.descriptor = 'value' # 在本例中,描述符是 method

    • < em>(代码中的instance.__dict__["method"] = value只是更新描述符的__dict__对象)

    • 所以这样做: instance.method = 'value' 现在检查 value = 'value' 是否在我们可以访问 __set__ 方法中设置描述符方法的__dict__对象。
      做:
      instance.method.__dict__ 打印:{'_name': 'Internal call', 'value': 'value'}

    • 或者你可以检查 使用 vars(instance.method) 的 __dict__ 值
      prints: {'name': '内部调用', 'value': 'value'}
      我希望现在事情已经清楚了:)


    Easy to digest (with example) Explanation for __get__ & __set__ & __call__ in classes, what is Owner, Instance?

    Some points to mug up before diving in:

    1. __get__ __set__ are called descriptors of the class to work/save their internal attributes namely: __name__ (name of class/owner class), variables - __dict__ etc. I will explain what is an owner later
    2. Descriptors are used in design patterers more commonly, for example, with decorators (to abstract things out). You can consider it's more often used in software architecture design to make things less redundant and more readable (seems ironical). Thus abiding SOLID and DRY principles.
    3. If you are not designing software that should abide by SOLID and DRY principles, you probably don't need them, but it's always wise to understand them.

    1. Conside this code:

    class Method:
        def __init__(self, name):
            self.name = name
        def __call__(self, instance, arg1, arg2):
            print(f"{self.name}: {instance} called with {arg1} and {arg2}")
    
    
    class MyClass:
        method = Method("Internal call")
    
    instance = MyClass()
    
    
    instance.method("first", "second")
    
    # Prints:TypeError: __call__() missing 1 required positional argument: 'arg2'
    
    

    So, when instance.method("first", "second") is called, __call__ method is called from the Method class (call method makes a class object just callable like a function - whenever a class instance is called __call__ gets instiantiated), and following arguments are assigned: instance: "first", arg1: "second", and the last arg2 is left out, this prints out the error: TypeError: __call__() missing 1 required positional argument: 'arg2'

    2. how to solve it?

    • Since __call__ takes instance as first argument (instance, arg1, arg2), but instance of what?

    • Instance is the instance of main class (MyClass) which is calling the descriptor class (Method). So, instance = MyClass() is the instance and so who is the owner? the class holding the discriptor class - MyClass, However, there is no method in our descriptor class (Method) to recognise it as an instance. So that is where we need __get__ method. Again consider the code below:

    
    
    from types import MethodType
    class Method:
        def __init__(self, name):
            self.name = name
        def __call__(self, instance, arg1, arg2):
            print(f"{self.name}: {instance} called with {arg1} and {arg2}")
        def __set__(self, instance, value):
            self.value = value
            instance.__dict__["method"] = value
        def __get__(self, instance, owner):
            if instance is None:
                return self
            print (instance, owner)
            return MethodType(self, instance)   
    
    
    class MyClass:
        method = Method("Internal call")
    
    instance = MyClass()
    
    
    instance.method("first", "second") 
    # Prints: Internal call: <__main__.MyClass object at 0x7fb7dd989690> called with first and second
    

    forget about set for now according to docs:

    __get__ "Called to get the attribute of the owner class (class attribute access) or of an instance of that class (instance attribute access)."

    if you do: instance.method.__get__(instance)

    Prints:<__main__.MyClass object at 0x7fb7dd9eab90> <class '__main__.MyClass'>

    this means instance: object of MyClass which is instance
    and Owner is MyClass itself

    3. __set__ Explaination:

    __set__ is used to set some value in the class __dict__ object (let's say using a command line). command for setting the internal value for set is: instance.descriptor = 'value' # where descriptor is method in this case

    • (instance.__dict__["method"] = value in the code just update the __dict__ object of the descriptor)

    • So do: instance.method = 'value' now to check if the value = 'value' is set in the __set__ method we can access __dict__ object of the descriptor method.
      Do:
      instance.method.__dict__ prints: {'_name': 'Internal call', 'value': 'value'}

    • Or you can check the __dict__ value using vars(instance.method)
      prints: {'name': 'Internal call', 'value': 'value'}
      I hope things are clear now:)

    微暖i 2024-10-02 10:38:39

    我尝试了安德鲁·库克答案中的代码(按照建议进行了微小的更改)。 (我正在运行 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
    

    对于 Python 3 之前的版本,请确保从对象进行子类化,这将使描述符正常工作,因为 get 魔法对旧样式类不起作用。

    I tried (with minor changes as suggested) the code from Andrew Cooke's answer. (I am running python 2.7).

    The code:

    #!/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)
    

    The result:

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

    With Python prior to 3, make sure you subclass from object which will make the descriptor work correctly as the get magic does not work for old style classes.

    ~没有更多了~
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文