☰
深入理解Python self:实例方法、类方法与面向对象底层逻辑
2026/10/5 4:32:34 网站建设 项目流程

1. 初学self关键字时,我最想骂人的那个瞬间

先说个我自己的经历。一年多前刚接触Python,写了一行代码:

class Dog: def bark(): print("汪汪")

然后调用Dog().bark(),啪,报错:TypeError: bark() takes 0 positional arguments but 1 was given。

我当时整个人是懵的。我明明没传参数,凭什么说"给了1个参数"?翻遍各种教程,得到的解释基本都是"因为你没写self,加上就好了"。这种回答比报错本身更让人抓狂——为什么加上一个self就天下太平?这个self到底是个什么东西?凭什么它偷偷占了一个参数位?

后来在反复踩坑、翻源码、看别人项目代码之后,我才算是真正把这玩意儿弄明白了。这篇笔记不是教科书式的定义复读,而是把我从"背语法"到"理解设计逻辑"的完整过程写下来,包括那些让我卡了很久的细节和误解。如果你正在为self发愁,看完应该能少走不少弯路。

先说结论:self就是实例对象本身的一个参数名约定。它不是什么魔法关键字,Python里甚至不强制你叫它self,你叫hello、叫this_obj都行,只是没人会那么干。真正有讲究的是背后的传参机制和你对"类、实例、方法"这三者关系的理解。

这篇笔记适合三类人:刚学到类和对象的初学者、写了好一阵子但搞不清self和cls区别的人、以及想给别人讲清楚这个概念但总觉得差点意思的人。后面所有内容围绕的都是同一句话展开:Python里方法之所以非得有self,是因为"把对象自己传给自己"这件事是Python实现面向对象的方式。

2. 为什么Python非要搞一个self出来?从"函数"到"方法"的那一步

2.1 先回到函数:Python里一切都是对象

要理解为什么有self,先看一个更基础的事实:在Python里,类里面定义的函数和外面定义的函数,本质上没有区别。

def bark(dog): print(f"{dog.name}在汪汪叫") class Dog: def __init__(self): self.name = "旺财" d = Dog() bark(d)

你看,我在类外面定义了一个普通函数,传一个对象进去,完全能正常工作。函数的执行就是"接收参数、处理、返回结果",它根本不关心你那个参数是叫self还是叫dog。

那类里面的方法呢?

class Dog: def __init__(self): self.name = "旺财" def bark(self): print(f"{self.name}在汪汪叫") d = Dog() d.bark()

这里bark(self)看上去多了一个参数,但实际上调用的时候也没传参。再对照一下最开头的报错:bark() takes 0 positional arguments but 1 was given——报错已经暗示了真相:d.bark()在底层被翻译成了Dog.bark(d)。

2.2 魔法在"."上:实例点方法时Python做了件事

这是整个self机制最核心的一层,所有初学者没绕过来的弯都在这。当你写下d.bark()的时候,Python解释器实际执行的是两步:

  1. 找出d这个对象的类型,也就是Dog这个类;
  2. 从Dog的命名空间里取出bark这个函数对象,然后把d作为第一个参数传进去。

也就是说,点号后面跟的"方法",其实是一个"绑定了实例的函数"。绑定之后,这个函数默认把实例自己塞进第一个参数位。这个参数位叫什么名字都不影响机制本身,但整个Python社区约定俗成叫它self。

我当初理解不了,是因为我把"方法"当成了一种完全独立于"函数"的东西,觉得它有魔法。其实它就是一个普普通通的函数,只因为定义在一个类里面、通过实例去调用,才被传入了第一个参数。去掉那层皮,它的本质就是:

Dog.bark(d) # 等价于 d.bark()

而这个d,就是self。

2.3 生活类比:方法就像"带着病人病历去会诊"

和"面向过程"编程里的普通函数对比如下:

写法调用时背后实际发生的事
普通函数bark(dog)你把狗作为参数传进去
实例方法dog.bark()解释器替你把狗传进去

打个比方。普通函数是你拿着一张病历,去找医生:"医生,这是病人的情况,你看看。"你把病历递过去。实例方法则是医生拿着病历本直接跟你走——病历本不用你递,它自动就在医生手里。这个"自动在手里"的病历本,就是self。

所以为什么Python坚持用显式的self而不是像Java/JavaScript那样用隐式的this?这是设计者Guido的一个偏好:"显式优于隐式"。与其让解释器背地里偷偷塞一个当前对象进方法体,不如明明白白地让你看到、让你在方法定义时第一个参数写出来。这样你就永远不会误解:方法就是函数,对象就是参数。

