从 Java 或 Kotlin 转到 Python 的开发者,几乎都会在同一个地方愣住:每个方法的第一个参数都叫 self,而且非写不可。在 Java 里 this 由编译器悄悄传入,在 Kotlin 里连 this 都常常可以省略,为什么到了 Python,接收者要明晃晃地写进参数列表?
答案一句话就能说完:self 就是显式化的 this,而 obj.method(x) 只是 Class.method(obj, x) 的语法糖。理解了这一点,self 的各种"怪癖"——绑定方法、classmethod 里的 cls、__init__ 里没有字段声明——都会顺理成章。本文按"self 的真实身份 → 语法糖的本质 → Java 与 Kotlin 中的同款机制 → 设计取舍 → 实战要点"的顺序展开。
1、self 的真实身份
self 是当前实例的引用,与 Java、Kotlin 中的 this 指向同一个东西。区别不在语义,而在书写位置:Java 和 Kotlin 由编译器把实例隐式传进方法,Python 则要求把它显式声明为第一个参数,方法体内也必须通过它访问实例成员。
先用一个最小的例子把等价关系立起来:
class Dog:
def __init__(self, name):
self.name = name # self.name 相当于 Java 的 this.name
def bark(self):
print(f"{self.name} 在叫")
d = Dog("旺财")
d.bark() # 常规写法
Dog.bark(d) # 与上一行完全等价
解读:最后两行输出完全相同。第二种写法把 bark 当成普通函数调用,手动把 d 塞进第一个参数——这才是 Python 方法的真实形态。官方 Python 常见问题 给出的解释也很直接:方法本质上是写在类里的普通函数,实例必须作为第一个参数传入,"self" 只是这个参数的惯用名。
2、点号调用的语法糖本质
d.bark() 掩盖了实际发生的两步:先在类上找到 bark 函数,再把"函数 + 实例"打包成一个绑定方法(bound method)去执行。两者的类型可以直接打印出来对比:
print(Dog.bark) # <function Dog.bark ...> —— 类上是普通函数
print(d.bark) # <bound method ...> —— 绑定了实例 d 的方法
解读:Dog.bark 是函数,d.bark 是绑定方法。点号访问触发了描述符协议,把函数与实例包装成调用时自动传入 self 的对象,完整机制见官方 描述符指南。
绑定方法是一等公民,可以像普通值一样保存和传递:
f = d.bark # 绑定方法已经记住了 d
f() # 输出"旺财 在叫",无需再传实例
解读:f 携带了实例信息,调用时不能也不需要再传 d。这个能力与 Kotlin 的 d::bark、Java 的方法引用是同一件事——把"对哪个对象执行哪个方法"打包成一个可传递的值,回调场景里非常好用。
3、Java 与 Kotlin 的同款机制
"接收者作为首参传递"并不是 Python 的发明,JVM(Java Virtual Machine,Java 虚拟机)系语言一直在底层这么干,只是语言层面藏得更深。先看 Java:
public class Dog {
private String name;
public Dog(String name) { this.name = name; }
public void bark() {
// 字节码层面:bark 的第一个参数就是当前实例
System.out.println(this.name + " 在叫");
}
}
解读:Java 源码里 bark() 没有接收者参数,但编译产物中实例方法的首参正是 this,调用时由字节码指令把实例压栈传入。Java 把这一层完全藏在了语言之下。
Kotlin 的扩展函数则把机制露出一半:
class Dog(val name: String) {
fun bark() = println("$name 在叫") // 成员方法,this 隐式
}
fun Dog.barkLoud() = println("$name 大声叫") // 扩展函数
// barkLoud 编译后的形态等价于:
// public static void barkLoud(Dog $this$barkLoud) { ... }
解读:扩展函数会被编译成静态函数,接收者成为第一个参数,这正是 Kotlin 官方文档描述的行为。它恰好就是 Python 所有方法的形态——只不过 Python 要求把接收者写在源码里。三种语言的传递方式对比如下:
| 语言 | 接收者传递方式 | 方法内引用方式 |
|---|---|---|
| Java | 编译器隐式传入,字节码首参即实例 | this,多数场景可省略 |
| Kotlin | 成员方法隐式,扩展函数显式占首参 | this,扩展内可直接用成员名 |
| Python | 一律显式写在第一个参数 | self,不可省略 |
从表格可以看出,三者只是"藏多深"的区别:Java 全藏,Kotlin 藏一半,Python 全部摆在明面上。
4、显式 self 的设计取舍
Python 之禅(PEP 20,全称 Python Enhancement Proposal 20)中有一条格言:
Explicit is better than implicit.
意为"显式优于隐式"。显式 self 正是这条哲学的落地,它至少带来三个实际收益。
第一,方法作用域绝对干净。方法体内出现的每个名字,要么是参数或局部变量,要么带 self. 前缀,不存在第三种来源:
class Counter:
def reset(self):
count = 0 # 局部变量,方法结束即消失
self.count = 0 # 实例属性,两者永不混淆
解读:在 Java 里,count = 0 是给字段赋值还是声明局部变量,取决于类里有没有同名字段,同名遮蔽是经典 bug 来源;Python 里 count 与 self.count 一眼可辨,不存在歧义。
第二,self 不是关键字,只是 PEP 8 规定的参数惯用名。把它改叫 this 或 me,程序照样运行,只是会被 linter 和同事嫌弃。这个事实反过来印证了它的本质:一个普通参数。
第三,函数与方法统一,动态特性因此不需要特殊机制。既然方法就是"首参为实例的函数",装饰器、偏函数这些变换作用在方法上就和作用在普通函数上完全一样。Guido van Rossum(Python 之父)曾专门撰文解释保留显式 self 的理由,核心一条正是它:一旦引入隐式 this,方法就必须变成特殊的语法结构,无法再像普通函数一样定义和变换。一个直接推论是:函数可以定义在类外,再挂到类上变成方法:
def wag(self):
print(f"{self.name} 摇尾巴")
Dog.wag = wag # 类外定义的函数,挂上即是方法
d.wag() # 输出"旺财 摇尾巴"
解读:函数与方法的界限如此模糊,正是因为接收者只是一个普通首参。Dog.wag = wag 这行猴子补丁之所以能成立,前提就是方法没有独立于函数的特殊身份。
代价同样真实:每个方法多写一个参数、新手容易漏写、从 JVM 语言转来的人初期觉得冗余。这是 Python 在"简洁"与"明确"之间的明确站队——它选了后者。
5、实战要点与常见坑
5.1、三类方法的第一个参数
self 的规则统一适用于三类方法,区别只在调用时首参被替换成什么。各类型对比如下:
| 方法种类 | 定义写法 | 首参接收什么 | 典型用途 |
|---|---|---|---|
| 实例方法 | def m(self) |
实例本身 | 操作实例状态 |
| 类方法 | @classmethod |
类本身 | 工厂方法、备选构造器 |
| 静态方法 | @staticmethod |
无首参 | 与类相关的纯函数 |
三种写法在标准库源码里随处可见,示例如下:
class Date:
def __init__(self, y, m, d):
self.y, self.m, self.d = y, m, d
@classmethod
def from_string(cls, text): # cls 接收类本身
y, m, d = map(int, text.split("-"))
return cls(y, m, d) # 工厂:子类调用返回子类实例
@staticmethod
def is_leap(y): # 既不要实例也不要类
return y % 4 == 0 and (y % 100 != 0 or y % 400 == 0)
解读:from_string 是备选构造器的标准写法,用 cls(y, m, d) 而不是 Date(y, m, d),子类继承时就能拿到子类实例;is_leap 与实例无关,用静态方法表达"逻辑上属于这个类"。
5.2、两个高频报错现场
第一个坑:访问成员忘了 self. 前缀。Python 没有字段声明语句,__init__ 里的赋值即声明:
class User:
def __init__(self, name):
self.name = name # 赋值即声明字段
def greet(self):
# return "Hi, " + name # NameError:裸 name 不在作用域
return "Hi, " + self.name # 实例属性必须经 self 访问
解读:JVM 语言里 name 会自动解析到同名字段,Python 不会。实例属性必须经 self 访问,NameError 就是这条规则的直接体现。
第二个坑:定义方法时漏写 self。此时调用 d.greet() 会把 d 当成第一个实参塞进函数,参数对不上就抛 TypeError,报错信息还常常令人困惑。规律很简单:写方法先写 self,就像写 Java 方法前先想清楚 this 指向谁。
6、总结
回顾全文的核心结论:self 是显式化的 this;点号调用是"函数 + 实例打包成绑定方法"的语法糖;JVM 在字节码层、Kotlin 在扩展函数里早有同款机制;显式 self 换来的是干净的作用域与统一的函数模型。对于刚从 Java 或 Kotlin 转到 Python 的开发者,建议把三条写进肌肉记忆:定义方法第一个参数永远写 self;访问实例成员永远带 self. 前缀;传递"对象点方法"时,记得它是记住实例的绑定方法。适应期通常不超过一周,之后多半会开始欣赏这种把一切摆在明面上的设计。

