Logo
Published on

54.2.MethodType

Authors
  • avatar
    Name
    xiaobai
    Twitter

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 属性查找优先级

仔细理解下列规则:

  1. 数据描述符(定义了 __set__
  2. 实例字典(instance.__dict__
  3. 非数据描述符(仅有 __get__
  4. 类字典/父类字典
  5. __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

# 定义一个空的 Catclass 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)都基于此。