Python - 描述符 - 损坏 class
Python - Descriptor - Broken class
我跟着教程出了docs and an example by fluent Python。在书中,他们教我如何通过 get 避免 AttributeError
(例如,当您执行 z = Testing.x
时),我想用 set 方法做一些类似的事情。但看起来,它导致 class 没有错误。
要更具体地说明问题:
- 注释掉的行
Testing.x = 1
它会调用 __set__
方法。
- 对于未注释的行
#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
是:
- 实例绑定:如果绑定到对象实例,
a.x
转化为调用:type(a).__dict__['x'].__get__(a, type(a))
.
- Class 绑定: 如果绑定到 class,
A.x
将转换为调用:A.__dict__['x'].__get__(None, A)
.
- 超级绑定: [...]
对于这个问题的范围,只有 (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__
我跟着教程出了docs and an example by fluent Python。在书中,他们教我如何通过 get 避免 AttributeError
(例如,当您执行 z = Testing.x
时),我想用 set 方法做一些类似的事情。但看起来,它导致 class 没有错误。
要更具体地说明问题:
- 注释掉的行
Testing.x = 1
它会调用__set__
方法。 - 对于未注释的行
#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
是:
- 实例绑定:如果绑定到对象实例,
a.x
转化为调用:type(a).__dict__['x'].__get__(a, type(a))
. - Class 绑定: 如果绑定到 class,
A.x
将转换为调用:A.__dict__['x'].__get__(None, A)
. - 超级绑定: [...]
对于这个问题的范围,只有 (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__
.
因此,为了防止 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__