了解 __get__ 和 __set__ 以及 Python 描述符

Posted

技术标签:

【中文标题】了解 __get__ 和 __set__ 以及 Python 描述符【英文标题】:Understanding __get__ and __set__ and Python descriptors 【发布时间】:2011-04-17 11:07:46 【问题描述】:

我正在尝试了解 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()

    为什么需要描述符类?

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

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

【问题讨论】:

【参考方案1】:

描述符是 Python 的 property 类型是如何实现的。描述符简单地实现__get____set__ 等,然后在其定义中添加到另一个类(就像您在上面对温度类所做的那样)。例如:

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

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

__get__ 中的instance 是该类的实例(如上所示,__get__ 将接收temp,而owner 是具有描述符的类(因此它将是Temperature)。

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

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

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

【讨论】:

selfinstance有什么区别? 'instance' 可以是任何类的实例,self 是同一个类的实例。 @LemmaPrism self 是描述符实例,instance 是描述符所在的类的实例(如果已实例化)(instance.__class__ is owner)。 Temperature.celsius 根据代码celsius = Celsius() 给出值0.0。描述符摄氏度被调用,因此它的实例具有分配给温度类属性摄氏度的初始值0.0【参考方案2】:

为什么需要描述符类?

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

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

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

这里的instanceowner 是什么? (在__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 类中),但这说明了可以做什么...

【讨论】:

转换错误:应该是 C=5(F−32)/9, F=32+9C/5。 确保你有一个温度对象。做以下事情会搞砸。 t1 = Temperature(190) print t1.celsius t1.celsius = 100 print t1.fahrenheit 现在,当您检查 t.celcius 和 t.fahrenheit 时,它们也会被修改。 t.celcius 是 115,t.fahrenheit 是 32。这显然是错误的。 @埃里克 @IshanBhatt:我认为这是因为上面 musiphil 指出的错误。另外,这不是我的答案【参考方案3】:

我尝试了(按照建议进行了细微更改)Andrew Cooke 回答中的代码。 (我正在运行 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 魔法不适用于旧式类。

【讨论】:

描述符仅适用于新样式类。对于 python 2.x,这意味着从“object”派生你的类,这是 Python 3 中的默认设置。【参考方案4】:

我试图了解 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)的结果。

函数/方法、绑定方法、propertyclassmethodstaticmethod 都使用这些特殊方法来控制如何通过点分查找来访问它们。

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

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

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

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

但是了解描述符的工作原理可以让人们更加自信地掌握 Python。

深入了解:什么是描述符?

描述符是具有以下任何方法(__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 是其类包含描述符对象实例的实例。 selfdescriptor 的实例(可能只是 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))

Data Descriptor 有一个__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 一般功能

非数据描述符

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

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

点查找顺序

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

obj_instance.attribute
    首先看一下属性是否是实例类上的Data-Descriptor, 如果不是,则查看属性是否在obj_instance__dict__中,然后 它最终退回到非数据描述符。

这种查找顺序的结果是,像函数/方法这样的非数据描述符可以是overridden by instances。

回顾和后续步骤

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

您的描述符确保您始终拥有 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)
    这里的实例和所有者是什么? (在 get 中)。这些参数的用途是什么?

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 程序员实现此结果的预期方式是使用 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)

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

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

结论

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

同样,您将如何使用问题的示例?我希望你不会。我希望您从我的第一个建议(一个简单的类属性)开始,如果您觉得有必要,请继续执行第二个建议(属性装饰器)。

【讨论】:

很好,我从这个答案中学到了最多的东西(当然也从其他人那里学到了)。关于此陈述的问题“大多数有经验的 Python 程序员将实现此结果的预期方式......”。您在语句之前和之后定义的 Temperature 类是相同的。我错过了你在这里得到的东西吗? @YoloVoe 不,没错,我添加了一些括号内的措辞来强调这是对上述内容的重复。 这是一个了不起的答案。我需要再读几遍,但我觉得我对 Python 的理解提高了几个档次【参考方案5】:

为什么需要描述符类?

灵感来自于 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 类来进行更常见的验证

【讨论】:

