Python类的属性描述符使用
Python类的属性描述符基础概念
在Python中,属性描述符是一种特殊的类,它定义了访问类属性时的行为。属性描述符是Python数据模型中的一个强大特性,它允许我们在获取、设置或删除属性时执行自定义代码。
属性描述符类需要实现以下三个特殊方法中的一个或多个:
__get__(self, instance, owner)
:当获取属性值时调用。self
是描述符实例,instance
是拥有该属性的类实例(如果是通过类访问属性,instance
为None
),owner
是拥有该属性的类。__set__(self, instance, value)
:当设置属性值时调用。self
是描述符实例,instance
是拥有该属性的类实例,value
是要设置的值。__delete__(self, instance)
:当删除属性时调用。self
是描述符实例,instance
是拥有该属性的类实例。
如果一个类定义了 __set__
或 __delete__
方法,它被称为数据描述符。如果只定义了 __get__
方法,它被称为非数据描述符。
简单的数据描述符示例
让我们从一个简单的数据描述符示例开始。假设我们有一个类,我们希望其中某个属性的值始终为正数。我们可以使用属性描述符来实现这个逻辑。
class PositiveNumber:
def __init__(self, name):
self.name = name
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __set__(self, instance, value):
if value < 0:
raise ValueError('Value must be positive')
instance.__dict__[self.name] = value
def __delete__(self, instance):
del instance.__dict__[self.name]
class MyClass:
num = PositiveNumber('num')
obj = MyClass()
obj.num = 5
print(obj.num)
try:
obj.num = -1
except ValueError as e:
print(e)
在这个例子中,PositiveNumber
类是一个数据描述符。__init__
方法接受属性的名称。__get__
方法从实例的 __dict__
中获取属性值。__set__
方法检查要设置的值是否为正数,如果不是则抛出 ValueError
。__delete__
方法从实例的 __dict__
中删除属性。
在 MyClass
中,num
属性被定义为 PositiveNumber
描述符的实例。当我们尝试设置 obj.num
为负数时,会触发 ValueError
。
非数据描述符示例
非数据描述符通常用于实现一些只读属性或在获取属性时进行一些计算。
class ReadOnlyAttribute:
def __init__(self, value):
self.value = value
def __get__(self, instance, owner):
return self.value
class MyReadOnlyClass:
read_only = ReadOnlyAttribute(42)
obj = MyReadOnlyClass()
print(obj.read_only)
try:
obj.read_only = 10
except AttributeError as e:
print(e)
在这个例子中,ReadOnlyAttribute
是一个非数据描述符,因为它只定义了 __get__
方法。MyReadOnlyClass
中的 read_only
属性是 ReadOnlyAttribute
的实例,值为 42
。由于它是一个非数据描述符,尝试设置该属性会导致 AttributeError
,因为默认情况下非数据描述符不允许设置值。
描述符查找顺序
理解描述符的查找顺序对于正确使用它们至关重要。Python使用以下顺序查找属性:
- 实例字典:如果属性在实例的
__dict__
中存在,并且该属性不是数据描述符,那么直接返回实例__dict__
中的值。 - 类字典:如果属性不在实例的
__dict__
中,Python会在类的字典中查找。如果找到的是数据描述符(定义了__set__
或__delete__
),则调用其__get__
方法。 - 父类字典:如果在类的字典中没有找到属性,Python会在父类的字典中查找,查找方式与在类字典中查找相同。
- 非数据描述符:如果在实例、类和父类的字典中都没有找到属性,并且类的字典中有一个非数据描述符,那么调用该非数据描述符的
__get__
方法。 __getattr__
方法:如果上述步骤都没有找到属性,Python会调用实例的__getattr__
方法(如果定义了)。
描述符与 property
装饰器的关系
property
是Python中用于创建属性的内置函数,它本质上也是基于描述符实现的。property
函数创建了一个数据描述符,它结合了 __get__
、__set__
和 __delete__
方法。
class MyClassWithProperty:
def __init__(self):
self._value = 0
@property
def value(self):
return self._value
@value.setter
def value(self, new_value):
if new_value < 0:
raise ValueError('Value must be non - negative')
self._value = new_value
@value.deleter
def value(self):
del self._value
obj = MyClassWithProperty()
obj.value = 10
print(obj.value)
try:
obj.value = -5
except ValueError as e:
print(e)
del obj.value
try:
print(obj.value)
except AttributeError as e:
print(e)
在这个例子中,@property
装饰器将 value
方法转换为一个属性的 __get__
方法。@value.setter
装饰器定义了 __set__
方法,@value.deleter
装饰器定义了 __delete__
方法。这与我们之前手动定义描述符类的功能类似,但使用 property
装饰器更加简洁和直观。
描述符在缓存中的应用
描述符在缓存属性值方面非常有用。假设我们有一个计算开销较大的属性,我们希望只计算一次并缓存结果。
class CachedProperty:
def __init__(self, func):
self.func = func
def __get__(self, instance, owner):
if instance is None:
return self
value = self.func(instance)
setattr(instance, self.func.__name__, value)
return value
class ExpensiveCalculation:
def __init__(self):
self.data = [i for i in range(1000000)]
@CachedProperty
def sum_of_data(self):
return sum(self.data)
obj = ExpensiveCalculation()
print(obj.sum_of_data)
print(obj.sum_of_data)
在这个例子中,CachedProperty
是一个描述符。__init__
方法接受一个函数,该函数是我们要缓存结果的计算方法。__get__
方法首先检查是否是通过类访问属性(instance
为 None
),如果是则返回自身。否则,它调用原始函数计算值,然后使用 setattr
将计算结果设置为实例的属性(属性名与原始函数名相同),这样下次访问该属性时,直接从实例的 __dict__
中获取值,而不需要再次计算。
描述符在类型检查中的应用
我们可以使用描述符来实现类型检查,确保属性被设置为正确的类型。
class TypedProperty:
def __init__(self, name, expected_type):
self.name = name
self.expected_type = expected_type
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __set__(self, instance, value):
if not isinstance(value, self.expected_type):
raise TypeError(f'Expected {self.expected_type.__name__}, got {type(value).__name__}')
instance.__dict__[self.name] = value
def __delete__(self, instance):
del instance.__dict__[self.name]
class MyTypedClass:
num = TypedProperty('num', int)
text = TypedProperty('text', str)
obj = MyTypedClass()
obj.num = 10
obj.text = 'Hello'
try:
obj.num = 'ten'
except TypeError as e:
print(e)
try:
obj.text = 10
except TypeError as e:
print(e)
在这个例子中,TypedProperty
描述符确保 num
属性只能设置为 int
类型,text
属性只能设置为 str
类型。如果设置了错误的类型,会抛出 TypeError
。
多重描述符的情况
当一个类中有多个描述符作用于同一个属性时,情况会变得稍微复杂一些。例如,假设我们有一个类,同时有一个数据描述符和一个非数据描述符作用于同一个属性。
class DataDescriptor:
def __init__(self, name):
self.name = name
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __set__(self, instance, value):
instance.__dict__[self.name] = value
class NonDataDescriptor:
def __init__(self, name):
self.name = name
def __get__(self, instance, owner):
return f'Non - data descriptor value for {self.name}'
class MyMultiDescriptorClass:
data_desc = DataDescriptor('data_desc')
non_data_desc = NonDataDescriptor('non_data_desc')
obj = MyMultiDescriptorClass()
obj.data_desc = 'data value'
print(obj.data_desc)
print(obj.non_data_desc)
在这个例子中,data_desc
是数据描述符,non_data_desc
是非数据描述符。由于数据描述符的优先级高于非数据描述符,当我们访问 obj.data_desc
时,会调用数据描述符的 __get__
方法。而访问 obj.non_data_desc
时,会调用非数据描述符的 __get__
方法。
描述符在元类中的应用
描述符在元类中也有重要的应用。元类是用于创建类的类,我们可以在元类中使用描述符来修改类的属性定义。
class DescriptorMeta(type):
def __new__(mcls, name, bases, attrs):
for key, value in attrs.items():
if isinstance(value, Descriptor):
value.name = key
return super().__new__(mcls, name, bases, attrs)
class Descriptor:
def __init__(self):
self.name = None
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __set__(self, instance, value):
instance.__dict__[self.name] = value
class MyMetaClass(metaclass=DescriptorMeta):
my_attr = Descriptor()
obj = MyMetaClass()
obj.my_attr = 42
print(obj.my_attr)
在这个例子中,DescriptorMeta
是一个元类。在 __new__
方法中,它遍历类的属性,如果属性是 Descriptor
的实例,它会设置描述符的 name
属性为属性名。这样在描述符的 __get__
和 __set__
方法中就可以正确使用属性名来访问实例的 __dict__
。
描述符的继承
描述符类本身也可以被继承,从而复用一些通用的逻辑。
class BaseDescriptor:
def __init__(self, name):
self.name = name
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __set__(self, instance, value):
instance.__dict__[self.name] = value
class RestrictedDescriptor(BaseDescriptor):
def __init__(self, name, max_value):
super().__init__(name)
self.max_value = max_value
def __set__(self, instance, value):
if value > self.max_value:
raise ValueError(f'Value must be less than or equal to {self.max_value}')
super().__set__(instance, value)
class MyRestrictedClass:
restricted_attr = RestrictedDescriptor('restricted_attr', 100)
obj = MyRestrictedClass()
obj.restricted_attr = 50
print(obj.restricted_attr)
try:
obj.restricted_attr = 150
except ValueError as e:
print(e)
在这个例子中,RestrictedDescriptor
继承自 BaseDescriptor
。它复用了 BaseDescriptor
的 __get__
和部分 __set__
逻辑,并在 __set__
方法中添加了值的限制逻辑。MyRestrictedClass
使用 RestrictedDescriptor
来定义 restricted_attr
属性,确保该属性的值不超过 100
。
描述符的性能考虑
虽然描述符提供了强大的功能,但在性能敏感的应用中,需要考虑其性能影响。每次访问属性时,如果涉及描述符,会有一定的额外开销,因为要调用描述符的方法。
例如,与直接访问实例属性相比,使用描述符来访问属性会慢一些。
import timeit
class SimpleClass:
def __init__(self):
self.value = 0
class DescriptorClass:
def __init__(self):
self._value = 0
def get_value(self):
return self._value
def set_value(self, value):
self._value = value
value = property(get_value, set_value)
simple_obj = SimpleClass()
desc_obj = DescriptorClass()
simple_time = timeit.timeit(lambda: simple_obj.value, number = 1000000)
desc_time = timeit.timeit(lambda: desc_obj.value, number = 1000000)
print(f'Simple access time: {simple_time}')
print(f'Descriptor access time: {desc_time}')
在这个性能测试中,我们可以看到直接访问实例属性的 SimpleClass
比使用 property
描述符的 DescriptorClass
访问属性要快。因此,在性能关键的代码部分,如果不需要描述符提供的额外功能,应尽量避免使用描述符。
描述符与动态属性
描述符还可以与动态属性相结合。动态属性是在运行时动态添加或修改的属性。
class DynamicDescriptor:
def __init__(self):
self.value = None
def __get__(self, instance, owner):
if self.value is None:
self.value = self.calculate_value()
return self.value
def calculate_value(self):
# 这里可以是复杂的计算逻辑
return 42
class MyDynamicClass:
dynamic_attr = DynamicDescriptor()
obj = MyDynamicClass()
print(obj.dynamic_attr)
print(obj.dynamic_attr)
在这个例子中,DynamicDescriptor
的 __get__
方法在属性值未初始化时调用 calculate_value
方法来计算值,并缓存结果。每次访问 obj.dynamic_attr
时,如果值已经计算过,就直接返回缓存的值。这展示了描述符如何与动态属性的按需计算相结合。
描述符在数据库映射中的应用
在数据库映射库(如SQLAlchemy)中,描述符被广泛应用。例如,假设有一个简单的数据库表映射场景。
class Column:
def __init__(self, name, data_type):
self.name = name
self.data_type = data_type
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __set__(self, instance, value):
if not isinstance(value, self.data_type):
raise TypeError(f'Expected {self.data_type.__name__}, got {type(value).__name__}')
instance.__dict__[self.name] = value
class User:
id = Column('id', int)
name = Column('name', str)
user = User()
user.id = 1
user.name = 'John'
try:
user.id = 'one'
except TypeError as e:
print(e)
在这个简单的数据库映射示例中,Column
类是一个描述符,用于定义表的列。它进行类型检查,确保属性设置为正确的数据类型。User
类使用 Column
描述符来定义 id
和 name
属性,模拟数据库表字段的映射。
通过以上多个方面对Python类的属性描述符进行深入探讨,我们可以看到描述符在Python编程中是一个非常强大且灵活的工具,无论是用于数据验证、缓存、类型检查还是在更复杂的框架如数据库映射中的应用,都能发挥重要作用。同时,我们也了解了描述符的查找顺序、性能影响以及与其他Python特性如 property
装饰器、元类等的关系,这有助于我们在实际编程中更准确、高效地使用描述符。