这个设计被人吐槽过很多次,说"啰嗦",但实际写下来你会发现它有个好处:你随时能清楚地看到这个方法的"上下文"来自哪里,想在类外面造一个伪方法也能直接操作。往后读到装饰器、闭包、函数式编程时,你会越来越感受到"函数与对象平等"这个设计的价值。

3. 参数被谁吃掉了?self的入参与传参全流程拆解

3.1 定义时写self,调用时不写:这就是规则本身

我看到很多教程喜欢用"小抄式记忆法"来教:定义实例方法时第一个参数写self,调用时不传。记住了,考试不挂,但心里总觉得悬。现在我把它彻彻底底拆开给你看。

先看定义与调用全貌:

class Person: def __init__(self, name, age): self.name = name self.age = age def introduce(self): print(f"我叫{self.name},今年{self.age}岁") p = Person("小明", 20) p.introduce()

上面代码中:

  • __init__是一个特殊方法,它负责初始化实例的"属性绑定",但它的本质也是普通函数。当你写Person("小明", 20)时,Python做的是:先创建了一个空白实例,再调用Person.__init__(实例, "小明", 20)。所以__init__里看到的self.name = name,意思是"在这个全新的实例对象上,给它的name属性赋值"。
  • p.introduce()调用时,p自己被塞进了introduce的第一个参数位置。self.name实际就是p.name,self.age就是p.age。

一句话记住:self是"正在被操作的那个具体对象自己"。

3.2 对象关联性关键点:多实例各自持有自己的self

self最容易误会的点在于:它不是一个共享的全局变量,而是每次调用都跟着具体实例走的。看这个细节:

class Counter: def __init__(self, initial=0): self.count = initial def add(self, step=1): self.count += step return self.count a = Counter() b = Counter(10) a.add() a.add() b.add() print(a.count) # 2 print(b.count) # 11

a和b是两个完全独立的空间,a.add()里的self是a,b.add()里的self是b。它们各自的count互不干扰。如果你在里面写成count += step,连等号左边那个count来源都成问题——局部变量还是全局变量?解释器会把这个count当成局部变量来算,要么报UnboundLocalError,要么读了外部全局值,然后你写出一个"计数器互相串台"的诡异bug。

这里我当年踩过一个很深的坑。我在一个类里维护一个全局状态的列表,写成了类属性:

class Task: tasks = [] # 类属性,所有实例共享 def __init__(self, name): self.name = name def add_task(self): self.tasks.append(self.name)

忘了用self.tasks初始化的话,t1 = Task("a"); t2 = Task("b"); t1.add_task(); t2.add_task()之后,t1.tasks是['a','b'],t2.tasks也是['a','b']。因为类属性是所有实例共享的一份。正确的做法是:

def __init__(self, name): self.name = name self.tasks = [] # 每个实例一份,这才是self的意义

类属性和实例属性的区别,是理解self的另一个枢纽。**用self.xxx赋值,就是在给"当前这个实例"建立专属属性;不通过self直接写在类体里,那只是类的模板属性。**这两个空间在初始化与赋值过程中经常被混着用,直到出bug才被察觉。

3.3 self能拿到什么?在方法里self到底可以干什么

一旦你拿住了self,这个对象自己身上的一切都变成可访问的。具体来说:

  • 读取/修改自己的属性:self.name、self.age = age这类操作是最常见的;
  • 调用自己的其他方法:在类的方法之间互调时,必须用self.xxx();
  • 调用自己的类方法或静态方法:用self.调用由@classmethod装饰的方法时,它会把实例类型传过去;
  • 访问类本身的属性:self.__class__可以拿到实例对应的类,从而访问类属性、类方法。
class Calculator: tax_rate = 0.1 # 类属性 def __init__(self, base): self.base = base def add_tax(self): return self.base + self.base * self.tax_rate def double_base(self): self.base *= 2 # 修改自己的属性 return self.base def show_both(self): # 方法里调用其他方法 print(self.add_tax()) print(self.double_base())

方法之间的互调,只要你在方法内部想用本类的另一个方法,永远不能省略self.。同样名字的函数在类外面定义,和你这个类没有任何关系,Python解释器不会帮你"就近查一下外面的函数"。这是很多初学者在方法里直接写函数名导致NameError的常见原因。

4. 名字能乱起吗?self的硬约定与两条必须知道的边界

