Python通过描述器协议封装普通函数 实例访问自动补全self

【python】class里定义的函数是怎么变成方法的?函数里的self有什么特殊意义么? — 码农高天

码农高天

类内定义的函数本质上均为普通函数对象,仅在通过实例访问触发描述器协议时才会封装为自动填充首位参数的方法对象,若直接动态挂载到实例字典则会因绕过该协议而报错。

AI-assisted summary of 【python】class里定义的函数是怎么变成方法的?函数里的self有什么特殊意义么? — original source. Check the original for full context.

Source published:

Key ideas

  • Python 类中定义的函数本质上只是普通的函数对象,之所以通过实例访问时会变成“绑定方法”(Bound Method),核心驱动力是函数对象本身实现了描述器协议(Descriptor Protocol)。 (5:00)
  • CPython 源码中,通过实例访问方法会触发描述器的 get 函数并调用 PyMethod_New 创建 method 对象;调用该对象时底层通过 method_vectorcall 自动将绑定的实例注入参数列表首位。 (5:00)
  • 动态给类添加函数(A.f = f)可在实例访问时正常转换为绑定方法;但若直接赋值给实例(o.f = f),因直接命中实例字典而不触发描述器,调用时必须显式手动传入实例参数。 (8:31)
  • 参数名 self 在 Python 语法和底层实现中没有任何特殊魔力,仅仅是社区约定俗成用于占位第一个位置参数的普通标识符。 (8:31)

Detailed notes

## Background 该视频由技术博主“码农高天”于 2024 年 2 月 3 日发布在 YouTube 平台。视频围绕 Python 面向对象编程中的经典疑问展开,系统剖析了类中定义的函数如何转变为绑定方法,以及为什么定义时存在的 self 参数在调用时无需显式传递的底层运作机制。

## TL;DR 视频从普通函数(function)与绑定方法(bound method)的对比切入,揭示了 Python 中方法调用的核心驱动力是函数对象自带的描述器(Descriptor)协议。高天结合 CPython 底层源码剖析指出,当通过实例访问属性时,解释器会触发函数的描述器 get 方法并调用 PyMethod_New 生成绑定当前实例的方法对象,在执行时底层借由 method_vectorcall 自动将实例塞入首个参数位置。视频进一步通过动态给类或实例赋值函数、自定义描述器等边界场景验证了属性查找与描述器触发规则,并最终阐明 self 纯粹是社区约定的占位符,免传实参的机制完全归功于描述器的暗中转换。

## Atmosphere & style 视频整体呈现出严谨扎实而又通俗透彻的硬核技术研读风格,讲解节奏平稳流畅、条理分明。高天以日常代码中的直观现象作为引子,层层剥茧至 CPython 源码与底层实现,并借助多组对比实验现场击破认知误区,营造出一种拨开迷雾、让底层黑盒彻底透明化的技术爽感与启发感。

## Bulletin - [05:00](https://www.youtube.com/watch?v=Le-vZ6ueh0E&t=300s) Python 类中定义的函数本质上只是普通的函数对象,之所以通过实例访问时会变成“绑定方法”(Bound Method),核心驱动力是函数对象本身实现了描述器协议(Descriptor Protocol)。 - [05:00](https://www.youtube.com/watch?v=Le-vZ6ueh0E&t=300s) CPython 源码中,通过实例访问方法会触发描述器的 get 函数并调用 PyMethod_New 创建 method 对象;调用该对象时底层通过 method_vectorcall 自动将绑定的实例注入参数列表首位。 - [08:31](https://www.youtube.com/watch?v=Le-vZ6ueh0E&t=511s) 动态给类添加函数(A.f = f)可在实例访问时正常转换为绑定方法;但若直接赋值给实例(o.f = f),因直接命中实例字典而不触发描述器,调用时必须显式手动传入实例参数。 - [08:31](https://www.youtube.com/watch?v=Le-vZ6ueh0E&t=511s) 参数名 self 在 Python 语法和底层实现中没有任何特殊魔力,仅仅是社区约定俗成用于占位第一个位置参数的普通标识符。

