前言:
什么是描述符?
在Python中,描述符是对多个属性访问操作的方法集合的形式化。简而言之,描述符让你能够控制属性的访问、设置、删除等操作。Python的描述符是实现了特定协议的对象,这个协议包括__get__、__set__和__delete__方法。通过这些方法可以在属性访问时定义自定义行为,这为Python的属性访问提供了极高的灵活性。
1 描述符示例
假设我们想要定义一个类属性,这个属性在被访问时会打印日志。我们可以创建一个描述符来做这件事:
"""
# @Time : 2024/4/19
# @Author : Summer
# @File : Descriptor
# @describe:
"""
class LoggedAccess:
def __init__(self, name):
self.name = name
def __get__(self, instance, owner):
value = instance.__dict__[self.name]
print(f"Accessing {self.name}, Value: {value}")
return value
def __set__(self, instance, value):
print(f"Updating {self.name}, New value: {value}")
instance.__dict__[self.name] = value
# 使用描述符
class MyClass:
# 创建描述符实例
my_attr = LoggedAccess("my_attr")
def __init__(self, value):
# 给描述符赋值将调用__set__
self.my_attr = value
# 测试代码
obj = MyClass(10)
# 访问属性,将触发__get__,打印日志 Updating my_attr, New value: 10
x = obj.my_attr
# 更新属性,将触发__set__,打印日志 Accessing my_attr, Value: 10
obj.my_attr = 20 # Updating my_attr, New value: 20
2 描述符使用场景
2.1 类型检查
描述符可以用于强制类型检查,确保属性值符合预期的类型。这在静态语言中很常见,但在Python这样的动态语言中,通过描述符来实现类型检查可以增加代码的健壮性。
#!/usr/bin/env python
# coding=utf-8
"""
# @Time : 2024/4/19
# @Author : Summer
# @File : Descriptor
# @describe:
"""
class TypeCheck:
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}")
instance.__dict__[self.name] = value
class MyClass:
name = TypeCheck("name", str)
age = TypeCheck("age", int)
def __init__(self, name, age):
self.name = name
self.age = age
obj = MyClass("Alice", 30) # Works fine
obj.age = "thirty" # Raises TypeError: Expected <class 'int'>
2.2. 数据校验
描述符也可以用于数据校验,确保设置的属性值满足特定条件或约束。
#!/usr/bin/env python
# coding=utf-8
"""
# @Time : 2024/4/19
# @Author : Summer
# @File : Descriptor
# @describe:
"""
class ValueConstraint:
def __init__(self, name, minval, maxval):
self.name = name
self.minval = minval
self.maxval = maxval
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __set__(self, instance, value):
if not (self.minval <= value <= self.maxval):
raise ValueError(f"Value must be between {self.minval} and {self.maxval}")
instance.__dict__[self.name] = value
class Person:
age = ValueConstraint("age", 0, 150)
def __init__(self, age):
self.age = age # Validates age
obj = Person(35) # Works fine
obj.age = 200 # Raises ValueError: Value must be between 0 and 150
2.3 缓存属性的结果
当计算属性值代价较高时,可以使用描述符来缓存这个值。只有在第一次访问属性时计算值,并在后续访问时从缓存返回。
#!/usr/bin/env python
# coding=utf-8
"""
# @Time : 2024/4/19
# @Author : Summer
# @File : Descriptor
# @describe:
"""
class CachedProperty:
def __init__(self, method):
self.method = method
self.cache_name = f"_{method.__name__}"
def __get__(self, instance, owner):
if hasattr(instance, self.cache_name):
return getattr(instance, self.cache_name)
value = self.method(instance)
setattr(instance, self.cache_name, value)
return value
class ExpensiveObject:
def __init__(self, value):
self.value = value
@CachedProperty
def expensive_computation(self):
# Simulate an expensive computation
print("Computing...")
return self.value * 2
obj = ExpensiveObject(42)
print(obj.expensive_computation) # Computing... \n 84
print(obj.expensive_computation) # 84 (No "Computing..." printed)
3 描述符与property的区别
实现方式:property是Python中的内置装饰器,用于创建可管理的属性,其底层其实是利用了描述符的机制。另一方面,描述符是一个更通用的机制,它让我们通过定义特定的方法(get, set, delete)来控制属性访问。
灵活性:描述符提供了更大的灵活性。你可以定义一个描述符类,这个类可以被多个属性或者不同的类重用。与此相比,每个property通常只用于单个属性。
用例:如果你只想简单地控制一个类的单个属性的访问,property可能是更简单的方式。如果你想要更通用的控制属性访问的方法,或者想要同样的行为应用于多个属性或类,使用描述符可能是更好的选择。
总之,虽然property为属性访问提供了便利的封装,但描述符提供了更为强大和灵活的机制来定制属性的行为,适用于复杂的场景。