4.1 换掉self会怎样?能跑,但别那么干

前面说了,self并不是关键字。你可以写:

class Foo: def bar(me): return me

调用f = Foo(); f.bar()完全正常,因为那个位置只是"第一个参数位",叫什么名字完全由你决定。

那为什么大家统一叫self?第一个原因是可读性。你写me、this、obj,别人凑合能懂,但如果你写了个x,别人看代码时就得琢磨"x是什么?哦,原来是这个对象自己"。这种琢磨很烦人。第二个原因是静态检查工具。主流的IDE和linter(pylint、ruff等)默认约定方法第一个参数命名为self,如果你用其他名字,它们会给出invalid-first-argument-name之类的警告。不会报错,但满屏黄色波浪线很闹心。

所以我的建议:知道能改,但永远别改。规范的价值不在于"不改就出错",而在于"改了就让团队协作成本上升"。Python社区把这叫"约定优于配置"的一个微观体现。

4.2 self在构造方法__init__里:初始化专属属性的必经之路

这里还有一个值得细说的地方,__init__里的self和普通方法里的self地位完全相同,但作用上有一个特殊性:它的主要职责是"为这个全新的、空白的实例填上属性"。

你写self.name = name,本质上是做了一件事:在实例的__dict__(每个对象内部维护的一个字典,存属性名和值)里插入一个键值对。你可以自己验证:

class Demo: def __init__(self): self.a = 1 d = Demo() print(d.__dict__) # {'a': 1}

这就是为什么"所有实例属性必须在方法里用self.赋值"才算是实例属性,因为只有self.xxx = yyy这种写法会往实例的__dict__里写。直接类体里a = 1写的是类属性,存在类的__dict__里,所有实例共享。

搞清楚这个底层存储,你就能理解几个常见的诡异现象:

  • 两个实例有同名的属性,但互不影响——因为它们各自的__dict__是两份;
  • 实例属性会遮蔽类属性——Python查找属性时,先去实例的__dict__找,找不到再去类里找;
  • 给一个实例动态添加self.新属性是允许的——只需要d.new_attr = 5或者通过方法里self.new_attr = 5就行。

4.3 self只有这一种吗?还有cls、staticmethod和不用的场景

self讲完,自然要提它的兄弟姐妹——cls。因为初学阶段经常把self和@classmethod里的cls搞混。

装饰器第一个参数约定名指向对象使用场景
无(实例方法)self实例对象操作实例属性/方法,是绝大多数方法的形式
@classmethodcls类对象操作类属性、生成实例的备选构造方法等
@staticmethod无固定参数无特殊指向逻辑上属于类但其行为不依赖实例/类的工具函数

具体代码长这样:

class Date: fmt = "%Y-%m-%d" def __init__(self, year, month, day): self.year = year self.month = month self.day = day @classmethod def from_string(cls, s): # cls是Date这个类,不是实例 year, month, day = map(int, s.split("-")) return cls(year, month, day) @staticmethod def is_valid(s): # 不依赖实例也不依赖类的状态,就是工具函数 try: year, month, day = map(int, s.split("-")) return True except ValueError: return False d = Date.from_string("2026-01-15") print(d) # <__main__.Date object at ...> print(Date.is_valid("2026-13-40")) # True,这里没做严格月日校验

from_string里的cls指向的是Date类本身,所以cls(year, month, day)等价于Date(year, month, day)。这个模式在标准库里大量存在,比如dict.fromkeys()。

而@staticmethod装饰的方法不需要self也不需要考虑特殊的第一个参数。你可以把它想象成一个"碰巧放在了类里面的普通函数"。新人最容易犯的错是:给一个明明不依赖实例状态、不依赖类状态的功能,硬把self写上,然后在方法里也没用到self,导致pylint警告unused-argument。这种情况下,果断改成@staticmethod会让代码干净很多。

5. 从报错到恍然大悟:四个让新手怀疑人生的常见陷阱

5.1 类型错误:takes 1 positional argument but 2 were given

这是最经典的self报错,我把它分成三种来源,每种都有不同的修法:

情况一:定义方法时忘了写self

class Wrong: def greet(name): print(f"Hello {name}") w = Wrong() w.greet("Tom")

报错:TypeError: greet() takes 1 positional argument but 2 were given。因为w.greet("Tom")被解释为Wrong.greet(w, "Tom"),你定义了只接收一个参数的方法,它收到了两个。

情况二:self被放在了最后或者中间

