Python self关键字详解:从实例方法到面向对象的本质理解

发布时间:2026/10/10 8:05:48
Python self关键字详解:从实例方法到面向对象的本质理解 我学Python的时候特别喜欢记流水账07篇恰好是self关键字。当时笔记里只写了五行字“方法里要带self不带会报错带了这个方法才能访问对象的属性__init__里的self代表新对象self可以改名待精简。”后来回头一整理发现self背后的东西远比那句笔记深得多。self表面看只是一个参数名实际上承担着整个面向对象机制里“对象识别”的核心职责方法调用、实例属性、继承、类方法全部绕着它转。这篇文章就把当年的流水账重新梳理展开成一份入门级但又不流于表面的总结。适合刚接触Python面向对象、经常被self绕晕的新手也适合写得出代码、但不太清楚“为什么必须这样写”的进阶者。1. 先搞明白self不是Python的关键字是“第一个形参”1.1 被误认为是“魔法变量”的self很多人第一次看到def __init__(self):以为self是Python内置的特殊变量。我坦白说我最初也这样想甚至觉得“self”这个名字本身就带某种隐藏力量好像写上它类就能自动变成一坨魔法。后来翻了官方文档才理解self不是关键字不是保留字它只是一个普通参数的约定名字。Python文档里self、args、kwargs这类命名都属于“惯例”语法层面没有任何强制要求。你可以把它改成this、改成obj、改成anything程序照样运行。我刚开始学的时候专门做过实验把一个方法里的self全改成“mine”功能完全不受影响。这个事实会颠覆很多人的认知既然不是关键字为什么每个方法都要写self因为Python不像Java那样隐式提供一个this变量它把实例方法设计成“普通函数 第一个参数为实例引用”的形态。你不在签名里留位置解释器就没有地方塞“调用者”这个信息。这里可以和生活类比一下self就像你入职时领到的工牌。每个员工的行为规范和操作流程都差不多但操作电脑时一定要先刷自己的工牌系统才知道“现在是谁在干活”。Python里的self就是这个工牌方法执行时解释器自动把“谁调用了这个方法”塞到第一个参数让方法知道自己在哪个对象上运行。没有这张工牌对象状态就无从谈起。1.2 从“无self”到“有self”的对比先写一段最简单的代码class Dog: def bark(self): print(汪汪) d Dog() d.bark() # 汪汪这看起来没什么稀奇。那如果去掉self呢class Dog: def bark(): print(汪汪) d Dog() d.bark() # TypeError: bark() takes 0 positional arguments but 1 was given这个报错非常经典明明是“没有参数”的函数系统却说“给了一个参数”。多出来的那个参数正是被自动传入的实例d。理解了这一点你就解开了Python初学阶段最常见的一个谜。更直白的等价写法是d.bark()等同于Dog.bark(d)。当你调用一个实例方法Python会从“实例方法绑定机制”里取出函数再把当前实例作为第一个实参传进去这个实参就是你写在签名里的self。因此第一个参数不是可有可无它是方法运行时的“主视角”。2. self在方法调用中到底扮演什么角色2.1 绑定方法与普通函数的区别在Python中d.bark和Dog.bark是两种完全不同的东西。我学习时专门验证过这一点一个是“已绑定方法”一个是“普通函数对象”。class Cat: def say(self): print(f我是{id(self)}号猫喵) c Cat() print(c.say) # bound method Cat.say of __main__.Cat object at 0x... print(Cat.say) # function Cat.say at 0x...c.say这个绑定方法内部已经记住了实例c所以调用c.say()时不需要再传参数Cat.say是存在类字典里的原始函数它没有绑定任何实例调用时要显式传一个实例才行Cat.say(c)。我给新手的记忆方法是实例方法定义时的第一个参数就是“占位符”用来接收未来真正的调用者。当你通过实例访问方法时Python自动完成占位填充而你通过类名直接访问时填充动作不会发生。这个区别在后面对装饰器、类方法、静态方法的理解中都会反复出现。2.2 self参数在装饰器场景下的微妙影响如果你给实例方法包一个装饰器self会被“透传”还是被“拦截”取决于装饰器怎么处理参数。这个坑是我用装饰器给类方法加日志时遇到的。import functools def log(func): functools.wraps(func) def wrapper(*args, **kwargs): print(调用前) return func(*args, **kwargs) return wrapper class Demo: log def hello(self): print(hello) d Demo() d.hello() # 调用前 hello一切正常这里能正常工作的原因是wrapper用了(*args, **kwargs)它原样接收第一个位置参数self再原样传给原始函数。如果你在装饰器里自作主张地“帮”调用方消耗掉第一个参数或者返回一个完全不同的可调用对象self就会丢原方法收到的就不是实例而是None或者其他错误的东西。这类问题在Flask、Django的装饰器实践中也很常见。排查时不要怀疑Python先看装饰器返回的那一层函数签名里有没有保住第一个参数的位置。3. 面向对象开发中self的常用姿态3.1 __init__中最重要的动作把参数挂到实例上几乎每个实例方法都会用到self但用得最密集的地方一定是__init__。它是对象的初始化方法self代表刚被创建出来的新对象self.属性 参数的本质就是往当前对象的属性空间里写数据。class Student: def __init__(self, name, score): self.name name self.score score如果我在初始化方法里图省事把self.name写成name就会出现两种糟糕情况写name name时Python编译器会把两侧的name都当成同一个局部变量右侧读到的还是尚未赋值的局部变量直接抛UnboundLocalError就算你用一个临时变量绕开了这个报错只要没有写self.xxx这个信息也不会保存到实例上之后访问student.name照样报AttributeError。所以我慢慢养成一个习惯凡是想让对象长期保存的数据一律写成 self.xxx凡是方法内部临时用一下的中间变量才写普通局部变量。这个划分看起来很简单但真能杜绝一大批初学者最常犯的错误。3.2 类属性与实例属性self会“骗人”这里要聊的坑很多写了两三个月项目的人也会踩。看这个例子class Counter: count 0 # 类属性 def increment(self): self.count 1调用c1.increment()之后类上的count到底变了吗表面看好像加了实际上没有。self.count 1等价于先去查找count再往实例自己的字典里写入一个新的实例属性count。也就是说你并没有修改类属性而是创建了一个同名实例属性把类属性“盖住”了。想真正修改类属性得用type(self).count 1或者Counter.count 1。这个细节和self的解析顺序直接相关self查找属性时先看实例自己的空间找不到才去类上找。把实例属性和类属性分清楚是面向对象学习路上一个重要的分水岭也是很多共享状态bug的来源。3.3 继承链路上的self始终是“最初的那个实例”多写几个类之后你会发现self会跟着继承走。子类调用父类方法时父类方法拿到的self仍然是子类实例绝不会被偷偷替换成父类对象。这一点初学者非常容易搞错。class Animal: def __init__(self, name): self.name name class Bird(Animal): def __init__(self, name, skill): super().__init__(name) # self透传给父类 self.skill skillsuper().__init__(name)完整含义是把当前这个bird对象传给父类的初始化方法让父类初始化逻辑在这个对象上执行而不是“创建一个新父类对象”。你甚至可以手动写成Animal.__init__(self, name)效果一样。理解了这一点后面看多继承、MRO方法解析顺序时就不会慌因为无论调用链多长self始终是起点那个实例。4. 初学self最容易踩的四个坑4.1 坑一忘写self被参数个数报错弄懵这个坑实在常见。新手在类里写class Person: def introduce(): print(我是人) p Person() p.introduce() # TypeError: introduce() takes 0 positional arguments but 1 was given报错中文翻译是“introduce()接受0个位置参数但给了1个”。很多新手会怀疑是不是Python自动加了什么隐藏参数其实那个“1个参数”就是实例p。实例方法天然要接收实例本身你没在签名里留self这个参数就无处安放。排查方式非常简单如果函数是类里的实例方法签名第一个位置写self如果它根本不需要操作实例可以考虑用staticmethod显式声明成静态方法这样调用时Python就不会往里塞self。4.2 坑二在类体里写self有段时间我写类会在方法外面写self.name 张三以为像Java一样先声明字段。在Python里这是错的类体中没有self。self只存在于实例方法的局部作用域里类体里只有“类属性”这回事。class Person: # 下面这行在类体里self没有定义 name 张三 # 这是类属性不需要self def __init__(self): self.name 李四 # 实例属性需要self类属性的本质是定义在类对象上的变量所有实例共享同一个初始引用实例属性则是每个实例自己的。如果你要的是“所有实例共享”的数据用类属性如果你要的是“每个实例各不相同”的状态请到__init__里用self定义。把这两层空间分开理解很多“改一个对象全变样”的诡异现象都能解释清楚。4.3 坑三可变默认参数和self联手坑你下面这段代码几乎出现在每个资深开发者的实战踩坑清单里class Box: def __init__(self, items[]): self.items items b1 Box() b1.items.append(苹果) b2 Box() print(b2.items) # [苹果]为什么Box()没传参数b2却能看到b1放进去的苹果因为items[]这个默认列表在函数定义时只创建了一次。后续每次调用如果没有传入新列表拿到的都是同一个列表对象。self.items items做的只是把同一个列表引用保存起来三个实例最终指向同一个列表数据自然就串了。正确写法是用None作为哨兵class Box: def __init__(self, itemsNone): self.items [] if items is None else items不要小看这一行改动不少生产环境里的“状态串扰”bug源头就是可变默认参数进了__init__。4.4 坑四self.xxx 的解析顺序比你想象的复杂self.xxx的查找顺序是“实例属性 - 类属性 - 父类属性”这个顺序会带来不少连锁反应。前面讲的Counter例子就是最典型的情况。更隐蔽的是当类属性是不可变对象int、str、tuple时写self.count 1只会创建实例副本当类属性是可变对象list、dict时调用self.queue.append(...)会原地修改共享对象所有实例都受影响。想判断自己到底在改实例属性还是类属性最直观的方法是看vars(self)和vars(类名)。在交互式环境或调试器里跑一下数据落在哪个空间一目了然。另外如果某个self属性保存的是对象自己或者其他容器对象的引用在做JSON序列化或深拷贝时会遇到类似“self referencing loop detected”的循环引用报错本质就是对象图里形成了环。处理办法是在序列化前对属性做转换或者自己写递归逻辑手动控制。5. 进阶self之外还有一个cls5.1 cls就是“类版本”的selfself是实例方法的第一个参数接收的是具体实例Python还专门提供了classmethod它的第一个参数习惯叫cls接收的是类本身。理解自我之后理解cls就是顺水推舟的事。class Tool: def __init__(self, name): self.name name classmethod def create(cls, name): obj cls(name) # 通过cls创建实例 obj.name f工具-{name} return obj tool Tool.create(锤子) print(tool.name) # 工具-锤子cls在这里表示“用当前这个类来创建对象”。和self一样子类调用时cls就是子类。要是你在classmethod里写死了Tool(name)子类继承复用这套工厂逻辑时创建出来的对象类型就会出错。正因为cls保留了“调用者是谁”的信息工厂方法才特别喜欢用classmethod实现。5.2 什么时候该用staticmethod还有一种方法既不需要self也不需要cls叫静态方法。它就像放在类命名空间里的普通函数仅仅为了组织代码方便。class MathUtils: staticmethod def double(x): return x * 2判断标准可以这样来方法体内不访问任何实例属性也不需要读取类属性那它就是静态方法。很多人的习惯是先写self再说其实改成staticmethod之后代码意图更明确测试也更方便。因为这个方法已经不绑定具体对象状态了你甚至可以把它抽成模块级函数放到外面。需要注意的是staticmethod里不能调用self也不能调用cls否则会直接报错。这时候应该反问自己我到底需要一个实例方法、类方法还是纯函数想清楚这个问题比记住各种冠名的区别更有价值。6. 回看这篇“流水账”我对self学习的几点体会6.1 调试时直接用断点看self当年我是靠“打印大法”理解self的。在每个方法里放一行print(self)或者用vars(self)输出属性字典能亲眼看到对象的内存地址和属性空间。现在用PyCharm或者VSCode调试器更简单打个断点运行到方法内部左边的Variables面板里点开self所有实例属性、类属性、对象ID全都列在那里。我建议你手写一个类在方法里加断点然后观察self指向的对象。多看几次比背十遍规则都管用。特别是当你想弄明白实例属性和类属性区别的时候调试器里看数据落在哪个空间一眼就清楚了。6.2 读源码时怎么快速识别self阅读开源项目时看到方法签名第一个参数是self就知道它是实例方法函数里的self.xxx都在操作实例属性看到第一个参数是cls就去关注类属性和子类逻辑两者都没有就是纯工具函数。按这个规则过滤你能很快梳理出一个类的“数据面”和“逻辑面”。还有一个更不容易踩雷的小技巧当源码里某个方法签名带self但整个方法体完全没用到self你可以怀疑这个方法其实应该改成staticmethod。不少老代码就是这样方法挂在类上但压根不依赖实例状态改成staticmethod之后语义更清晰调用也更轻量。6.3 别把self当成背诵内容去理解它为什么存在我见过太多人把“方法里必须写self”当成死记硬背的口诀一旦遇到装饰器、继承、类方法组合的场景就乱了。实际上self不是一个需要背的语法规则它是一个“绑定关系”的产物对象调用方法对象本身成为方法里的可用变量。最后分享一个我愿意重复一百遍的心法当你写def f(self)时把它理解成“f要操作的是这个对象自己”。以后遇到所有带self的代码都可以翻译成“从对象自己身上拿数据、改数据、调方法”。一旦建立起这个翻译能力self这个坎就算彻底迈过去了后面再学装饰器、元类、描述符协议都会轻松很多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询