ARTICLE DETAIL

资讯详情

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

Python中self彻底解析:从实例方法到类方法与静态方法

Python中self彻底解析:从实例方法到类方法与静态方法 1. 为什么self值得单独拎出来讲刚学Python那会儿我踩过最大的一个坑就是类里面的self。写函数写得好好的一进类的方法就报错提示少传了一个位置参数翻来覆去看代码也没发现哪里少了参数。后来才明白那个凭空多出来的参数就是self。这件事听起来很小但它卡住了大量初学者从会写脚本到会写类的这一步。self这个东西说穿了就是实例方法里的第一个参数指向调用这个方法的实例本身。它不是什么关键字也不是Python语法强制保留的魔法名字你完全可以把它写成this、me、甚至abc代码照样跑。但整个Python社区约定俗成用self所以你也应该用self不然别人读你的代码会觉得别扭。这篇文章想做的事情很明确把self从知道有这么个东西讲到彻底搞明白它为什么存在、什么时候用、什么时候不用、容易在哪里翻车。适合已经会写Python函数、准备进入面向对象阶段的朋友也适合写了很久类但一直对self半懂不懂的人。我会从底层机制讲到实操细节再讲到常见报错和排查思路尽量把每一个为什么都讲透。先给一个最直观的类比。把类想象成一张房屋设计图纸实例就是按照这张图纸盖出来的一栋栋真实房子。self就是当前这栋房子的指代词。当你在图纸上写给客厅刷漆这个动作时你得说明是给哪栋房子的客厅刷漆self干的就是这个指认工作。没有selfPython就不知道你操作的是张三家的客厅还是李四家的客厅。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无是是操作实例状态类方法clsclassmethod否是替代构造函数静态方法无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调用时自动传了实例方法定义第一个参数补上selfX object has no attribute Y属性未初始化就访问检查__init__赋值顺序name self is not defined在方法外使用了selfself只能在方法内使用5.2 一个真实排查案例有次同事写了个类调用方法时一直报takes 1 positional argument but 2 were given。代码大概是这样class Config: def load(data): print(data) c Config() c.load()问题出在load定义时没写selfPython调用c.load()时自动把c作为第一个参数传进去于是data接收了c但方法只定义了一个参数实际传了两个c和隐式的所以报给了2个但只接受1个。修复方法就是加上selfclass Config: def load(self): print(loading)这个报错信息其实很直白但初学者容易懵因为2 were given听起来像是自己多传了参数实际上是Python自动传的self没地方放。5.3 排查self问题的通用思路遇到self相关报错我一般按这个顺序排查看报错类型。missing self说明调用方式不对too many arguments说明定义少了selfattribute 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不需要就静态。最后分享一个小技巧。如果你不确定一个方法该用哪种类型先写成实例方法跑通了再根据实际需要改成类方法或静态方法。因为实例方法最灵活能访问的东西最多改起来也最容易。等代码稳定了再优化比一开始就纠结用哪种要高效得多。
返回列表