class Wrong2: def greet_me(name, self): print("...")

定义时第一个参数是name,调用时实例被塞进第一个位置,name接收到的其实是实例对象。如果你没用到name,那解释器可能不报错,但逻辑就乱了。如果用到了,多半会报TypeError,比如尝试把实例对象当成字符串拼接。

情况三:在类外直接调用未绑定方法

class Foo: def method(self): return "ok" f = Foo() Foo.method() # 少了实例参数

报错:TypeError: method() missing 1 required positional argument: 'self'。这时候的修复方式是用Foo.method(f)显式传实例,或者用f.method()让Python帮你自动传。

5.2 名称错误:self.name没有绑定,但方法里用了它

class Broken: def show(self): print(self.name) b = Broken() b.show()

报错:AttributeError: 'Broken' object has no attribute 'name'。意思是这个实例的__dict__里根本没有name这个键。你没在__init__里初始化它,也没在别的地方赋值,所以自然找不到。

这个报错看起来很基础,但它的价值是帮我们认清:self.name不是一个"声明",而是一个"查找"。Python不会帮你自动把方法里的变量变成实例属性,它需要你在实例的命名空间里先放入这个属性。忘记在__init__里初始化,或者初始化写成了局部变量,都会导致这种错误。

新手最容易踩的变体是:在__init__里没写self.,直接name = name,然后其他方法里用self.name,找来找去找不到。name = name只是创建了一个局部变量,在__init__执行完就销毁了,实例上啥也没留下。

5.3 作用域错觉:方法里的变量不加self就是局部变量

我刚学的时候一直有个错觉:方法里写个x = 1,感觉就像是"放到了对象里面"。其实完全不是。区别如下:

class ScopeDemo: def set_x(self, val): x = val # 局部变量,方法结束后就没了 self.x = val # 实例属性,一直存活 def get_local(self): return x if 'x' in locals() else "x不在局部变量里"

get_local里那个'x' in locals()的写法是为了演示用途,实际很少这么查。重点是理解:方法体内部是一个普通函数作用域,不加self.的变量优先级顺序是:局部变量 > 全局变量 > 内置名。self.x根本不参与这个顺序,它是"实例命名空间"里的东西,需要通过self点出来。

如果你方法里同时存在局部变量x和实例属性self.x,它们互不干扰但容易写混。看这个典型bug:

class Price: def __init__(self, price): price = price * 1.1 # 只改了局部变量,实例没收到 self.price = price # 想让实例收到,必须这样赋值

第一行price = price * 1.1只是把参数price重新赋了一个值,和实例一点关系没有。如果你误以为"参数名和属性名一样时会自动同步",那就会写出price = price * 1.1以后直接不管了,然后p.price还是原值,怎么都想不通。解决办法就是用self.price = price * 1.1一次性搞定。

5.4 继承场景里self的"多态"属性:self永远指向实际实例

最后一个让我从前懵到后的坑,是继承时self的指向。看这段:

class Animal: def __init__(self): self.sound = "非常通用的声音" def speak(self): print(self.sound) class Dog(Animal): def __init__(self): super().__init__() self.sound = "汪汪" class Cat(Animal): def __init__(self): super().__init__() self.sound = "喵喵" animals = [Dog(), Cat()] for a in animals: a.speak()

输出的分别是"汪汪"和"喵喵"。这个例子看着简单,但背后有个不容忽视的机制:super().__init__()执行时,self是子类的实例,而不是父类的实例。所以父类__init__里的self.sound = "非常通用的声音",赋值给了Dog实例或Cat实例的属性空间;子类随后再赋值self.sound = "汪汪",把同一个属性覆盖掉了。

这就引出一个实用结论:如果父类__init__里建立了很多基础属性,子类最好先super().__init__()再覆盖或增加自己特有的属性,否则可能出现属性缺失。反过来,如果你在父类方法里调用了一个只在子类中存在的方法,Python也不会报错(只要实际实例有这个方法就行)——因为self永远指向实际创建的那个实例对象。这种"鸭子类型"的灵活性是Python面向对象的特色,但也要求你写代码时想清楚self在继承链上到底是谁。

6. 从self到面向对象:新手最舒服的一条学习路径

学self不能只背语法,得把它放到"面向对象"整个前提下理解。我给一个比较顺的路线,按这个顺序走,self和它周围的概念都能在一个框架里串起来:

6.1 先理解"对象"是一个自带数据的函数集合