## Key takeaways - [00:00](https://www.youtube.com/watch?v=Le-vZ6ueh0E&t=0s) 类定义在执行时只是在局部命名空间中运行,类中定义的函数作为普通 function object 保存在类的 __dict__ 字典中。 - [00:00](https://www.youtube.com/watch?v=Le-vZ6ueh0E&t=0s) 直接通过类访问(如 A.f)得到的是普通函数,而通过实例对象访问(如 o.f)得到的则是绑定方法(Bound Method)。 - [00:00](https://www.youtube.com/watch?v=Le-vZ6ueh0E&t=0s) Bound Method 对象内部记住了所绑定的具体实例,即使将其赋值给独立变量脱离原对象调用,也能自动访问实例属性且无需传入 self。 - [05:00](https://www.youtube.com/watch?v=Le-vZ6ueh0E&t=300s) 函数转为绑定方法的核心机制在于函数对象自带描述器协议,其实现了 CPython 底层的 __get__ 方法。 - [05:00](https://www.youtube.com/watch?v=Le-vZ6ueh0E&t=300s) CPython 源码中,函数对象的描述器 get 函数通过调用 PyMethod_New(func, obj) 实例化出一个持有原函数与目标实例的 method 对象。 - [05:00](https://www.youtube.com/watch?v=Le-vZ6ueh0E&t=300s) 在底层 method_vectorcall 调用逻辑中,解释器会自动将持有的实例作为首个位置参数拼入参数列表,再调用原始函数。 - [05:00](https://www.youtube.com/watch?v=Le-vZ6ueh0E&t=300s) 基于属性查找协议(LOAD_ATTR),访问 o.f 时若在实例自身的 __dict__ 找不到,会向上查找到类 A 并识别出描述器,从而触发 __get__ 转换。 - [08:31](https://www.youtube.com/watch?v=Le-vZ6ueh0E&t=511s) 动态将函数赋值给类(A.f = f)时,实例查找依然能回溯到类并触发描述器,表现与常规类方法一致。 - [08:31](https://www.youtube.com/watch?v=Le-vZ6ueh0E&t=511s) 动态将函数直接挂载到实例(o.f = f)时,函数被直接存入实例的 __dict__,查找过程不会触发描述器,o.f 依旧是普通函数。 - [08:31](https://www.youtube.com/watch?v=Le-vZ6ueh0E&t=511s) 直接调用实例字典中的普通函数若不显式传递自身(如 o.f("hello")),会因缺少必要位置参数而直接触发 TypeError。 - [08:31](https://www.youtube.com/watch?v=Le-vZ6ueh0E&t=511s) 若自定义描述器的 __get__ 返回一个普通函数对象,Python 不会对该返回值递归触发描述器,返回给调用者的依然是普通函数。 - [08:31](https://www.youtube.com/watch?v=Le-vZ6ueh0E&t=511s) 方法中 self 的名称完全可由开发者自定义替换,免显式传参的全过程纯粹是由描述器与底层参数补全机制协作完成的。

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=Le-vZ6ueh0E&t=0s) 类方法中的 `self` 谜题与绑定的方法(Bound Method)

在日常 Python 编程中,大家在类(`class`)内部定义方法(method)时,普遍习惯将第一个参数命名为 `self`。然而真正实例化一个对象(object)并调用该方法时,开发者却只需要传递 `self` 之后的实参。例如在类 `A` 中定义函数 `f(self, data)`,实例化 `o = A()` 之后调用 `o.f("hello")`,代码便会正常打印出 `"hello"`。进一步地,如果给实例对象 `o` 动态绑定一个属性 `o.name = "Bob"`,并将函数 `f` 修改为先打印 `self.name` 再打印 `data`,调用 `o.f("hello")` 就会依次输出 `"Bob"` 和 `"hello"`。

许多人知其然不知其所以然:这个 `self` 到底去哪了?从类的底层创建机制来看,类在定义时实际上只是在一个局部命名空间中执行代码块,随后将产生的局部变量全部存入该类的 `__dict__` 中。因此,类 `A` 中保存的 `f` 本质上只是一个普普通通的函数(function),它明确要求接收两个参数(`self` 和 `data`)。

关键的分水岭在于通过类访问还是通过对象实例访问。如果分别打印 `A.f` 与 `o.f`: - `A.f` 显示为一个非常普通的函数(`<function A.f at...>`); - `o.f` 则变成了一个绑定方法(`<bound method A.f of <__main__.A object at...>>`)。

