ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

深入理解Python self:实例方法、类方法与面向对象底层逻辑

深入理解Python self:实例方法、类方法与面向对象底层逻辑 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解释器实际执行的是两步找出d这个对象的类型也就是Dog这个类从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.nameself.age就是p.age。一句话记住self是正在被操作的那个具体对象自己。3.2 对象关联性关键点多实例各自持有自己的selfself最容易误会的点在于它不是一个共享的全局变量而是每次调用都跟着具体实例走的。看这个细节class Counter: def __init__(self, initial0): self.count initial def add(self, step1): self.count step return self.count a Counter() b Counter(10) a.add() a.add() b.add() print(a.count) # 2 print(b.count) # 11a和b是两个完全独立的空间a.add()里的self是ab.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和linterpylint、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报错我把它分成三种来源每种都有不同的修法情况一定义方法时忘了写selfclass Wrong: def greet(name): print(fHello {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它们互不干扰但容易写混。看这个典型bugclass 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__.Expdir(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这个知识点兜兜转转最终落到了三个核心句子上self是实例对象自己的引用方法之所以要它是因为Python用显式参数来完成方法与对象的绑定没有self方法就是普通函数有了self方法才能操作当前这个对象的属性、调用当前这个对象的其他方法self不是关键字但它是全社区统一的约定用不用它取决于你的规范意识而不取决于解释器。我自己的体会是学self最忌讳死记定义时写、调用时不写这个口诀。只有理解了点号调用方法 类方法名传实例进去你才能在各种报错面前不慌——因为报错信息里的takes N arguments讲的不是你想的那个参数个数而是解释器视角下的真实传参。最后分享一个小技巧遇到self相关的报错别急着查资料先在报错信息里找到方法名和给出的参数个数然后问自己三个问题——这个方法是实例方法还是类方法我调用时用的是实例还是类方法定义里的第一个参数是不是self绝大多数问题都能在这三个问题里找到答案。这比任何教程都靠谱。
返回列表