把类想象成一个"模具",实例是模具压出来的"产品"。产品自带属性(数据)和方法(功能)。self就是连接"产品自身"和"产品功能"的那根线——每个功能(方法)在执行时,都知道"我是哪一个产品"。

class 模具: def __init__(self, 材料): self.材料 = 材料 def 描述(self): print(f"我是{self.材料}做的") x = 模具("木头") y = 模具("金属") x.描述() # 我是木头做的 y.描述() # 我是金属做的

self的价值就在这个对比里一目了然:同样的方法,不同的实例,各自读到各自的材料。没有self,方法就无法知道该去读哪个对象的属性。

6.2 用dir()和__dict__探索内置机制

有时候看书不如动手。我强烈建议新手在交互环境下做一次实验:

class Exp: class_attr = "class" def __init__(self): self.instance_attr = "instance" e = Exp() print(e.__dict__) # 只看实例属性:{'instance_attr': 'instance'} print(Exp.__dict__) # 类的属性字典,里面有class_attr、方法等 print(type(e)) # <class '__main__.Exp'> print(e.__class__) # <class '__main__.Exp'>

dir(e)能看到一大堆__xxx__的方法,这些都是"魔法方法",__init__、__str__、__repr__等。你逐个把它们改成自己的实现,就能明白"Python里一切皆对象"这句话的分量——连__add__这种运算符重载,本质上也是一个带self的方法。你写a + b,解释器实际调用的是a.__add__(b),这个写法是不是看着眼熟?没错,self在这套机制里无处不在。

6.3 练习建议:用三个小任务巩固

光看不练很容易"眼会手废"。我给自己设计过三个小练习,效果很好,你也可以试试:

练习一:改造一个普通程序为类版本。随便写一个记账小脚本(输入金额、算总和的简单逻辑),先面向过程实现,再改成类,让所有操作都变成self.xxx。改的过程中你会自然遇到"为什么这里要self""这个变量到底放哪"的问题。

练习二:设计一个带计数的类。要求:每次创建实例时,实例编号自动增长(提示:类属性 +__init__里用self.__class__.count += 1)。这个练习会把实例属性和类属性的区别彻底打通。

练习三:写一个"银行卡"类。包含属性:持卡人、余额、每日限额;方法:存钱、取钱、查询余额。取钱要校验余额和限额。写这个能让你把self在方法间传数据的用法练熟,而且很贴近实际业务。

6.4 阅读源码时的观察点

等你基础打牢,我建议去读三个标准库源码片段,每次都会加深一层理解:

  • collections里的namedtuple源码:看它如何用元类动态创建类,如何让生成的类方法避开self(它的方法实现走的是"属性查找"路线);
  • datetime模块的date类:看它怎么用@classmethod提供fromtimestamp这类备选构造方法,理解cls和self的配合;
  • functools里的total_ordering装饰器:看它如何利用"约定的比较方法"(如__lt__、__eq__)来推导其他比较方法,理解"方法名不过是属性字典里的字符串"。

这三个模块都不长,但信息密度很大。读源码时带着一个问题:"如果这里没有self,这段逻辑还能写成什么样?"带着问题读,收获会完全不一样。

7. 最后写给我的"待精简流水账":用一句话总结self

回顾这篇学习笔记一开始的样子:标题写着"待精简",内容确实是流水账——想到哪儿写到哪儿。现在从头整理一遍,发现self这个知识点兜兜转转,最终落到了三个核心句子上:

  1. self是实例对象自己的引用,方法之所以要它,是因为Python用"显式参数"来完成方法与对象的绑定;
  2. 没有self,方法就是普通函数;有了self,方法才能操作"当前这个对象"的属性、调用"当前这个对象"的其他方法;
  3. self不是关键字,但它是全社区统一的约定,用不用它取决于你的规范意识,而不取决于解释器。

我自己的体会是,学self最忌讳死记"定义时写、调用时不写"这个口诀。只有理解了"点号调用方法 = 类方法名传实例进去",你才能在各种报错面前不慌——因为报错信息里的takes N arguments讲的不是你想的那个参数个数,而是解释器视角下的真实传参。

最后分享一个小技巧:遇到self相关的报错,别急着查资料,先在报错信息里找到"方法名"和"给出的参数个数",然后问自己三个问题——这个方法是实例方法还是类方法?我调用时用的是实例还是类?方法定义里的第一个参数是不是self?绝大多数问题都能在这三个问题里找到答案。这比任何教程都靠谱。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询