前言
self大概是 Python 初学者最先记住、又最先误解的东西。它的误解通常集中在三点:以为self是关键字;以为它必须叫self;以为「方法里的self会自动变成一个实例」。
真相要从描述符协议说起:函数本身就是一种非数据描述符(non-data descriptor),它被定义在类里之后,通过实例做点号访问时,会自动返回一个「绑定方法(bound method)」,把实例塞到第一个参数的位置上。你写下的self,本质上只是那个被塞进来的第一个形参的名字。
官方文档写得很直白:按惯例实例参数叫self,但它叫this或任何别的名字都可以。所以这是一条「约定」,不是语法。
本文专讲self:它到底是什么、为什么必须是第一个参数、@staticmethod/@classmethod与实例方法三者的差别、以及cls与self的区别。代码按 Python 3.8+ 给出。
一、self 只是一个形参名
先看一段故意「不守规矩」的代码,证明self不是关键字:
# 适用于 Python 3.8+
class Point:
def __init__(this, x, y): # 形参名故意叫 this
this.x = x
this.y = y
def norm(self): # 这个方法里叫 self
return (self.x ** 2 + self.y ** 2) ** 0.5
p = Point(3, 4)
print(p.norm()) # 推演输出:5.0
print(Point.norm(p)) # 推演输出:5.0两个方法一个用this、一个用self,程序照跑不误。因为this和self只是位置上的第一个形参,名字本身没有任何魔法。
真正有魔法的是「点号访问 + 函数描述符」这套机制:
# 适用于 Python 3.8+
p = Point(3, 4)
print(p.norm) # 推演输出:<bound method Point.norm of ...>
print(p.norm.__self__ is p) # 推演输出:True
print(p.norm.__func__ is Point.__dict__["norm"]) # 推演输出:Truep.norm拿到的不是原函数,而是一个绑定方法对象。它内部记着两样东西:原始函数__func__,以及被绑定的实例__self__。调用p.norm()时,CPython 实际执行的是Point.norm(p)——实例被自动塞到第一个参数的位置,这就是self的全部来源。
二、为什么必须是第一个参数
因为绑定机制永远是「把实例放到参数列表的最前面」,没有例外。你写def norm(self),接收的就是这个被塞进来的实例;你写成def norm(a, b),那么a拿到实例、b只能等调用者再传一个参数。
于是下面三条规则是强行成立的:
| 写法 | 实际调用形式 | 结果 |
|---|
p.method() | type(p).method(p) | 正常,实例占据第一个参数 |
Point.method(p) | 同上,实例是显式传的 | 正常,两种写法完全等价 |
Point.method() | 少了实例这个参数 | TypeError,参数个数不足 |
再对照官方的绑定规则表,三种方法在「从实例调用」和「从类调用」时的变换一目了然:
| 方法种类 | 从实例obj.f(*args)调用 | 从类Cls.f(*args)调用 |
|---|
| 普通函数(实例方法) | f(obj, *args) | f(*args) |
@staticmethod | f(*args) | f(*args) |
@classmethod | f(type(obj), *args) | f(cls, *args) |
注意第二、三列:普通函数从类调用时不再自动补参数。这就是「Point.method()会报参数不足」的原因——走类调用时没人给你补实例。
三、实例方法、类方法、静态方法三兄弟对比
# 适用于 Python 3.8+
class Temperature:
unit = "C"
def __init__(self, value):
self.value = value
def show(self): # 实例方法:第一个参数是实例
return f"{self.value}{self.unit}"
@classmethod
def from_fahrenheit(cls, f): # 类方法:第一个参数是类
return cls((f - 32) * 5 / 9)
@classmethod
def set_unit(cls, u):
cls.unit = u
@staticmethod
def is_valid(v): # 静态方法:不接收任何隐式参数
return -273.15 <= v
t = Temperature.from_fahrenheit(212)
print(t.show()) # 推演输出:100.0C
print(Temperature.is_valid(-300)) # 推演输出:False
print(t.is_valid(20)) # 推演输出:True三个要点:
- 实例方法用
self,因为它要读 / 写实例自己的数据(这里是self.value)。 - 类方法用
cls,因为它操作的是类一级的数据(这里改了cls.unit)或者要做「备用构造器」(from_fahrenheit返回一个新实例)。 - 静态方法其实就是「放在类命名空间里的普通函数」。它不接收
self也不接收cls。官方建议是:当你有个函数在概念上属于这个类、但不需要访问实例或类的数据时,用@staticmethod。如果它连类的概念都不需要,那它更适合当一个模块级函数。
注意Temperature.unit = "C"是类变量,所以set_unit改它会让所有实例都跟着变;这也正是类方法存在的价值。
四、cls 与 self 的区别
一句话:self是「一个实例」,cls是「一个类」。它们的名字同样是约定,同样只是「绑定进来的第一个参数」——只不过@classmethod绑定进来的是类而不是实例。
关键差别在继承时的表现:
# 适用于 Python 3.8+
class Plugin:
@classmethod
def create(cls):
return cls() # cls 是「实际被调用的那个类」
@staticmethod
def describe():
return "Plugin 的说明"
class AudioPlugin(Plugin):
pass
print(type(AudioPlugin.create()).__name__) # 推演输出:AudioPlugin
print(type(Plugin.create()).__name__) # 推演输出:Plugin
print(AudioPlugin.describe()) # 推演输出:Plugin 的说明AudioPlugin.create()里cls拿到的是AudioPlugin而不是Plugin,所以造出来的是AudioPlugin实例。这就是「备用构造器」为什么一定要用cls而不是写死类名——写死类名的话,子类调用时会造出父类的实例。
而describe是静态方法,它不知道调用者是谁,AudioPlugin.describe()与Plugin.describe()返回同一个字符串。静态方法里没有cls,也没有self,这一点要和类方法严格区分开。
常见坑点
1. 定义实例方法时忘写self。
❌def show():然后里面用self.value,调用时报NameError或参数不匹配。 ✅def show(self):,第一个参数永远留给实例。
2. 把self当成关键字,不敢改名。
❌ 以为写def __init__(this, x)会报语法错误。 ✅self只是惯例名,换成任何合法标识符都能跑;但为了可读性,团队里应统一用self。
3. 在静态方法里使用self。
❌@staticmethod修饰的方法里写self.x,报NameError。 ✅ 静态方法不接收隐式参数,需要数据就通过参数显式传入,或改用实例方法。
4. 通过类名调用实例方法却忘了传实例。
❌Point.norm(),报TypeError: norm() missing 1 required positional argument。 ✅ 要么p.norm(),要么Point.norm(p),把实例显式补上。
5. 在类方法里写死类名而不是用cls。
❌@classmethod里写return Base(),子类调用时造出的是父类实例。 ✅ 写return cls(),让它跟随实际调用的类。
6. 在__init__里把属性名拼错,之后才报错。
❌self.vlaue = value,然后在show()里访问self.value,构造时不报错、用的时候才AttributeError。 ✅ 赋值和读取用同一个拼写;__slots__能帮你在赋值阶段就把不存在的属性名挡下来。
7. 以为self会自动创建实例属性。
❌ 方法里只写了self.total + 1(没赋值),期待属性凭空出现。 ✅ 实例属性是靠self.名字 = 值这个赋值动作创建的;只读不写不会创建任何东西。
总结
| 概念 | 是什么 | 由谁绑定 | 典型用途 |
|---|
self | 第一个形参,按约定叫self | 实例方法自动把实例传进来 | 读写实例自己的数据 |
cls | 第一个形参,按约定叫cls | 类方法自动把类传进来 | 备用构造器、改类级数据 |
@staticmethod | 不接收隐式参数的普通函数 | 什么都不绑定 | 与类概念相关但不依赖实例 / 类数据 |
| 绑定方法对象 | 函数 + 实例的组合 | 函数描述符在点号访问时生成 | 让p.m()等价于Cls.m(p) |
一句话收尾:self不是关键字,而是「绑定进来的第一个参数」的惯例名;记住函数描述符与绑定方法这条链路,实例方法、类方法、静态方法三种形式就都能一眼看穿。