Python类与对象彻底搞懂:从实例化到继承的避坑指南

📅 发布时间:2026/10/16 8:15:36
Python类与对象彻底搞懂:从实例化到继承的避坑指南
我见过不少刚学完Python基础语法的人一到类与对象这里就被卡住了——明明函数、列表、字典都用得挺溜怎么一提到“类”就感觉抽象得像玄学最典型的问题就是“类和对象到底有什么区别为什么我非得用类直接写函数不也一样吗”先说结论类是代码世界的生产图纸对象是按图纸造出来的实体。我记得有一次给某初学者讲这个概念随口打了个比方类就像“点心模具”对象就是压出来的月饼、饼干模具可以反复使用每一块点心都有自己的花纹但烤焦了只能重新压一块绝不会影响模具本身。这个比方虽然简单但相当好用理解了这一层再看什么__init____、self都会顺很多。这篇文章不会跟你扯太多理论主要以“怎么想明白”为目标把类与对象拆开揉碎讲清楚包括实例化的本质、属性方法在内存里怎么回事、继承和组合该怎么选、还有那些初学者百分之百会踩的坑。适合已经掌握Python基础语法但在面向对象这块还有些模棱两可的朋友。1. 类和对象的底层关系模具与产品的生产逻辑1.1 为什么需要类把数据和操作捆成一个整体先想一个问题你写程序处理学生信息时最原始的做法是什么用三个列表一个存姓名一个存年龄一个存成绩。数据少的时候还好等数据多了、逻辑复杂了就会发现很别扭——你得时刻记得下标0对应姓名、下标1对应年龄一旦列表顺序乱了或者多存了一种数据整个程序的管理成本就上去了。类的本质就是把相关的数据和操作封装在一起。比如定义一个学生类里面既存姓名年龄成绩又自带计算平均分、打印信息这类方法。以后要操作某个学生的数据直接调用这个对象的方法就行不用在全局到处找对应的函数。这个“封装”的价值在项目规模稍微大一点的时候会体现得淋漓尽致。再进一步想现实世界中我们也是这么组织信息的。“学生”这个概念不是孤零零一个名字或一个分数而是一整套属性加上一整套行为。程序要模拟现实逻辑自然也需要这样一个载体。你定义的每一个类其实是在创造一种新的数据类型而这种类型恰好符合你的业务需求。1.2 实例化图纸本身不能住人楼房才是对象我特别强调初学者的一件事是类本身不存储具体数据它只是定义结构的“图纸”。你写class Student:里面写了name、age但这只是说明“学生这种类型会有这两个属性”并不代表已经存在任何一个具体学生。要真正创建出一个学生必须执行实例化s Student()。这个操作的本质是什么呢Python解释器会去类这个“图纸”里找一种叫__new__的东西分配一块新内存再交给__init__进行属性赋值最终把这块内存的引用交还给你存放在变量s中。从这一刻起s就是一个独立的学生对象它拥有自己独立的属性空间。图纸可以画无数张同款模具可以压无数块点心。同样地一个类可以实例化出无数个互相不干扰的对象。你给a.name 小明完全不会影响b.name 小红。我自己刚学的时候总以为对象之间会共享数据其实它们关系就像班级里的两个同学——都长得像“人”这个模板但谁考了多少分、谁写了作业完全是各自独立的事。1.3 第一份代码从定义到使用的完整链路来看一段最基础的代码class Student: school 某某中学 def __init__(self, name, age): self.name name self.age age def show_info(self): print(f{self.name}{self.age}岁在{self.school}读书) s1 Student(小明, 15) s2 Student(小红, 16) s1.show_info() s2.show_info()这里的school是类属性逻辑上属于“整个学校共享的信息”name和age是实例属性每个对象各自的show_info是方法。执行s1.show_info()时Python本质上是在调用Student.show_info(s1)这个细节在后面理解“方法绑定”时很关键。这段代码值得你亲手跑一遍然后思考一个问题如果把school也放进__init__里用self.school定义行为会有什么差别想明白这一点你就已经摸到类与对象的第一分水岭了。2.__init__不是构造函数它是一个初始化工人2.1 self 身上的谜团为什么每个方法都要写它很多初学Python的人对self非常抓狂为什么别人写Java不用显式传thisPython却非要在每个方法参数列表里放一个self答案是Python的设计哲学是“显式优于隐式”它把接收者对象明明白白写出来让你清楚知道一份数据是给哪个对象用的。当你在实例上调用方法时比如s1.show_info()底层会被翻译成Student.show_info(s1)self指向的就是那个实例自身。这个机制非常透明但也容易给新手造成误解——以为self是特殊关键字。其实它在Python里只是一个约定俗成的名字理论上你把它改成this、aaa程序照样能跑。当然没人会真这么干因为可读性会崩掉。关键在于self就是“当前那个正在被操作的实例”。类里的方法之所以必须写它是因为类只提供逻辑模板不绑定具体数据只有等实例传进来后才知道去读哪块内存里的属性。这有点像工厂流水线上的设备图纸写的是“放材料A到位置B”但不告诉你具体是哪块材料生产时必须由操作员把具体的材料递到机器面前。2.2 初始化不等于建造真正分配内存的是__new__这里要纠正一个流传很广的误解很多人管__init__叫构造函数其实严格来说并不是。Python里真正负责分配内存空间的是__new__方法它是一个类方法返回新创建对象的引用__init__只是在这个对象已经存在后负责给它的属性填值。所以s Student(小明, 15)这行代码实际经历了两个阶段先由__new__创建一块空白的学生对象内存再由__init__把name和age写进去。绝大多数情况下你不需要覆盖__new__它是给那些需要控制内存分配的进阶场景用的比如实现单例模式时。理解这个区别有什么实际价值最大的好处是你不会再错误地认为__init__能提前 return 一个别的对象。在__init__里写 return 非None值会直接报错因为它本质上是在做“初始化”不是在做“生产”。2.3 初始化的进阶用法参数设计决定使用体验__init__的参数设计直接影响你类的使用体验。我建议保持一个原则必须由调用方提供的信息才作为参数能推导出来的信息尽量在初始化内部计算有默认值可选的给默认值。举个例子class Circle: def __init__(self, radius, colorred): self.radius radius self.area 3.14 * radius * radius self.color color这里area不需要调用方手动传初始化的过程中顺带就算出来并存好。这样设计的好处是避免使用者为了算个面积到处导入数学模块坏处是如果半径变了面积不会自动更新。你想一下这个矛盾应该怎么化解我的习惯是像这种依赖其他属性动态计算的值不如用方法或者属性访问器去实时计算别偷懒一次性存死。很多人写__init__时把所有属性一股脑塞进去参数多达七八个调用方每次创建对象都像在填身份证申请表。我的建议是超过三个必传参数时考虑把部分参数分组到子对象里或者提供类方法替代构造函数。这个设计感是区分“能跑”和“好用”的分水岭。3. 属性的作用域类属性与实例属性不可不说的纠缠3.1 属性访问的顺序先找自己的再找大家的一个对象访问属性时Python内部遵循一套查找顺序先查实例自身有没有这个属性如果没有再去类里面找类里也没有就去父类找再找不到就抛出AttributeError。这个顺序可以简单记为“自己的优先于大家的”。这个机制带来一个常见现象你可以先给某个实例绑定一个和类属性同名的实例属性它会悄悄“遮蔽”掉类属性不影响其他实例。比如类里定了school 某某中学某天转来一个借读生你就可以只对单个实例执行s2.school 另一所学校其他同学的school还是原样。明白了这一点很多让人困惑的现象就有了解释空间。比如说为什么修改类名.属性会影响所有未覆盖该属性的实例而修改实例.属性只会影响这个实例。这不是Python在变魔法而是属性查找链路的自然结果。3.2 可变默认值的坑类属性背着共享的锅如果类属性是不可变类型字符串、数字、元组用类属性做共享常量一般没问题。但如果类属性是可变的比如列表、字典情况就麻烦了。来看这段代码class Bag: items [] def add(self, item): self.items.append(item) a Bag() b Bag() a.add(苹果) print(b.items) # 输出 [苹果]注意看items是类属性所有实例共享同一个列表。通过a加进去的元素b也能看到。很多初学者在设计“购物车”这类类时踩过这个坑每个实例本该有自己的物品清单结果全挤到同一个列表里去了。解决方案也很简单凡是每个实例应该有独立副本的都放到__init__里用self来绑定比如self.items []。这样每个实例执行初始化时都会得到一份全新的空列表互不干扰。这条规则我总结成一句话能放实例的就别放类里除非你明确需要“全家共享一份”的数据。3.3__slots__和属性管控内存优化与约束Python的类默认使用字典存储实例属性意味着每个实例都可以随时绑定新的属性非常灵活但代价是每个对象都要维护一个字典内存开销相对大。对于需要创建成千上万个对象的应用比如游戏中的粒子、大数据处理中间结果可以考虑使用__slots__。class Point: __slots__ (x, y) def __init__(self, x, y): self.x x self.y y声明__slots__后实例不再有__dict__只允许x、y这两个属性名任何尝试给实例绑定新属性的操作都会报错。这是一种带约束的取舍用灵活性换性能。不过我对新手通常不建议一上来就引入__slots__因为“对象可以动态添加属性”是Python的特性和乐趣所在过早限制会让调试变得更难。等你在真实项目中确实遇到内存瓶颈再来考虑这个优化也不迟——知道有这么个工具比急着用更重要。4. 方法的三张面孔实例方法、类方法、静态方法4.1 实例方法必须先有对象才谈逻辑类里面写的大多数方法第一个参数是self这类方法叫实例方法。它默认接收一个具体实例作为参数能访问这个实例的所有属性和其他方法。实例方法是最常用的因为它代表了“某个对象在这类场景下能干什么”。比如说一个send_message方法它需要读取self.phone才能给当前用户发消息这就是典型的实例方法——不同的手机号对应不同的发送动作。没有实例这个方法是没有意义的因为你连发给谁都不知道。调用时必须要有一个实例对象user.send_message(你好)。系统会自动把user传进方法的self参数里。很多新手在这里容易记错误以为User.send_message(你好)也能用结果发现报错缺少一个位置参数。原因就是类调用时不会自动传实例进去你就得手动传User.send_message(user, 你好)。4.2 类方法不需要实例但需要类本身classmethod装饰器修饰的方法第一个参数约定俗成是cls指向类本身而不是某个实例。类方法不能访问实例属性但可以访问类属性也可以调用其他类方法。它最主要的应用场景是做“备选的构造方式”。举个例子一个日期类既可以接收年月日直接创建也可以从字符串2024-05-01解析创建class Date: def __init__(self, year, month, day): self.year year self.month month self.day day classmethod def from_string(cls, s): y, m, d s.split(-) return cls(int(y), int(m), int(d))注意到from_string方法里用的是cls(...)而不是写死Date(...)这是一个非常重要的细节即使将来有人继承了这个类cls会自动指向子类工厂方法照样能产生正确类型的对象。这种写法叫“多态构造器”在实际项目中非常常用。4.3 静态方法只是恰好住在类里的普通函数staticmethod装饰器修饰的方法既不接收self也不接收cls它本质上就是一个放在类命名空间里的普通函数。那为什么不直接定义成模块级函数非要放进类里主要目的是代码组织当这个函数和类的概念强相关但不依赖任何类或实例数据时放进去能让使用者更直观地找到它。比如一个计算工具类里面有个validate_phone方法它只做格式校验不关心调用者是谁。用静态方法就很合适。好处是调用时不用创建实例直接用类名.方法名()就行。我自己的选型心得是逻辑里只要用到实例数据就必是实例方法逻辑主要面向类本身、要搞工厂构造的用类方法逻辑既不需要实例数据也不需要类数据纯粹是一段和该类领域相关的功能代码用静态方法。按这个标准去套一般不会选错。5. 继承与组合什么时候该“是一个”什么时候该“有一个”5.1 继承is-a 关系的表达方式继承解决的是“子类本质上是父类的一种”的问题。比如一个动物类有breathe()方法狗类继承动物类那么狗不用重新写呼吸逻辑天然就拥有这个方法。这种关系用代码来表达class Animal: def __init__(self, name): self.name name def breathe(self): print(呼吸中) class Dog(Animal): def bark(self): print(汪汪)继承最大的好处是代码复用和逻辑归类公共部分放在父类差异化部分放在子类。你还能在子类里重写父类的方法通过定义同名方法实现多态。但继承的坑也很深。用得最烂的地方是试图用继承表达那些表面相似、实际不相关的概念。比如因为“猫”和“狗”都有名字和年龄就把它们都继承自一个动物类这个没问题但如果“发动机”和“汽车”都占位你一冲动让发动机继承汽车那就乱套了——发动机不是一个“汽车”它是汽车的一个组成部分这里应该用组合而不是继承。5.2 组合更灵活、更常用的结构方式组合的表达方式是has-a关系即一个对象内部创建或持有另一个对象。回到上面那个例子一辆汽车应该有发动机、有轮胎、有底盘这些是组成汽车的部件class Engine: def start(self): print(引擎启动) class Car: def __init__(self): self.engine Engine() def go(self): self.engine.start()组合比继承更灵活原因是边界清晰、耦合度低。汽车的引擎换了型号只需要传递一个新的引擎对象进去汽车类本身根本不用动。而如果使用继承子类的行为被父类牢牢绑定想换个行为就要改动继承体系改动成本高得多。业界有句话叫“组合优于继承”在实际项目中我基本同意但并不是说继承完全不能用。在数据层次清晰、公共逻辑稳定、扩展点明确的情况下继承仍然是高效表达抽象关系的手段。重点不是“哪个更好”而是“这个关系到底是 is-a 还是 has-a”。5.3 多层继承的恐怖故事能绕就绕初学者特别容易写出一长串继承链A 继承 BB 继承 CC 继承 D。每一层听上去都合理但排错的时候你会疯掉——一个属性不知道是从哪一层来的一个方法被谁重写了也说不清改一处三层遭殃。这就是所谓的“多层继承陷阱”。我的建议是继承层级控制在两层以内最多不超过三层。如果发现第三层开始出现“为了复用一个小方法而强行继承”的冲动赶紧停下来思考是不是该用组合。把公共方法抽取到独立的小类里然后让需要的类通过组合持有它往往更干净。实际项目里还有个很容易被忽略的问题是菱形继承。比如D继承B和CB和C都继承A那么D的方法解析顺序会变得比较绕MRO容易把人绕晕。除非你能精准解释MRO算法否则尽量避免这种结构。6. 类与对象实战中的经典雷区全是我见过的真实事故6.1 调用时忘记加括号拿到的是类自己写s Student和s Student()是完全不同的。前者s指向的是类对象后者才是实例。这个问题别看低级我见过不止一个项目里因为漏写括号导致后面的代码全乱了报错信息还特别难懂什么“missing required positional argument”“object is not callable”一时间完全摸不着头脑。检查方法很简单打印一下变量看看输出是class __main__.Student还是__main__.Student object at 0x...。一看到是前者立刻意识到漏括号了。另外提醒一下如果确实想传类作为参数比如传给一个工厂函数那么不带括号才是对的要区分清楚这个用意。6.2 可变对象作为默认参数初学者的头号刺客虽然这个问题和类无直接关系但在类的方法里最常见。你肯定见过这种写法def __init__(self, items[]): self.items itemsPython的默认参数在函数定义时只被评估一次所以所有调用者共享同一个列表。第一次创建的实例往列表里加了东西第二次创建新实例时这个“默认空列表”已经不再为空了。这在类里极其隐蔽因为属性本身还是实例属性但初始值在背后共享了。正确写法是def __init__(self, itemsNone): self.items items if items is not None else []这么写的好处是每次执行初始化都会创建一个全新的列表彻底断掉共享关系。6.3 面向对象最重要的还不是语法是设计最后说点更高维度的体会。类与对象的语法其实半天就能学会真正难的是“怎么把需求合理拆成类”。常见的坏设计是类的一行代码都没有业务逻辑全是getter和setter或者一个类干了几十件事职责比瑞士军刀还多又或者类之间互相调用来调用去循环依赖剪不断理还乱。我个人的判断标准很简单拿到一段需求先试着用一句话描述这个类“是什么”。说不清楚或者要用二十个词才能描述这个类的设计就有问题。一个好的类名应该能直接流露它的职责。如果哪天你重构代码时发现某类的职责越缩越小到最后整个类都可以删掉那说明你当初设计时聚合粒度搞小了。用组合代替继承、用类方法做工厂、用不可变设计减少隐式共享这些理念都是慢慢在实践中积累的。类与对象不是背概念是建立一种组织代码的思维方式。你在真实项目中每写一次类都会比上一次更知道边界在哪里。我自己的体会是学类与对象最有效的办法不是去刷一堆理论题而是手上找个真实小项目把之前所有“函数全局变量”的实现改成面向对象的版本然后观察代码发生了什么变化。改完你就知道面向对象真正解决的问题不是让程序跑得快而是让程序在长大之后还理得清、改得动。这一步迈过去后面那些什么继承多态封装之类的东西你自然会觉得顺理成章。