Python - 描述符 - 损坏 class

Python - Descriptor - Broken class

我跟着教程出了docs and an example by fluent Python。在书中,他们教我如何通过 get 避免 AttributeError(例如,当您执行 z = Testing.x 时),我想用 set 方法做一些类似的事情。但看起来,它导致 class 没有错误。

要更具体地说明问题:

  1. 注释掉的行 Testing.x = 1 它会调用 __set__ 方法。
  2. 对于未注释的行 #Testing.x = 1,它 不会 调用 __set__ 方法。

谁能教我为什么会这样?

import abc

class Descriptor:
    def __init__(self):
        cls = self.__class__
        self.storage_name = cls.__name__

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return getattr(instance, self.storage_name)

    def __set__(self, instance, value):
        print(instance,self.storage_name)
        setattr(instance, self.storage_name, value)

class Validator(Descriptor):

    def __set__(self, instance, value):
        value = self.validate(instance, value)
        super().__set__(instance, value)

    @abc.abstractmethod
    def validate(self, instance, value):
        """return validated value or raise ValueError"""

class NonNegative(Validator):
    
    def validate(self, instance, value):
        if value <= 0:
            raise ValueError(f'{value!r} must be > 0')
        return value

class Testing:
    x = NonNegative()
    def __init__(self,number):
        self.x = number

#Testing.x = 1
t = Testing(1)
t.x = 1

Testing.x = 1

将您设置为 Testing 的 class 属性的描述符替换为整数。

由于没有描述符,self.x = ...t.x = ... 只是一个不涉及描述符的赋值。


顺便说一句,您肯定已经注意到您的描述符不再具有真正的 x 属性,并且您不能在没有冲突的情况下使用同一描述符的多个实例?

class Testing:
    x = NonNegative()
    y = NonNegative()

    def __init__(self, number):
        self.x = number


t = Testing(2345)
t.x = 1234
t.y = 5678
print(vars(t))

打印出来

{'NonNegative': 5678}

属性访问一般由object.__getattribute__ and type.__getattribute__ (for instances of type, i.e. classes). When an attribute lookup of the form a.x involves a descriptor as x, then various binding rules处理生效,根据x是:

  1. 实例绑定:如果绑定到对象实例,a.x转化为调用:type(a).__dict__['x'].__get__(a, type(a)).
  2. Class 绑定: 如果绑定到 class,A.x 将转换为调用:A.__dict__['x'].__get__(None, A).
  3. 超级绑定: [...]

对于这个问题的范围,只有 (2) 是相关的。这里,Testing.x 通过 __get__(None, Testing) 调用描述符。现在有人可能会问为什么这样做而不是简单地返回描述符对象本身(就好像它是任何其他对象,比如 int)。此行为对于实现 classmethod decorator. The descriptor HowTo guide provides an example implementation:

很有用
class ClassMethod:
    def __init__(self, f):
        self.f = f

    def __get__(self, obj, cls=None):
        print(f'{obj = }, {cls = }')
        return self.f.__get__(cls, cls)  # simplified version


class Test:
    @ClassMethod
    def func(cls, x):
        pass


Test().func(2)  # call from instance
Test.func(1)  # this requires binding without any instance

我们可以观察到第二种情况 Test.func(1) 没有涉及实例,但是 ClassMethod 描述符仍然可以绑定到 cls.

鉴于 __get__ 用于实例和 class 绑定,有人可能会问为什么 __set__ 不是这种情况。具体来说,对于 x.y = z,如果 y 是数据描述符,为什么它不调用 y.__set__(None, z)?我想原因是没有好的用例,它不必要地使描述符 API 复杂化。无论如何,描述符将如何处理该信息?通常,通过 object.__setattr__type.__setattr__.

class(或类型的 metaclass)来管理属性的设置方式。

因此,为了防止 Testing.x 被用户替换,您可以使用自定义元数据class:

class ProtectDataDescriptors(type):
    def __setattr__(self, name, value):
        if hasattr(getattr(self, name, None), '__set__'):
            raise AttributeError(f'Cannot override data descriptor {name!r}')
        super().__setattr__(name, value)


class Testing(metaclass=ProtectDataDescriptors):
    x = NonNegative()

    def __init__(self, number):
        self.x = number


Testing.x = 1  # now this raises AttributeError

但是,这不是绝对保证,因为用户仍然可以直接使用 type.__setattr__ 来覆盖该属性:

type.__setattr__(Testing, 'x', 1)  # this will bypass ProtectDataDescriptors.__setattr__