← 返回首页
📝

描述符协议深入

📂 python ⏱ 5 min 807 words

描述符协议深入

描述符是Python中实现属性访问的基础机制。理解描述符协议是掌握Python对象模型的关键,它驱动着property、方法、类方法、静态方法等核心特性。

描述符协议基础

描述符是实现了以下任一方法的对象:

class MyDescriptor:
    """自定义描述符示例"""
    
    def __init__(self, name):
        self.name = name
    
    def __get__(self, obj, objtype=None):
        if obj is None:
            # 类访问,返回描述符本身
            return self
        print(f"获取 {self.name}")
        return getattr(obj, f'_{self.name}', None)
    
    def __set__(self, obj, value):
        print(f"设置 {self.name} = {value}")
        setattr(obj, f'_{self.name}', value)
    
    def __delete__(self, obj):
        print(f"删除 {self.name}")
        delattr(obj, f'_{self.name}')

class MyClass:
    x = MyDescriptor('x')
    y = MyDescriptor('y')

obj = MyClass()
obj.x = 10        # 设置 x = 10
print(obj.x)      # 获取 x → 10
del obj.x         # 删除 x

数据描述符与非数据描述符

描述符分为两类,区分标准是是否实现了__set____delete__

class DataDescriptor:
    """数据描述符"""
    
    def __get__(self, obj, objtype=None):
        return "数据描述符"
    
    def __set__(self, obj, value):
        print(f"数据描述符拦截设置: {value}")

class NonDataDescriptor:
    """非数据描述符"""
    
    def __get__(self, obj, objtype=None):
        return "非数据描述符"

class TestClass:
    data_desc = DataDescriptor()
    non_data_desc = NonDataDescriptor()

obj = TestClass()

# 数据描述符优先于实例属性
obj.data_desc = "实例值"  # 数据描述符拦截设置: 实例值
print(obj.data_desc)      # 数据描述符

# 实例属性优先于非数据描述符
obj.non_data_desc = "实例值"
print(obj.non_data_desc)  # 实例值

优先级规则

# 优先级从高到低:
# 1. 数据描述符
# 2. 实例字典
# 3. 非数据描述符

class HighPriority:
    """高优先级描述符"""
    def __get__(self, obj, objtype=None):
        return "高优先级"
    def __set__(self, obj, value):
        pass

class LowPriority:
    """低优先级描述符"""
    def __get__(self, obj, objtype=None):
        return "低优先级"

class PriorityDemo:
    high = HighPriority()
    low = LowPriority()

obj = PriorityDemo()
obj.high = "实例值"
obj.low = "实例值"

print(obj.high)    # 高优先级(数据描述符优先)
print(obj.low)     # 实例值(非数据描述符被覆盖)

实现property

property就是基于数据描述符实现的:

class Property:
    """模拟property实现"""
    
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        self.__doc__ = doc or (fget.__doc__ if fget else None)
    
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("不可读")
        return self.fget(obj)
    
    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("不可设置")
        self.fset(obj, value)
    
    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("不可删除")
        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__)

class Temperature:
    def __init__(self):
        self._celsius = 0
    
    @Property
    def celsius(self):
        """温度(摄氏度)"""
        return self._celsius
    
    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("温度不能低于绝对零度")
        self._celsius = value
    
    @Property
    def fahrenheit(self):
        """温度(华氏度)"""
        return self._celsius * 9/5 + 32
    
    @fahrenheit.setter
    def fahrenheit(self, value):
        self.celsius = (value - 32) * 5/9

t = Temperature()
t.celsius = 25
print(f"摄氏: {t.celsius}°C")   # 摄氏: 25°C
print(f"华氏: {t.fahrenheit}°F") # 华氏: 77.0°F

t.fahrenheit = 212
print(f"摄氏: {t.celsius}°C")   # 摄氏: 100.0°C

方法描述符

Python中的方法就是非数据描述符:

class MethodDescriptor:
    """模拟方法描述符"""
    
    def __init__(self, func):
        self.func = func
    
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self.func
        # 绑定方法
        def bound_method(*args, **kwargs):
            return self.func(obj, *args, **kwargs)
        return bound_method