所谓的 bound method,本质上是一个绑定了实例对象 `o` 的方法对象(method object)。为了证明这一点,可以将 `o.f` 赋值给一个独立变量 `my_func = o.f`,随后直接调用 `my_func("hello")`。此时调用过程表面上已经与变量 `o` 无关,但调用时依然只需传入一个参数,且依然能准确打印出 `o.name`(即 `"Bob"`)。这证明 `o.f` 返回的并不是原先定义在类中的那个普通函数,而是一个记住了所绑定对象的全新 method 对象;当调用该 method 时,它会在底层自动把自己绑定的对象塞入第一个参数位置。码农高天指出:“换言之 O.f 并不是你在 class A 定义的这个 F 了。”

```mermaid flowchart TD accTitle: 普通函数转化为绑定方法的流程 accDescr: 展示了通过类访问函数与通过实例访问函数时,函数如何通过描述器转换为绑定的方法对象。

subgraph Definition["定义阶段"] A["Class A 定义 f(self, data)"] --> DictA["A.__dict__['f'] = function object"] end

subgraph Access["属性访问阶段"] DictA --> DirectAccess["通过类访问:A.f"] DirectAccess --> FuncOut["返回普通 Function<br/>调用需显式传参:A.f(o, 'hello')"]

DictA --> InstanceAccess["通过实例访问:o.f"] InstanceAccess --> DescCall["触发 Function 的 __get__(o, A)"] DescCall --> MethodOut["返回 Bound Method<br/>绑定实例 o 到 self"] end

subgraph Execution["执行阶段"] MethodOut --> Invoke["调用 o.f('hello')<br/>(底层通过 method_vectorcall 补全 self)"] end ```

---

