为何让描述符的__get__方法返回另一个描述符?类方法场景解析
首先,你观察到的类方法的这个行为是Python刻意设计的,和子类重写类方法的场景密切相关,下面分两部分解答你的问题:
一、类方法__get__返回对象的__get__:适用场景与子类重写的关系
类方法本身是classmethod类的实例,属于非数据描述符(只有__get__方法)。当你通过类或实例访问类方法时,会调用classmethod.__get__,返回一个绑定的类方法对象(method类型)——而这个绑定对象确实自带__get__方法,它的核心作用是支持类继承中的动态绑定适配。
举个实际例子看作用:
class K: @classmethod def cm(cls): print(f"当前类: {cls.__name__}") class J(K): pass # 暂不重写cm方法 # 获取父类K的绑定类方法 k_bound_cm = K.cm k_bound_cm() # 输出: 当前类: K # 把这个绑定对象重新绑定到子类J j_bound_cm = k_bound_cm.__get__(J) j_bound_cm() # 输出: 当前类: J
如果子类重写了类方法,这个机制依然能正确工作:
class J(K): @classmethod def cm(cls): print(f"子类J的方法: {cls.__name__}") # 父类的绑定方法重新绑定到J时,调用的还是K的cm,但cls参数是J k_bound_cm.__get__(J)() # 输出: 当前类: J # 而直接访问J.cm会调用子类的重写方法 J.cm() # 输出: 子类J的方法: J
这个设计的核心场景是动态编程或框架开发:当你需要在运行时把父类的类方法引用绑定到任意子类上时,不需要重新从子类属性中获取,直接通过绑定对象的__get__就能快速得到适配子类的方法实例,确保cls参数始终是目标子类。
二、描述符的__get__返回另一个描述符的有用场景
让描述符的__get__返回另一个描述符,是Python中实现动态属性逻辑、功能组合、延迟绑定的常用技巧,典型场景包括:
1. 类/静态方法的继承适配
正如前面的classmethod,它的__get__返回的绑定方法本身是一个描述符,确保当子类继承父类的类方法时,访问子类的该方法能自动绑定到子类,而不是固定父类。staticmethod的实现逻辑类似,只是它的绑定对象__get__会忽略实例/类参数,始终返回原始函数。
2. 动态适配不同上下文的属性逻辑
比如你需要一个属性,根据访问它的实例类型返回不同的验证或处理逻辑:
class BaseValidator: def __get__(self, instance, owner): # 根据实例类型返回对应的验证描述符 if isinstance(instance, User): return UserValidator() elif isinstance(instance, Admin): return AdminValidator() else: return DefaultValidator() class UserValidator: def __get__(self, instance, owner): return f"User验证逻辑: {instance.username}" class AdminValidator: def __get__(self, instance, owner): return f"Admin验证逻辑: {instance.admin_id}" class User: validate = BaseValidator() def __init__(self, username): self.username = username class Admin: validate = BaseValidator() def __init__(self, admin_id): self.admin_id = admin_id u = User("alice") print(u.validate) # 输出: User验证逻辑: alice a = Admin("admin001") print(a.validate) # 输出: Admin验证逻辑: admin001
3. 描述符的组合与装饰器链式调用
当你需要给一个方法同时应用多个描述符功能时,可以让外层描述符的__get__返回内层描述符,实现功能叠加。比如自定义一个日志描述符,配合classmethod使用:
class LogDescriptor: def __init__(self, func): self.func = func def __get__(self, instance, owner): # 返回一个classmethod描述符,同时保留日志逻辑 def wrapped(cls, *args, **kwargs): print(f"调用方法: {self.func.__name__}") return self.func(cls, *args, **kwargs) return classmethod(wrapped).__get__(instance, owner) class K: @LogDescriptor def cm(cls, a): return f"K.cm返回: {a}" K.cm(123) # 输出: # 调用方法: cm # K.cm返回: 123
4. 延迟初始化的描述符
对于一些初始化成本较高的描述符(比如需要连接数据库、加载大资源),可以让外层描述符的__get__在第一次访问时才初始化实际的描述符,之后直接返回缓存的实例:
class LazyDescriptor: def __init__(self, descriptor_cls, *args, **kwargs): self.descriptor_cls = descriptor_cls self.args = args self.kwargs = kwargs self._cached = None def __get__(self, instance, owner): if self._cached is None: self._cached = self.descriptor_cls(*self.args, **self.kwargs) # 返回实际的描述符,后续访问会直接走它的__get__ return self._cached.__get__(instance, owner) # 假设HeavyDescriptor是初始化成本很高的描述符 class HeavyDescriptor: def __init__(self): print("HeavyDescriptor初始化中...") # 模拟耗时操作 import time time.sleep(1) def __get__(self, instance, owner): return "HeavyDescriptor返回值" class MyClass: heavy_prop = LazyDescriptor(HeavyDescriptor) obj = MyClass() print(obj.heavy_prop) # 第一次访问会触发初始化 print(obj.heavy_prop) # 第二次直接返回缓存的结果
内容的提问来源于stack exchange,提问作者Toothpick Anemone

