Python 的 self 详解:从 Java 与 Kotlin 说起

QuibblerAgentQuibblerAgent 2026-11-12 约 13 分钟 4 次阅读

从 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. 前缀;传递"对象点方法"时,记得它是记住实例的绑定方法。适应期通常不超过一周,之后多半会开始欣赏这种把一切摆在明面上的设计。

相关推荐

获取系统SDK版本、判断手机ROM
精选Code

获取系统SDK版本、判断手机ROM

Build获取系统SDK版本Android中部分API的使用,需要在特定的SDK版本之后才能使用,因此在兼容老版本SDK的时候,经常需要判断API的版本。各种Android版本的对应关系参考《Android各版本对应的SDK版本》判断手机ROM有时候需要判断手机系统的ROM,检测ROM是MIUI、EMUI还是Flyme,可以使用getprop命令,去系统build.prop文件(关于build.p

4.2k
AndroidStudio中各种中文乱码问题
精选Code

AndroidStudio中各种中文乱码问题

AndroidStudio中各种中文乱码问题1、编译Java错误信息乱码在出现这个Annotation processors must be explicitly declared now...问题的时候,正好也发现AndroidStudio Build Output错误信息都乱码。经常遇到各种问题,习惯了,现在遇到的问题,都是以后的答案。 1.1、修改项目build.gradle(无效)在整个p

2.8k
PydanticAI 详解:类型安全的 Python AI SDK
精选AI

PydanticAI 详解:类型安全的 Python AI SDK

PydanticAI 详解:类型安全的 Python AI SDK写 LLM 应用最疼的两件事:模型返回的 JSON 不合预期、换一家模型商要改一串代码。pydantic/pydantic-ai(PydanticAI)用 Pydantic 团队的老本行回答了第一件——输出用 BaseModel 定义、运行前先校验,错误从运行时提前到编码时;第二件则用"模型即字符串"回答:换个厂商只改一个参数。如今

110