1. 什么是描述符协议? #
描述符(Descriptor)是能自定义属性访问行为的对象,是掌控Python中调用逻辑、属性校验、缓存、事件通知等“横切关注点”的标准工具。
心智模型:
- 普通属性:存/取时直连实例字典(
instance.__dict__)。 实例字典指的是每个对象自身的属性存储空间,也就是通过obj.attr = ...动态赋值时,属性会写入这里。如果这里存有与描述符同名的属性,会优先返回这里的值(除非是数据描述符)。 - 描述符属性:存/取会间接调用该属性的特殊方法(由类级描述符对象代理)。
常见作用:属性型验证、缓存、延迟计算(lazy)、访问日志、联动通知等。
1.1 协议方法简介 #
__get__(self, instance, owner=None) # 读取属性时执行
__set__(self, instance, value) # 设置属性时执行
__delete__(self, instance) # 删除属性时执行只需实现其中之一,你的类就成为“描述符”。
- 同时有
__get__&__set__:数据描述符(Data Descriptor)(如 property, @property装饰器) - 只有
__get__:非数据描述符(Non-data Descriptor)(如普通函数、类方法、静态方法)
1.1.1 自定义数据描述符 #
# 定义一个数据描述符类
class DataDescriptor:
# 初始化方法,设置初始值
def __init__(self, initial_value=None):
self.value = initial_value
# 获取属性时调用的方法
def __get__(self, instance, owner):
print(f"Getting value: {self.value}")
return self.value
# 设置属性时调用的方法
def __set__(self, instance, value):
print(f"Setting value to: {value}")
self.value = value
# 定义一个包含描述符属性的类
class MyClass:
attr = DataDescriptor("初值")
# 创建MyClass的实例
obj = MyClass()
# 访问attr属性,触发__get__方法,会输出“Getting ...”
print(obj.attr) # 出现“Getting ...”提示
# 设置attr属性,触发__set__方法,会输出“Setting ...”
obj.attr = "新值" # 出现“Setting ...”提示// 定义一个类
class MyClass {
// 构造函数,初始化_attr_value属性为"初值"
constructor() {
this._attr_value = "初值";
}
}
// 在原型上定义属性"attr",设置getter和setter以拦截获取和赋值操作
Object.defineProperty(MyClass.prototype, "attr", {
// getter方法,在获取attr属性时调用
get: function () {
// 输出获取提示,并返回属性值
console.log(`Getting value: ${this._attr_value}`);
return this._attr_value;
},
// setter方法,在设置attr属性时调用
set: function (value) {
// 输出设置提示,并更新属性值
console.log(`Setting value to: ${value}`);
this._attr_value = value;
},
// 属性可配置
configurable: true,
// 属性可枚举
enumerable: true
});
// 创建MyClass类的实例
const obj = new MyClass();
// 访问attr属性,触发getter,会输出获取提示
console.log(obj.attr);
// 设置attr属性,触发setter,会输出设置提示
obj.attr = "新值";1.1.2 非数据描述符 #
class NonDataDescriptor:
def __get__(self, instance, owner):
return "只读虚拟值"
class MyClass:
nd = NonDataDescriptor()
obj = MyClass()
print(obj.nd) # 输出 只读虚拟值
# obj.nd = "whatever" # 会直接在实例字典写值,覆盖描述符效果1.2 属性查找优先级 #
仔细理解下列规则:
- 数据描述符(定义了
__set__) - 实例字典(
instance.__dict__) - 非数据描述符(仅有
__get__) - 类字典/父类字典
__getattr__
数据描述符永远具有最高优先级,非数据描述符会被实例同名属性覆盖。
# __getattr__指的是Python中的一个特殊方法,用于在正常属性查找失败时被调用
class DynamicClass:
def __init__(self):
self.existing_attr = "我是已存在的属性"
def __getattr__(self, name):
"""当属性找不到时调用此方法"""
print(f"__getattr__ 被调用,查找属性: {name}")
return f"动态创建的属性: {name}"
obj = DynamicClass()
# 访问已存在的属性 - 不会触发 __getattr__
print(obj.existing_attr) # 输出: "我是已存在的属性"
# 访问不存在的属性 - 触发 __getattr__
print(obj.any_name) # 输出: "__getattr__ 被调用,查找属性: any_name"
# 输出: "动态创建的属性: any_name"
print(obj.other_attr) # 同样触发 __getattr__1.3 property 就是非数据描述符 #
# 定义一个名为DemoObj的类
class DemoObj:
# 定义初始化方法,接收参数v并赋值给实例变量_v
def __init__(self, v):
self._v = v
# 使用@property装饰器,将v方法变成属性
@property
# 定义v属性的获取方法
def v(self):
# 获取v属性时输出提示信息
print("触发 property __get__")
# 返回实例变量_v的值
return self._v
# 创建DemoObj类的实例,传入66作为初始值
d = DemoObj(66)
# 打印d的v属性,输出属性值,并带有提示信息
print(d.v) # 输出 66(带提示)2. MethodType:运行时让“函数”变成“绑定方法” #
理解了描述符协议后,Python通过 types.MethodType(或 function.__get__)让任意普通函数在运行时绑定为“某个实例的方法”,赋予它自动补全 self/cls 参数的能力。
2.1 基本用法 #
# 导入types模块,用于绑定方法
import types
# 定义一个空的Animal类
class Animal: pass
# 定义一个函数,带有self参数
def say_hi(self):
return f"Hi! I am {self.name}"
# 创建Animal类的实例duck
duck = Animal()
# 给duck实例添加name属性
duck.name = "鸭鸭"
# 将say_hi函数绑定为duck实例的方法
duck.say_hi = types.MethodType(say_hi, duck)
# 调用绑定后的say_hi方法,自动传入self,输出 Hi! I am 鸭鸭
print(duck.say_hi()) # 自动补 self,输出 Hi! I am 鸭鸭等价写法(利用描述符协议 get )
# 导入types模块,用于绑定方法
import types
# 定义一个空的Animal类
class Animal: pass
# 定义一个函数,带有self参数
def say_hi(self):
return f"Hi! I am {self.name}"
# 创建Animal类的实例duck
duck = Animal()
# 给duck实例添加name属性
duck.name = "鸭鸭"
# 将say_hi函数绑定为duck实例的方法
duck.say_hi = say_hi.__get__(duck, Animal)
# 调用绑定后的say_hi方法,自动传入self,输出 Hi! I am 鸭鸭
print(duck.say_hi()) # 自动补 self,输出 Hi! I am 鸭鸭2.2 原理解析 #
types.MethodType(f, obj)≈f.__get__(obj, type(obj))- 作用:返回一个“绑定方法”对象,其
__self__指向 obj,__func__指向 f - 这样调用 say_hi() 时 self 自动是 duck,不用手动传参。
2.3 动态给类/实例添加方法对比 #
# 导入 types 模块用于动态绑定方法
import types
# 定义一个空的 Cat 类
class Cat:
pass
# 定义一个 meow 方法,接收 self 参数
def meow(self):
return f"{self.name} 喵~"
# 创建两个 Cat 的实例 cat1 和 cat2
cat1, cat2 = Cat(), Cat()
# 设置 cat1 和 cat2 的 name 属性
cat1.name, cat2.name = "小白", "小黑"
# 将 meow 方法动态绑定到 cat1 的 speak 属性上
cat1.speak = types.MethodType(meow, cat1)
# 将 meow 方法动态绑定到 cat2 的 speak 属性上
cat2.speak = types.MethodType(meow, cat2)
# 调用 cat1 的 speak 方法并打印结果
print(cat1.speak())
# 调用 cat2 的 speak 方法并打印结果
print(cat2.speak())仅对目标实例生效,其他实例无影响。
3. 函数的 get :方法自动绑定的背后 #
3.1 概念 #
Python的所有函数本身都是非数据描述符,都有一个 __get__ 方法,这让“函数作为类属性”时,能在读取时自动变成“绑定方法”。
# 定义一个函数,需要一个self参数
def intro(self):
return f"我是 {self.name}"
# 定义一个空类P
class P:
pass
# 创建P类的实例对象
p = P()
# 动态为p实例添加name属性
p.name = "张三"
# 将函数intro直接赋值给p的intro属性(此时只是一个函数引用,还未绑定self)
p.intro = intro # 只是函数引用
# # 下面这一行如果解除注释会报错:TypeError: 缺少self参数
# p.intro() # TypeError: 缺少 self
# 使用__get__方法把函数intro绑定到p实例上,变为一个方法
p.intro = intro.__get__(p, P) # 手动绑定
# 调用绑定后的方法,输出“我是 张三”
print(p.intro())3.2 get #
# 导入types模块,用于方法绑定
from types import MethodType
# 定义一个函数,参数为self
def intro(self):
return f"我是 {self.name}"
# 定义一个空类P
class P:
pass
# 创建P类的实例对象
p = P()
# 为实例p动态添加name属性,赋值为"张三"
p.name = "张三"
# 将函数intro赋值给p的intro属性,仅为函数引用
p.intro = intro # 只是函数引用
# 下面这一行如果取消注释会报错:TypeError: 缺少self参数
# p.intro() # TypeError: 缺少 self
# 定义__get__方法,用于函数绑定为方法
def __get__(self, instance, owner):
# 如果instance为None,返回函数本身
if instance is None:
return self
# 否则返回一个types.MethodType对象,将函数绑定到instance上
return MethodType(self, instance)
# 将__get__方法绑定为intro的__get__属性
intro.__get__ = __get__
# 使用intro的__get__方法将intro绑定到实例p上,变为方法
p.intro = intro.__get__(intro, p, P) # 手动绑定
# 调用绑定后的方法,输出“我是 张三”
print(p.intro())3.3 MethodType #
# 导入types模块,用于方法绑定
#from types import MethodType
# 定义一个函数,参数为self
def intro(self):
return f"我是 {self.name}"
# 定义一个空类P
class P:
pass
# 创建P类的实例对象
p = P()
# 为实例p动态添加name属性,赋值为"张三"
p.name = "张三"
# 将函数intro赋值给p的intro属性,仅为函数引用
p.intro = intro # 只是函数引用
# 下面这一行如果取消注释会报错:TypeError: 缺少self参数
# p.intro() # TypeError: 缺少 self
# 自定义一个函数,将函数与实例绑定为方法
def MethodType(func, instance):
# 定义绑定后实际调用的方法
def bound(*args, **kwargs):
return func(instance, *args, **kwargs)
return bound
# 定义__get__方法,用于函数绑定为方法
def __get__(self, instance, owner):
# 如果instance为None,返回函数本身
if instance is None:
return self
# 否则返回一个types.MethodType对象,将函数绑定到instance上
return MethodType(self, instance)
# 将__get__方法绑定为intro的__get__属性
intro.__get__ = __get__
# 使用intro的__get__方法将intro绑定到实例p上,变为方法
p.intro = intro.__get__(intro, p, P) # 手动绑定
# 调用绑定后的方法,输出“我是 张三”
print(p.intro())3.3 类定义方法的本质 #
# 定义一个名为People的类
class People:
# 定义实例方法hello,返回向name属性打招呼的字符串
def hello(self):
return f"Hi, {self.name}!"
# 创建People类的实例p
p = People()
# 给p实例动态添加name属性,赋值为"李四"
p.name="李四"
# 调用p的hello方法,并打印结果
# 实际等价于:People.hello.__get__(p, People)()
print(p.hello()) # 其实是等价于:People.hello.__get__(p, People)()3.4 综合理解 #
function.__get__是最核心的自动装配机制:让函数+实例→绑定方法。types.MethodType提供了自己“动态装配”的能力。- 一切“方法”的魔法(包括装饰器、@classmethod/@staticmethod)都基于此。