有趣的用例,因为它展示了如何使用描述符与多个用户实例进行交互。我最初不明白重点:必须在类命名空间中创建具有描述符的属性(例如weight = Quantity(),但必须在实例命名空间中设置值,仅使用self(例如self.weight = 4),否则属性将被重新绑定到新值并且描述符将被丢弃。很好! 我无法理解一件事。您将 weight = Quantity() 定义为类变量,其 __get____set__ 正在处理实例变量。怎么样? @Technocrat 我认为您的误解在于理解权重可以不是类属性,如果 <__class__.__dict___> 实现例如<__set__> 即是一个数据描述符。如果是,它将指向您的 对象的 <__get__> -方法。为此,您的实例属性 和类变量 的名称必须匹配。【参考方案6】:

你会看到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__)

【讨论】:

这不回答问题或提供任何有用的信息。【参考方案7】:

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

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

这表示在Classinstance 上查找属性foobar

这里有两个重要的条件:

如果instance 的类有一个属性名条目,并且它有__get____set__。 如果instanceno 条目作为属性名称,但类有一个并且它有__get__

这就是描述符的作用:

数据描述符,同时具有__get____set__非数据描述符,只有__get__

在这两种情况下,返回值都通过__get__ 调用,实例作为第一个参数,类作为第二个参数。

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

让我们转到您的具体问题:

为什么需要描述符类?

在大多数情况下,您不需要编写描述符类!但是,您可能是一个非常普通的最终用户。例如函数。函数是描述符,这就是函数可以用作方法的方式,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。忽略gettersetterdeleterproperty 描述符是(来自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)

因为它是一个数据描述符,每当您查找 property 的“名称”时就会调用它,它只是委托给用 @property@name.setter@name.deleter(如果存在)装饰的函数。

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

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

另一个例子是class property。

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

或“惰性属性”:

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

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

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

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

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

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

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

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

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

您的示例基本上是一个属性,它只允许可以转换为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

这就是为什么描述符通常使用第二个参数 (instance) 来存储值以避免共享它。然而,在某些情况下,可能需要在实例之间共享一个值(尽管我现在想不出一个场景)。然而,对于温度等级的摄氏属性几乎没有任何意义......除了可能是纯粹的学术练习。

【讨论】:

不确定是否应该将图形的透明背景作为一个错误报告给***。 @Tshirtman 我认为这是图像本身的问题。它不是完全透明的......我把它从博客文章中拿走了,不知道如何用适当的透明背景重新创建它。太糟糕了,深色背景看起来很奇怪:(【参考方案8】:

易于理解(举例)__get__ &amp; __set__ &amp; __call__在课堂上的解释,Owner, Instance是什么?

在潜入之前需要注意的几点:

    __get__ __set__ 被称为类的描述符以工作/保存其内部属性,即:__name__(类/所有者类的名称)、变量 - __dict__ 等。我稍后会解释什么是所有者 描述符更常用于设计模式,例如,与装饰器一起使用(将事物抽象出来)。您可以认为它更常用于软件架构设计,以减少冗余并提高可读性(似乎具有讽刺意味)。从而遵守 SOLID 和 DRY 原则。 如果您设计的软件不应该遵守 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__ 就会被实例化),然后分配了参数:instance: "first", arg1: "second",最后一个 arg2 被省略,这将打印出错误:TypeError: __call__() missing 1 required positional argument: 'arg2'

2。如何解决?

由于__call__instance 作为第一个参数(实例、arg1、arg2),但instance 是什么?

Instance 是调用描述符类(Method) 的主类(MyClass) 的实例。那么instance = MyClass()instance 那么owner 是谁呢?持有描述符类的类 - MyClass,但是,我们的描述符类 (Method) 中没有方法可以将其识别为 instance。这就是我们需要__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:&lt;__main__.MyClass object at 0x7fb7dd9eab90&gt; &lt;class '__main__.MyClass'&gt;

这意味着实例:MyClass 的对象,即instanceOwner 本身就是MyClass

3。 __set__解释:

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

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

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

或者您可以使用vars(instance.method) 检查__dict__ 的值 打印:'name': 'Internal call', 'value': 'value' 我希望现在一切都清楚了:)

【讨论】:

以上是关于了解 __get__ 和 __set__ 以及 Python 描述符的主要内容,如果未能解决你的问题,请参考以下文章

python __set__ __get__ 等解释

(实用篇)浅谈PHP拦截器之__set()与__get()的理解与使用方法

描述符(\_\_get\_\_和\_\_set\_\_和\_\_delete\_\_)

php __set()和__get()的具体用法,举例说明,谢~

php 中__set()和__get()的具体用法

PHP魔术法__set和__get