1. 为什么self值得单独拎出来讲
刚学Python那会儿,我踩过最大的一个坑就是类里面的self。写函数写得好好的,一进类的方法就报错,提示少传了一个位置参数,翻来覆去看代码也没发现哪里少了参数。后来才明白,那个"凭空多出来"的参数就是self。这件事听起来很小,但它卡住了大量初学者从"会写脚本"到"会写类"的这一步。
self这个东西,说穿了就是实例方法里的第一个参数,指向调用这个方法的实例本身。它不是什么关键字,也不是Python语法强制保留的魔法名字,你完全可以把它写成this、me、甚至abc,代码照样跑。但整个Python社区约定俗成用self,所以你也应该用self,不然别人读你的代码会觉得别扭。
这篇文章想做的事情很明确:把self从"知道有这么个东西"讲到"彻底搞明白它为什么存在、什么时候用、什么时候不用、容易在哪里翻车"。适合已经会写Python函数、准备进入面向对象阶段的朋友,也适合写了很久类但一直对self半懂不懂的人。我会从底层机制讲到实操细节,再讲到常见报错和排查思路,尽量把每一个"为什么"都讲透。
先给一个最直观的类比。把类想象成一张房屋设计图纸,实例就是按照这张图纸盖出来的一栋栋真实房子。self就是"当前这栋房子"的指代词。当你在图纸上写"给客厅刷漆"这个动作时,你得说明是给哪栋房子的客厅刷漆,self干的就是这个指认工作。没有self,Python就不知道你操作的是张三家的客厅还是李四家的客厅。
2. self的本质:它到底是个什么东西
2.1 从一次方法调用看self的传递过程
很多人对self的困惑,根源在于没看清方法调用时到底发生了什么。我们来看一段最普通的代码:
class Dog: def bark(self): print("wang wang") d = Dog() d.bark()表面上看,d.bark()没有传任何参数,但bark定义时明明要求一个self。这里的秘密在于,Python在背后自动把d作为第一个参数塞了进去。也就是说,d.bark()等价于Dog.bark(d)。你可以亲自试一下,下面这两行输出完全一样:
d.bark() Dog.bark(d)第二种写法就是"手动传self"。理解了这一点,很多报错就迎刃而解了。比如你写Dog.bark(),Python不会自动帮你补实例,于是它就认为你少传了一个参数,直接抛TypeError: bark() missing 1 required positional argument: 'self'。这个报错信息其实已经把答案告诉你了,只是初学者往往看不懂。
所以self的第一层本质是:它是实例方法接收"自己"的入口,由Python在调用时自动填充。它不是凭空出现的,而是实实在在的一个参数,只不过平时被语法糖藏起来了。
2.2 self不是关键字,但你必须尊重约定
我前面说过,self可以改名。给你看个能跑的版本:
class Cat: def meow(this): print("miao miao", this) c = Cat() c.meow()这段代码完全合法,输出miao miao <__main__.Cat object at 0x...>。Python解释器根本不认识self这个词,它只认"第一个参数"。那为什么还要坚持用self?原因有三个,都很实际。
第一,可读性。全世界的Python代码都用self,你换成this,别人读你的代码要多绕一道弯。第二,工具兼容性。很多IDE、静态检查工具、代码补全插件默认按self来识别实例方法,你改名可能导致提示失效。第三,团队协作。代码是给人看的,约定大于个性。所以结论很简单:知道它能改名,但永远别改。
提示:如果你在维护一份老代码,看到有人用this或self_之类的名字,不要急着改,先确认没有其他地方依赖这个名字,再统一替换。
2.3 self和实例属性的绑定关系
self最常用的场景就是绑定实例属性。看这段:
class Student: def __init__(self, name, age): self.name = name self.age = age def introduce(self): print(f"我是{self.name},今年{self.age}岁") s = Student("小明", 18) s.introduce()这里的self.name = name,左边是"这个实例的属性name",右边是"传进来的参数name"。Python里属性不需要提前声明,你第一次赋值就创建了它。self.name这个写法,本质上是"在self指向的那个实例对象上,挂一个叫name的属性"。
这里有个细节值得说:self.name和局部变量name是两个完全不同的东西。局部变量name在方法执行完就消失了,而self.name会一直挂在这个实例上,直到实例被销毁。这就是为什么introduce方法里能访问到self.name,因为它存在实例身上,不是存在方法里。
3. 不同方法类型里self的用法差异
3.1 实例方法:self是标配
实例方法是最常见的方法类型,第一个参数必须是self。它的特点是既能访问实例属性,也能访问类属性,还能修改实例状态。判断一个方法是不是实例方法,看它第一个参数是不是self就行。
class Counter: def __init__(self): self.count = 0 def add(self): self.count += 1 return self.count c = Counter() print(c.add()) # 1 print(c.add()) # 2每次调用add,操作的都是同一个实例的count,所以能累加。如果把self.count改成局部变量count,那每次都是0加1,结果永远是1。这个对比很能说明self的作用:它让方法能记住并修改实例的状态。
3.2 类方法:cls登场,self退场
类方法的第一个参数是cls,代表类本身,不是实例。用@classmethod装饰器标记。
class Pizza: def __init__(self, size): self.size = size @classmethod def margherita(cls): return cls("中号") p = Pizza.margherita() print(p.size) # 中号类方法常用于替代构造函数。比如你想提供多种创建实例的方式,就可以用类方法。这里cls的作用和self类似,只不过它指向的是类而不是实例。为什么用cls而不是写死Pizza?因为这样在子类继承时,cls会自动变成子类,创建出来的就是子类实例,这叫多态。
3.3 静态方法:谁都不需要
静态方法用@staticmethod装饰,第一个参数既不是self也不是cls。它本质上就是"挂在类命名空间下的普通函数"。
class MathUtil: @staticmethod def add(a, b): return a + b print(MathUtil.add(1, 2)) # 3静态方法不能访问实例属性和类属性,因为它压根不接收self或cls。那什么时候用?当某个函数逻辑上属于这个类,但不需要访问任何实例或类状态时。比如工具函数、纯计算函数。用静态方法的好处是语义清晰,告诉读代码的人"这个函数和实例无关"。
3.4 三种方法对比速查
| 方法类型 | 第一个参数 | 装饰器 | 能访问实例属性 | 能访问类属性 | 典型用途 |
|---|---|---|---|---|---|
| 实例方法 | self | 无 | 是 | 是 | 操作实例状态 |
| 类方法 | cls | @classmethod | 否 | 是 | 替代构造函数 |
| 静态方法 | 无 | @staticmethod | 否 | 否 | 工具函数 |
这张表我建议你存下来,初学阶段遇到"这个方法该用哪种"的纠结时,对着表看一眼就清楚了。判断标准很简单:需要实例数据就用实例方法,只需要类数据就用类方法,都不需要就用静态方法。
4. self在继承和特殊场景下的表现
4.1 继承时self指向的是子类实例
这是self一个容易被忽略但非常重要的特性。看这段代码:
class Animal: def speak(self): print(f"{self.__class__.__name__} 在叫") class Dog(Animal): pass d = Dog() d.speak() # Dog 在叫speak方法定义在Animal里,但通过Dog实例调用时,self指向的是Dog实例,所以self.__class__.__name__输出Dog而不是Animal。这就是多态的基础。self永远指向"实际调用方法的那个实例",而不是"方法定义所在的类"。
这个特性在写框架、写基类时特别有用。你可以在基类里写通用逻辑,用self去访问子类特有的属性或方法,实现"基类定义流程,子类填充细节"的设计模式。
4.2 self在__init__里的初始化顺序陷阱
__init__是self最常出现的地方,但这里有个坑:如果你在__init__里调用了一个依赖尚未初始化属性的方法,会报AttributeError。
class Bad: def __init__(self): self.setup() self.value = 10 def setup(self): print(self.value) # 报错,value还没赋值 b = Bad()这段代码会抛AttributeError: 'Bad' object has no attribute 'value'。原因是setup在self.value = 10之前被调用,此时value还不存在。解决办法是把赋值放在调用之前,或者给属性一个默认值。这个坑我在实际项目里踩过不止一次,尤其是当__init__逻辑变复杂、方法调用链变长时,很容易忽略初始化顺序。
注意:__init__里尽量只做属性赋值,把复杂逻辑抽到单独的方法里,在__init__末尾统一调用,这样初始化顺序一目了然。
4.3 self在闭包和嵌套函数里的坑
当你在方法内部定义嵌套函数时,self不会自动传进去,需要显式处理:
class Task: def __init__(self, name): self.name = name def run(self): def inner(): print(self.name) # 这里能访问self,因为闭包 inner() t = Task("测试") t.run() # 测试嵌套函数inner能访问self,是因为Python的闭包机制,inner捕获了外层函数的self变量。但如果你把inner定义成静态方法或者独立函数,就访问不到了。这个细节在写回调、写装饰器时经常遇到,理解闭包和self的关系能帮你少走弯路。
5. 常见报错与排查实录
5.1 报错速查表
| 报错信息 | 原因 | 解决方法 |
|---|---|---|
| missing 1 required positional argument: 'self' | 用类名直接调用实例方法,没传实例 | 用实例调用,或手动传实例 |
| takes 1 positional argument but 2 were given | 定义时没写self,调用时自动传了实例 | 方法定义第一个参数补上self |
| 'X' object has no attribute 'Y' | 属性未初始化就访问 | 检查__init__赋值顺序 |
| name 'self' is not defined | 在方法外使用了self | self只能在方法内使用 |
5.2 一个真实排查案例
有次同事写了个类,调用方法时一直报takes 1 positional argument but 2 were given。代码大概是这样:
class Config: def load(data): print(data) c = Config() c.load()问题出在load定义时没写self,Python调用c.load()时自动把c作为第一个参数传进去,于是data接收了c,但方法只定义了一个参数,实际传了两个(c和隐式的),所以报"给了2个但只接受1个"。修复方法就是加上self:
class Config: def load(self): print("loading")这个报错信息其实很直白,但初学者容易懵,因为"2 were given"听起来像是自己多传了参数,实际上是Python自动传的self没地方放。
5.3 排查self问题的通用思路
遇到self相关报错,我一般按这个顺序排查:
- 看报错类型。missing self说明调用方式不对,too many arguments说明定义少了self,attribute error说明属性没初始化。
- 看方法定义。确认第一个参数是不是self,装饰器用对没有。
- 看调用方式。是用实例调用的,还是用类名调用的。
- 看初始化顺序。属性是不是在被访问之前就赋值了。
这套流程走下来,九成以上的self问题都能定位。剩下的疑难杂症,多半和继承、元类、描述符有关,那属于进阶话题,初学阶段不用深究。
6. 几个容易混淆的进阶问题
6.1 self和局部变量的作用域区别
很多人分不清self.x和x。简单说,self.x是实例属性,生命周期和实例一样长,所有方法都能访问;x是局部变量,只在当前方法内有效,方法执行完就没了。看这个例子:
class Demo: def method(self): x = 10 self.y = 20 d = Demo() d.method() print(d.y) # 20 print(d.x) # 报错,x不是实例属性d.y能访问,因为它是实例属性;d.x报错,因为x只是method内部的局部变量,方法结束后就销毁了。这个区别在调试时特别重要,很多人以为方法里赋的值都能在外部访问,其实只有self.开头的才行。
6.2 self在__slots__下的表现
用__slots__可以限制实例能有哪些属性,节省内存。但它和self的配合有个细节:
class Point: __slots__ = ('x', 'y') def __init__(self, x, y): self.x = x self.y = y p = Point(1, 2) p.z = 3 # 报错,z不在__slots__里用了__slots__后,你只能给列出的属性赋值,self.z = 3会抛AttributeError。这个机制在创建大量实例时能显著省内存,但代价是失去动态添加属性的灵活性。什么时候用?当你确定实例属性固定、且需要创建海量实例时。
6.3 self在数据类里的隐式处理
Python 3.7引入的dataclass让self的写法简化了不少:
from dataclasses import dataclass @dataclass class User: name: str age: int u = User("小明", 18) print(u.name)dataclass自动帮你生成了__init__,你连self都不用写。但底层还是self那套机制,只是被装饰器藏起来了。理解self之后再学dataclass,你会明白它到底帮你省了什么。
7. 我个人的实操心得
写了这么多年Python,关于self我有几个体会想分享。
第一,别把self当成魔法,它就是个普通参数。一旦你接受"self是Python自动传的第一个参数"这个设定,所有困惑都会消散。我教别人的时候,第一句话就是"self不是关键字,是参数",效果比讲一堆理论好得多。
第二,命名约定要遵守,但心里要清楚它可改。知道self能改名,能帮你理解它的本质;坚持用self,能帮你融入社区。这两件事不矛盾。
第三,遇到self报错先看调用方式。九成的self问题都出在"用类名调用了实例方法"或者"定义时漏了self"。养成先看这两处的习惯,排查效率能翻倍。
第四,类方法用cls,静态方法不用参数,这个区分要刻进肌肉记忆。我见过太多人把该用类方法的地方写成静态方法,结果子类继承时行为不对。判断标准就一句话:需要类信息就用cls,不需要就静态。
最后分享一个小技巧。如果你不确定一个方法该用哪种类型,先写成实例方法,跑通了再根据实际需要改成类方法或静态方法。因为实例方法最灵活,能访问的东西最多,改起来也最容易。等代码稳定了再优化,比一开始就纠结用哪种要高效得多。