class Animal:
    def __init__(self, name):
        self.name = name
    
    @MethodDescriptor
    def speak(self):
        return f"{self.name}在叫"

cat = Animal("猫")
dog = Animal("狗")

print(cat.speak())  # 猫在叫
print(dog.speak())  # 狗在叫

# 访问未绑定的方法
print(Animal.speak)  # <function Animal.speak>

类方法和静态方法

class ClassMethodDescriptor:
    """类方法描述符"""
    
    def __init__(self, func):
        self.func = func
    
    def __get__(self, obj, objtype=None):
        if objtype is None:
            objtype = type(obj)
        def class_method(*args, **kwargs):
            return self.func(objtype, *args, **kwargs)
        return class_method

class StaticMethodDescriptor:
    """静态方法描述符"""
    
    def __init__(self, func):
        self.func = func
    
    def __get__(self, obj, objtype=None):
        return self.func

class Date:
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day
    
    @ClassMethodDescriptor
    def from_string(cls, date_str):
        """从字符串创建日期"""
        year, month, day = map(int, date_str.split('-'))
        return cls(year, month, day)
    
    @StaticMethodDescriptor
    def is_valid_date(year, month, day):
        """验证日期是否有效"""
        if not (1 <= month <= 12):
            return False
        days_in_month = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
        if month == 2 and year % 4 == 0 and (year % 100 != 0 or year % 400 == 0):
            days_in_month[2] = 29
        return 1 <= day <= days_in_month[month]

# 使用
date1 = Date.from_string("2024-01-15")
print(f"{date1.year}-{date1.month}-{date1.day}")

print(Date.is_valid_date(2024, 2, 29))  # True(闰年)
print(Date.is_valid_date(2023, 2, 29))  # False

描述符的应用场景

缓存描述符

import time
from functools import wraps

class cached_property:
    """缓存属性描述符"""
    
    def __init__(self, func):
        self.func = func
        self.attrname = None
    
    def __set_name__(self, owner, name):
        self.attrname = name
    
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        
        # 检查缓存
        try:
            cache = obj.__dict__
        except AttributeError:
            cache = {}
        
        if self.attrname in cache:
            return cache[self.attrname]
        
        # 计算并缓存
        value = self.func(obj)
        cache[self.attrname] = value
        return value

class DataAnalysis:
    def __init__(self, data):
        self.data = data
    
    @cached_property
    def mean(self):
        """计算平均值(只计算一次)"""
        print("计算平均值...")
        time.sleep(1)  # 模拟耗时计算
        return sum(self.data) / len(self.data)
    
    @cached_property
    def std(self):
        """计算标准差(只计算一次)"""
        print("计算标准差...")
        time.sleep(1)
        mean = self.mean
        variance = sum((x - mean) ** 2 for x in self.data) / len(self.data)
        return variance ** 0.5

analysis = DataAnalysis([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

print(analysis.mean)  # 计算平均值...
print(analysis.mean)  # 直接返回缓存
print(analysis.std)   # 计算标准差...

验证描述符链

class ValidatorChain:
    """验证器链描述符"""
    
    def __init__(self, *validators):
        self.validators = validators
        self.attrname = None
    
    def __set_name__(self, owner, name):
        self.attrname = name
    
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, f'_{self.attrname}', None)
    
    def __set__(self, obj, value):
        for validator in self.validators:
            value = validator(obj, value, self.attrname)
        setattr(obj, f'_{self.attrname}', value)

def not_empty(obj, value, name):
    if not value:
        raise ValueError(f"{name} 不能为空")
    return value

def min_length(min_len):
    def validator(obj, value, name):
        if len(value) < min_len:
            raise ValueError(f"{name} 长度不能小于 {min_len}")
        return value
    return validator

class User:
    username = ValidatorChain(not_empty, min_length(3))
    email = ValidatorChain(not_empty)
    
    def __init__(self, username, email):
        self.username = username
        self.email = email

try:
    user = User("ab", "test@example.com")  # ValueError
except ValueError as e:
    print(e)  # username 长度不能小于 3

总结

描述符是Python属性访问机制的核心:

理解描述符能让你更好地理解Python的内部机制,也能创建强大的自定义属性行为。