[05:00](https://www.youtube.com/watch?v=Le-vZ6ueh0E&t=300s) 描述器机制(Descriptor)与 CPython 源码剖析

同一个属性名在类和对象上访问会产生截然不同的对象类型,这一行为背后的核心驱动力就是描述器(Descriptor)机制。在 Python 中,所有定义的函数本质上都是一个函数对象(function object),而函数对象本身实现了解释器底层的描述器协议,自带 `__get__` 方法(在 CPython 源码中对应描述器的 get 函数)。

在 CPython 源码实现中,函数对象的 descriptor get 函数极其简单直白:它直接调用并返回了 `PyMethod_New(func, obj)`。这个函数会创建一个专门的 `method` 对象,该对象内部持有两个核心要素: 1. 原始函数对象 `func`; 2. 触发调用的实例对象 `object`(即方法内的 `self`)。

当这个 `method` 对象被调用时,它会走底层预设的调用函数 `method_vectorcall`。在该调用逻辑中,解释器会先获取绑定的 `self` 和原始 `func`,在各种分支处理下,“在各种 case 里面 它都会把 self 给放到 argument 的第一个”,随后将用户实际传入的其余参数紧随其后拼接成一个新的参数列表,最终以这个合并后的参数列表去调用原始的 `func`。这就是一个 function 转化为 method 的核心底层机制。

结合属性查找协议(`LOAD_ATTR`),整个查找与转换流程如下: - 当定义 `class A` 时,生成的普通 function object 存入了 `A.__dict__`。如果直接执行 `A.f(o, "hello")`,用户必须手工传入实例 `o` 与字符串参数; - 当实例化 `o = A()` 并执行 `o.f` 时,Python 先在 `o.__dict__` 中查找名为 `f` 的属性。由于找不到,查找逻辑顺着 MRO 链向上查找其类型对象 `A`; - 在 `A` 的命名空间中找到属性 `f` 后,解释器发现它是一个普通的 function object,但该 object 拥有描述器 get 函数(实现了 `__get__`); - 解释器因而不会直接返回该函数,而是调用该函数的 descriptor get 方法(传入实例 `o` 与类型 `A`),从而生成并返回绑定了实例 `o` 的 method object; - 最终,调用 `o.f("hello")` 时,method 对象内部自动把 `o` 补齐为第一个参数,实现了免显式传递 `self` 的体验。

---

[08:31](https://www.youtube.com/watch?v=Le-vZ6ueh0E&t=511s) 动态绑定与自定义描述器的边界情况

理解上述机制具有极强的实际应用价值,尤其是在需要动态为类或实例添加方法与函数时。许多人在动态增加函数时往往会困惑:究竟要不要在函数中写 `self`?调用时到底需不需要显式传入 `self`?

#### 情况一:动态添加到类对象 如果将一个普通的独立函数 `f(self, data)` 赋值给类本身: ```python A.f = f ``` 当后续新建的实例 `o` 调用 `o.f` 时,由于实例自身的字典中没有 `f`,查找逻辑会落到类 `A` 上并触发函数对象的描述器协议。该函数会被自动转化为 bound method,因此调用时无需显式传 `self`,使用 `o.f("hello")` 即可。

#### 情况二:动态添加到实例对象 如果将函数直接赋值到具体实例对象上: ```python o.f = f ``` 此时函数对象被直接写进了 `o.__dict__` 字典中。码农高天强调:“由于这个函数定义到了对象上 对象的 dict 里面 当你再使用 O.f 的时候它就不会再调用你拿到的这个 function object 的 descriptor get 函数了你拿到的这个 O.f 就是一个完整的 function object”。因此,如果直接调用 `o.f("hello")`,Python 会直接抛出缺少必要位置参数的 TypeError 异常(`missing required positional argument`)。此时正确的调用方式必须手动显式传入实例自身:`o.f(o, "hello")`。

#### 情况三:自定义描述器返回函数 若在类 `A` 中定义一个自定义的描述器对象,其 `__get__` 函数在被调用时直接返回一个普通的函数对象 `f`: ```python class FuncDescriptor: def __get__(self, instance, owner): return f # 直接返回普通函数

class A: f = FuncDescriptor() ``` 在此场景下调用 `o.f`,查找路径如下: 1. 实例 `o` 本身字典无 `f`,在类 `A` 中找到 `f`; 2. 类 `A` 中的 `f` 是自定义描述器实例,解释器检测到其具有 `__get__`,因而执行其 `__get__` 方法; 3. 自定义描述器的 `__get__` 直接返回了普通的 function object; 4. 解释器直接将该返回值交给调用方,而**不会**针对返回的函数对象再次递归触发描述器机制。

因此,`o.f` 拿到的是纯粹的普通函数而不是 bound method,调用时同样必须显式传参:`o.f(o, "hello")`。

| 绑定/定义场景 | `o.f` 实际拿到的对象 | 正确调用语法 | 底层原因 | |:--- |:--- |:--- |:--- | | **常规类内定义方法** (`class A: def f(self):...`) | `Bound Method` | `o.f("hello")` | 类字典中的函数在实例查找时触发自身的描述器,封装为 bound method | | **动态赋值给类** (`A.f = f`) | `Bound Method` | `o.f("hello")` | 查找回溯到类对象,触发函数自带的描述器逻辑 | | **动态赋值给实例** (`o.f = f`) | `Function` | `o.f(o, "hello")` | 直接命中实例自身的 `__dict__`,不触发描述器机制 | | **自定义描述器返回普通函数** | `Function` | `o.f(o, "hello")` | 自定义描述器运行后直接吐出普通函数,底层不会二次触发该函数的描述器 |

码农高天总结道,在类中定义常规方法时产生的本质上就是一个普通的函数对象,参数名 `self` 纯粹只是社区约定俗成的占位符,“它只是把第一个 argument 这个位置占了个坑而已 你叫它什么对于 Python 来说是一样的”。开发者在面向对象调用中感受到的所谓“方法绑定”,实际上全靠访问实例属性时由函数对象自带的 descriptor get 函数暗中协助,构造出绑定实例的 method 对象,从而免去了在参数列表中手动传递实例自身的繁琐操作。

## Mentioned Python, CPython, 码农高天, PyMethod_New, method_vectorcall

## Video description 这期视频讲一下大家常用但是未必知道背后原理的class里的method机制。明明定义的函数里有个self参数,怎么使用的时候不需要给呢?看完视频,全都明白了!

[Source](https://www.youtube.com/watch?v=Le-vZ6ueh0E)

中文简报

Python通过描述器协议封装普通函数 实例访问自动补全self 类内定义的函数本质上均为普通函数对象,仅在通过实例访问触发描述器协议时才会封装为自动填充首位参数的方法对象,若直接动态挂载到实例字典则会因绕过该协议而报错。 视频从普通函数(function)与绑定方法(bound method)的对比切入,揭示了 Python 中方法调用的核心驱动力是函数对象自带的描述器(Descriptor)协议。高天结合 CPython 底层源码剖析指出,当通过实例访问属性时,解释器会触发函数的描述器 get 方法并调用 PyMethod_New 生成绑定当前实例的方法对象,在执行时底层借由 method_vectorcall 自动将实例塞入首个参数位置。视频进一步通过动态给类或实例赋值函数、自定义描述器等边界场景验证了属性查找与描述器触发规则,并最终阐明 self 纯粹是社区约定的占位符,免传实参的机制完全归功于描述器的暗中转换。 Python 类中定义的函数本质上只是普通的函数对象,之所以通过实例访问时会变成“绑定方法”(Bound Method),核心驱动力是函数对象本身实现了描述器协议(Descriptor Protocol)。 (5:00) CPython 源码中,通过实例访问方法会触发描述器的 get 函数并调用 PyMethod_New 创建 method 对象;调用该对象时底层通过 method_vectorcall 自动将绑定的实例注入参数列表首位。 (5:00) 动态给类添加函数(A.f = f)可在实例访问时正常转换为绑定方法;但若直接赋值给实例(o.f = f),因直接命中实例字典而不触发描述器,调用时必须显式手动传入实例参数。 (8:31) 参数名 self 在 Python 语法和底层实现中没有任何特殊魔力,仅仅是社区约定俗成用于占位第一个位置参数的普通标识符。 (8:31)

Reading summary

码农高天指出,Python 类中定义的函数本质上只是保存在类字典中的普通函数对象,其转变为绑定方法(bound method)并自动注入实例的机制,完全依赖于底层函数对象自带的描述器(Descriptor)协议。 普通函数与绑定方法的分水岭 (0:00) 在类定义阶段,类内部的代码块在局部命名空间中执行,生成的函数作为普通函数对象(function object)存入类的 `__dict__` 中,其本质上需要接收包括 `self` 在内的完整参数。通过类直接访问属性(如 `A.f`)时,拿到的依然是该普通函数,调用必须手动传入实例(如 `A.f(o, 'hello')`)。但若通过实例访问(如 `o.f`),返回的则是一个绑定了具体实例的全新方法对象(bound method)。即使将 `o.f` 赋值给独立变量,该方法在调用时依然会自动将其绑定的实例补齐至首位参数,从而呈现出无需开发者显式传参的效果。 描述器协议与 CPython 源码实现 (5:00) 这一转换行为的核心动力在于描述器机制。Python 中的所有函数对象本身都实现了描述器协议的 `__get__` 方法。当通过实例对象触发属性查找(`LOAD_ATTR`)时,由于实例自身字典内缺少该属性,查找逻辑会沿 MRO 链上溯至类对象;在类命名空间中命中函数后,解释器检测到其具备描述器 get 函数,便不会直接返回函数,而是调用其 `__get__` 方法生成并返回一个 `PyMethod_New` 创建的 method 对象。 在 CPython 源码层面,该 method 对象同时持有原始函数 `func` 与触发调用的实例 `object`。当该方法被执行时,底层调用函数 `method_vectorcall` 介入处理,在各类分支中均会自动将绑定的实例放置在参数列表的第一个位置,随后拼接用户实际传入的其余参数并转发给底层原始函数,完成了无需显式传递首参数的完整闭环。 动态属性绑定的边界与异常表现 (8:31) 深入理解描述器协议能够直接解释动态赋值场景下的行为差异。若将普通函数动态挂载到类对象(`A.f = f`),由于查找仍会回溯至类,后续实例访问依然会触发描述器转换为 bound method,调用体验与原生方法一致。然而,若将函数直接赋值给具体实例对象(`o.f = f`),函数对象将直接写入该实例自身的 `__dict__`。此时实例访问直接命中本地属性,跳过了类属性查找流程,因此绝不会触发函数自带的描述器 get 函数,最终拿到的仅是普通函数对象,若直接调用 `o.f('hello')` 则会抛出缺少位置参数的 TypeError 异常,必须手动传入实例 `o.f(o, 'hello')`。 同样的边界情况也出现在自定义描述器中。如果类属性由一个自定义描述器承载,且其 `__get__` 方法直接返回普通函数对象,Python 解释器在获取该返回值后不会递归再次触发该函数对象的描述器,因此最终暴露给实例调用的依然是需要手动显式传参的普通函数。 所谓的方法绑定实质上是属性查找与函数描述器协同构建的抽象封装。在定义函数时,第一个参数命名为 `self` 仅是社区惯例,在语法底层只起到占位符的作用,其名称可被任意合法标识符替代。

Original transcript

[0:00] 这期视频我们来讲一个大家习以为常但是很多人并不知道背后原理的这么一个东西就是这个self那大家可能都知道我们在这个class里面定义一个方法 定义一个method的时候第一个argument 我们一般写成self然而实际建立了一个Object之后我们在调用这个方法的时候我们却只传Self之后的Ergument比如说左边这个非常简单的例子Class A里面有一个我定义的方法F这个F实际上take了两个argument,一个self,一个data但是当我建立了一个object o之后我实际上在调用o.f的时候传进去的argument是只有self之后的argument的也就是当我使用o.fhello的时候它就会打印出来hello当然为了避免大家不信我还特意的运行了一下就是这样的结果那接下来问题就来了这个self哪去了呢当然我们对python有一定了解的人就会知道这个self能保存的就是我自己本身的这个object就像现在这样 我在建立了这个Object之后 把他的AttributeName复制成Bob然后把这个F函数稍微改一下 先打印self.name这个时候我在调用o.fhello的时候呢就会先打bob再打hello所以接下来我们就来讲一讲SELF背后的魔法我们在之前将class视频里面提到过在我们定义一个class的时候 并没有发生非常多神奇的事情我们实际上就是有一个命名空间 运行了class里面那个code block然后又把那里面产生的所有的local variable给变到了这个class的下方线下方线dick里面这一整套机制都是我们剖析过的所以你现在就应该知道这个class里面保存的这个f函数并没有任何特别的它就是一个普普通通的函数它和我们正常定义的一个函数是没有任何区别的你让他take了两个argument self data他自己就知道我需要两个argument那为什么在我们调用的时候我们只给一个argument呢如果你在这个地方稍有研究的话你就会发现这个事情只有当你建立了一个这个class的object建立了一个对象之后在这个对象上调用这个方法的时候它才会变成这样也就是说这个self的魔法只会在对象上出现不会在原class上出现为了加以区分我们打印一下这个A.F和O.F注意这里的 A 是这个 class 也就是这个 type这个O是Instance或者叫Object是这个对象我们可以看到a.f就是一个非常普通的function它就是一个函数而已但是O.F它已经变成了Bound method而所谓的bound method就是这个函数上面绑定了一个对象绑定的这个对象就是O所以当你在调用O.F的时候他才会知道self是O这里怎么理解呢?就是O.f实际上是返回了一个method object而这个object 当你调用它的时候 它会记住它当年产生的时候 身上绑定的是哪个object为了让大家加强理解 我们写了这么一段程序 我们把o.f直接复制给一个variable叫myfunk然后我们调用my funk hello我们可以看到在调用这一步的时候实际上跟O已经没有关系了因为我调用的是my funk然而我们看到当我们使用mifunk的时候和我们使用o.f时候一样也是只需要传进去一个argument并且MyFunk本身他能记得住self指向的是谁在你做MyFunk Hello的时候他也把这个old name Bob 给print出来了这也就是一个证据告诉大家O.f实际上就是返回了一个绑定在O自己本身上面的一个method当你调用这个method的时候 你不需要给self这个argument它会在调用这个原函数也就是你在class A 里面定义的这个 F 函数的时候把自己这个object作为第一个argument传进去换言之O.F并不是你在class A定义的这个F了那O.F是什么呢或者说在一个类和它的对象里面去尝试拿同一个名字的attribute [5:00] 却得到了不同的结果这个机制是怎么做的呢那可能一直关注我视频的小伙伴已经想到了这个机制背后的核心就是我们前几期视频里面提到过的描述器也就是descriptor那我们接下来看一下元奈玩 终于把这件事解释清楚当我们在python里定义了一个函数的时候我们实际上是建立了一个function object而这个function object它是有一个descriptor get函数的它的descriptor get 函数就是我们面前的这个函数我们可以看到这个descriptor get 函数很简单它就是返回了一个py method new这里注意他把这个函数funk和调用的对象object都传进去了而py method new这个函数就是建立了一个method对象这个method对象里面保存着funk保存着传进来的这个object它叫self同时他定义了自己在被调用的时候会使用这个method vector call函数那在这个Method Vector Call函数里呢他首先拿到了这个Self和Funk在各种case里面 它都会把self给放到argument的第一个然后他再把剩下的你传进来的argument放到他后面最后用这个新的argument list作为argument 传到这个funk里面那这个就是一个function变成一个method的核心机制好那为了防止之前有同学上课不认真我们把整个这个过程捋一遍首先当我们在class A里定义了这个函数F的时候我们实际上产生了一个Function object这个function没有任何特别的它take了两个argument一个self 一个data然后我们把这个FunctionObject保存到ClassA的Dict里面也就是说当我们调用a.f的时候它就是原来我们定义的这个函数我们需要传进去一个self 传进去一个data在这里我们self传进去就是这个O而data就是hello他就先打印出来这个old name 然后再打印一个hello而当我们建立了一个class A的object O然后使用O.f的时候大家回忆一下我们之前descript那些视频的内容由于F这个名字并不在O的Dictln所以他要返回他的type也就是Aliener长这个名字他发现A 里面有F这个名字然后他就把f拿出来 这个时候的f还是一个function object就还是我们定义的函数然而我们之前讲过当你用loadattr顺着mro从你的类里面拿到一个attribute的时候它首先会检查这个object有没有 descriptor get 函数如果有这个descriptor get 函数它返回的就不是你拿到的 attribute而是这个attribute调用descriptor get 函数返回的值因此在你做O.F的时候由于它找到了class A 里所以他返回的并不是A.F而是a.f的descriptor get函数的返回值 也就是我们刚才介绍的那个method是一个绑定了调用对象O的方法 object也正因如此 当你调用这个method的时候 你只需要传进去self之后的内容这个method会自动的把这个object补到第一个argument上好 那知道了这个原理有什么用呢我们在实际编程的过程中有的时候会动态的给对象或者是类增加函数或者方法对不对这个时候你肯定会发懵我动态增加的函数要不要写self我在调用的时候要不要给self我们举个例子我们看A本来是一个空的class然后定义了一个很常规的函数F这个时候我想动态的把这个函数F给增加到A这个class里那这个时候我就可以想到我只需要做a.f=f就可以了那在之后我新建的对象在调用O.F的时候如果他找到了classA里面的function它会把它转换成method因为这个函数是定义在type里的好 那么改一下 如果我们想把这个函数给定义到对象上而不是class上的这时候你就应该能反应过来由于这个函数定义到了对象上 [10:00] 对象的dict里面当你再使用o.f的时候它就不会再调用你拿到的这个function object的descriptor get函数了你拿到的这个o.f就是一个完整的function object而你再这么使用o.fhello就是错的了我们可以看一下它就会说你missing required positional argument了那这种情况下正确的用法就是o.f你要传进去这个o然后再hello你得给进这个self argument好那我们再给一个更复杂一点的例子我们现在class a里面这个f变成了我们自己设计的一个descriptor这个descriptor的get函数呢是返回一个我们定义的f函数这个时候我们是应该用o.fhello还是o.fohello呢如果你没有办法一下想到答案的话就按照我们之前讲的一步一步来当我们使用o.f的时候它会先在自己这个object里面找有没有f这个名字没有然后去它的class里面找发现有f这个名字f是一个我们自己定义的func descriptor object它拿到这个f首先要检查它是不是一个descriptor而由于我们在这个class里面定义了下方线下方线概谈数它是一个descriptor所以它会直接调用这个descriptor的概谈数这个概谈数就返回了一个function object而这个function object就直接作为返回值返给了这个o.f所以o.f拿到的是一个function而不是一个method在我们使用的时候就要使用o.fohello这个例子里就要求你要理解描述器还有这个魔法是在哪发生的这个魔法是在function object的描述器里发生的而对于我们自定义的描述器它就会拿到这个描述器直接返回的这个function object那通过这期视频呢我希望能解答大家有关self的这个疑惑就是当你在class里面定义这个所谓的class method的时候它其实产生的就是一个正常的function object而这个function的第一个argumentself也只是一个约定俗成的名字它本身没有任何神奇的地方它只是把第一个argument这个位置占了个坑而已你叫它什么对于python来说是一样的而我们在编程过程中感受到的这种所谓方法或者叫method这个事情是当我们在对象里面取一个attribute的时候发生的实际上起效果的是function本身的descriptor get函数是你定义的这个function变出来了一个method object而变出来的这个method让你可以不用把object本身作为参数传来传去了好那这期视频就到这里希望给大家有所帮助如果喜欢的话记得点下关注别忘了意见三连哦