资讯动态

Python类与继承核心解析:从self到super()的完整指南

发布时间:2026/8/30 9:07:28 来源:尧图企业网站定制
很多 Python 学习者会有一种很典型的体验函数写得很顺手循环、列表、字典也能玩得转但一看到__init__、self、super()就觉得自己始终没有真正跨进面向对象编程的门槛。尤其是跟着 MIT 6.0001 这门计算机科学导论课程走的时候第 9 讲“Python 类和继承”几乎是大部分人的第一个分水岭跨过去你能从本质上理解“Python 中一切皆对象”这句话跨不过去后面涉及抽象、接口、代码组织的内容就会越学越吃力。这篇文章就来把第 9 讲的核心逻辑拆成一条清晰的线类是什么、对象怎么创建、方法为什么需要self、继承到底解决了什么问题、super()和 MRO 背后发生了什么。读完你可以动手把示例代码敲一遍跑通整套流程也知道遇到报错时应该从哪里开始排查。先给一个明确判断类和继承不是 Python 的语法负担而是组织代码的工程工具。脚本写三五十行可以靠函数写到几百行甚至上千行时如果数据和操作仍然散成一团维护成本就会失控。MIT 6.0001 之所以专门用一讲来讲类和继承不是因为这门课想要“炫语法”而是因为面向对象编程是计算机工程师必备的抽象能力。这一讲之后课程会快速进入算法、数据结构、测试和工程化内容如果类和继承没有学扎实后面的学习一定会反复回来补课。说明一下本文不是课堂讲义的逐字稿而是结合课程主题做的系统梳理和工程化补充。内容覆盖类的本质、实例化、方法机制、特殊方法、继承链、MRO、抽象类以及常见坑点既能支撑初学者把概念学透也能帮助有经验的朋友查漏补缺。1. 为什么 MIT 6.0001 要专门用一讲讲类和继承先看 MIT 6.0001 的课程结构。前面几讲讲 Python 基础语法、循环、字符串、元组、列表和函数到第 9 讲时学生已经能写出真正可以运行的简单程序。但这里会遇到一个很现实的问题用纯函数式思路写程序代码规模上去之后会越来越难组织。举一个最简单的场景。假设你要在程序里管理员工信息姓名、部门和工资用纯函数写法通常是这样# 文件路径employees_functional.py def create_employee(name, department, salary): return {name: name, department: department, salary: salary} def increase_salary(employee, amount): employee[salary] amount def describe(employee): return f{employee[name]} in {employee[department]}, salary{employee[salary]} e1 create_employee(Alice, Engineering, 10000) increase_salary(e1, 2000) print(describe(e1))程序只有几十行时这种写法没有问题。但如果项目里有几十种实体每个实体都有对应的操作函数命名会开始冲突参数顺序会变得难以记忆函数之间的依赖关系也会越来越模糊。比如increase_salary只接受特定结构的字典一旦字典字段名写错运行时才会报错错误定位成本非常高。类和对象的出现本质上是把这些“散装的函数 散装的数据”打包在一起。对象内部维护自己的状态外部通过方法调用与对象交互。这样做的好处至少有三个数据和行为在一起看到employee.increase_salary(2000)就知道这是对某个员工对象操作代码组织更清晰每个类负责自己的职责调用方只需要关心类对外暴露的方法便于扩展通过继承可以在不修改原有代码的情况下增加新的类型或覆盖原有行为。所以 MIT 6.0001 把类和继承放在这个位置正好是学生从“能写小程序”走向“能写复杂程序”的转折点。理解了这一点也就理解了为什么很多公司面试时喜欢问“什么是面向对象为什么需要它”——这不是在考定义而是在考你有没有经历过代码规模变大后的痛苦。2. 类是什么把数据和行为打包2.1 类是一种“模板”对象是“实例”类在 Python 中是一个对象工厂。用一句话概括类是模板对象是根据模板创建出来的具体实例。用现实生活类比如果把“员工档案表”看作一个类那么“员工 Alice 的具体档案”就是一个对象。类定义了员工有哪些字段、能执行哪些操作对象则是这份定义在内存中的一次具体落地。# 文件路径employee_class.py class Employee: def __init__(self, name, department, salary): self.name name self.department department self.salary salary def increase_salary(self, amount): self.salary amount return self.salary def describe(self): return f{self.name} in {self.department}, salary{self.salary}运行方式e1 Employee(Alice, Engineering, 10000) e1.increase_salary(2000) print(e1.describe())这段代码出现了一个最关键的名词__init__。在 Python 中__init__是“初始化方法”它在对象刚创建出来、内存已经分配后自动调用用来设置对象的初始状态。注意__init__并不是严格意义上的“构造函数”真正分配内存和创建对象的是__new__但对于绝大多数日常开发而言把__init__理解为“创建对象时执行的那段初始化逻辑”就够了。self是另一个绕不开的概念。它表示当前实例本身。e1.increase_salary(2000)在 Python 内部等价于Employee.increase_salary(e1, 2000)。也就是说方法定义时的第一个参数self不是你手动传的而是 Python 在调用方法时自动绑定的。这也是初学者最容易困惑的地方为什么函数定义时多了一个参数调用时却不传2.2 从函数式写法到类写法变化在哪里把前面函数式写法和类写法对比一下维度函数式写法类写法数据结构普通字典字段名靠约定类中明确定义属性操作函数独立函数参数较多绑定在类上实例调用数据完整性字典结构错误只能运行时发现初始化逻辑集中结构更稳定扩展方式新增函数逻辑分散新增类或继承结构清晰类写法的代价是概念变多了代码结构变复杂了。这也解释了为什么有人觉得 Python 入门容易深入难——前面用脚本思维写代码很轻松一旦要设计类层次就涉及抽象能力、设计判断和工程经验。3. 方法、self 和对象状态3.1 方法不只是函数Python 中的方法本质上也是一种函数但它和普通函数有一个关键区别方法绑定在对象上调用时自动接收对象本身作为第一个参数。# 文件路径method_demo.py class Counter: def __init__(self): self.count 0 def increment(self, step1): self.count step return self.count c Counter() print(c.increment()) # 1 print(c.increment(5)) # 6 print(Counter.increment(c, 2)) # 8等价于 c.increment(2)可以看到c.increment()和Counter.increment(c)是完全等价的。理解这一点很重要因为当你调试TypeError: increment() missing 1 required positional argument这类报错时本质就是在提醒你方法没有绑定到实例上常见于“直接用类名调用实例方法”或者“装饰器改变方法后的错误调用”。3.2 类属性与实例属性Python 的类里可以定义两种属性类属性和实例属性。# 文件路径attr_demo.py class Employee: company Example Corp # 类属性所有实例共享 def __init__(self, name): self.name name # 实例属性每个实例独立 e1 Employee(Alice) e2 Employee(Bob) print(e1.company) # Example Corp print(e2.company) # Example Corp Employee.company New Corp print(e1.company) # New Corp e1.company Per-Instance Corp print(e1.company) # Per-Instance Corp print(e2.company) # New Corp没有受 e1 的影响这里的核心机制是属性查找顺序实例属性优先于类属性。当你读取e1.company时Python 先到e1.__dict__里找如果没找到再到Employee.__dict__里找。当你写e1.company ...时Python 会在e1这个实例上创建新的属性而不是修改类属性。这个规则在实际项目中很容易踩坑。如果类属性是可变对象例如列表或字典并且类方法里修改了它所有实例都会受影响。所以工程上的建议是类属性只放不可变常量可变状态一律放到__init__中通过self定义。3.3 名称修饰与“伪私有”Python 中_name、__name、__name__三种写法含义完全不同name普通公开属性_name约定为“内部使用”只是一种规范不强制__name名称修饰Python 会把它改写成_ClassName__name__name__特殊方法或特殊属性用于 Python 内部协议。# 文件路径name_mangling_demo.py class Secret: def __init__(self): self.public 1 self._protected 2 self.__private 3 s Secret() print(s.public) # 1 print(s._protected) # 2可以使用但不建议跨类访问 # print(s.__private) # AttributeError print(s._Secret__private) # 3名称修饰后的真实属性名这里要强调一点__private并不是绝对不可访问的“私有字段”它只是一种避免命名冲突的改名机制。类设计中如果真的要隐藏内部状态靠__双下划线是不够的更可靠的方式是设计好对外接口让外部不要直接操作内部状态。4. 特殊方法让自定义类融入 Python 语法如果你只看普通方法的写法会觉得类和“带方法的字典”差不多。但 Python 真正强大的地方在于特殊方法。它们是双下划线包围的方法当对象参与某种语法操作时会被自动调用。4.1__str__与__repr__最直接的例子是打印对象。如果类中没有定义__str__print(obj)会输出类似__main__.Employee object at 0x7f...的地址信息这对调试几乎没有帮助。# 文件路径clock.py class Clock: def __init__(self, hours, minutes): self.hours hours self.minutes minutes def __str__(self): return f{self.hours:02d}:{self.minutes:02d} def __repr__(self): return fClock({self.hours}, {self.minutes}) c Clock(9, 30) print(c) # 09:30调用 __str__ print(repr(c)) # Clock(9, 30)调用 __repr__在工程实践中建议每个自定义类都至少实现__repr__。因为调试或日志输出时repr(obj)能给出更准确、更可重现的对象状态而__str__更偏向展示给用户看的格式。4.2__eq__与__lt__再来看两个更常用的特殊方法相等判断和比较排序。# 文件路径clock.py class Clock: def __init__(self, hours, minutes): self.hours hours self.minutes minutes def __str__(self): return f{self.hours:02d}:{self.minutes:02d} def __repr__(self): return fClock({self.hours}, {self.minutes}) def __eq__(self, other): if not isinstance(other, Clock): return False return self.hours other.hours and self.minutes other.minutes def __lt__(self, other): return (self.hours, self.minutes) (other.hours, other.minutes)定义__eq__之后两个内容相同的Clock对象用比较会返回True。定义__lt__之后Clock对象就能放进sorted()、max()、min()等需要比较大小的场景。times [Clock(8, 15), Clock(10, 5), Clock(9, 30)] print(sorted(times)) # [08:15, 09:30, 10:05]这种设计体现了“对象参与语法操作”的思路。Python 解释器看到a b实际调用的是a.__eq__(b)看到a b实际调用的是a.__lt__(b)。特殊方法不是给开发者手动调用的而是用来接入语言内置逻辑的。5. 继承子类如何复用并扩展父类5.1 继承解决什么问题继承的核心意图是在新类型与已有类型存在“is-a”关系时复用已有实现同时允许修改或扩展行为。“is-a”关系很好理解企鹅是一只鸟鸟是一个动物。那在代码里企鹅类应该继承鸟类鸟类应该继承动物类。反过来如果某个类和已有类只有代码相似的“has-a”或“can-do”关系就不一定适合用继承。MIT 6.0001 的课堂上常用动物层次来讲解继承这个例子非常经典。我们来复刻一个简化版本。5.2 一个完整的继承链# 文件路径animals.py class Animal: def __init__(self, name): self.name name def speak(self): raise NotImplementedError(子类必须实现 speak 方法) def __str__(self): return fAnimal({self.name}) class Bird(Animal): def __init__(self, name, wingspan): super().__init__(name) self.wingspan wingspan def speak(self): return chirp def fly(self): return f{self.name} is flying class Penguin(Bird): def __init__(self, name): super().__init__(name, wingspan60) self.flightless True def speak(self): return honk def fly(self): return f{self.name} cannot fly because its flightless测试代码a Animal(Generic) # print(a.speak()) # NotImplementedError bird Bird(Tweety, 20) print(bird.speak()) # chirp print(bird.fly()) # Tweety is flying penguin Penguin(Happy Feet) print(penguin.speak()) # honk print(penguin.fly()) # Happy Feet cannot fly because its flightless print(penguin.wingspan) # 60这段代码体现了继承的几个关键点Penguin(Bird)意味着企鹅类是鸟类的一个子类自动拥有鸟类的name、wingspan属性Penguin没有定义__init__时会调用父类的__init__一旦定义了__init__就必须显式通过super().__init__(...)调用父类初始化逻辑Penguin覆盖了speak和fly方法调用时执行的是子类版本而不是父类版本Animal.speak中用了raise NotImplementedError这是一种“占位约定”不推荐直接实例化一个没有实现speak的动物。5.3 方法覆盖与属性查找顺序Python 对象调用方法的规则可以总结为一个简单的查找顺序先在对象自己的__dict__中查找实例属性找不到就去类定义中查找类中没有就去父类中查找父类也没有就继续沿着继承链往上找直到object还找不到就抛出AttributeError。因此子类覆盖父类方法时不用担心“同时还存在一个旧方法会干扰”的问题。Python 的继承链是线性的子类中一旦定义同名方法调用就会直接命中子类版本。但是这里隐藏着一个重要问题如果子类方法中想调用父类原本的方法该怎么办答案就是super()。6. super()、多重继承与 MRO6.1 super() 的作用super()返回一个代理对象它的作用是在当前类的方法中沿着 MRO 找到下一个应该调用的方法。# 文件路径super_demo.py class A: def __init__(self): print(A.__init__) class B(A): def __init__(self): print(B.__init__ before super) super().__init__() print(B.__init__ after super)b B() # 输出顺序 # B.__init__ before super # A.__init__ # B.__init__ after super这段代码很好地展示了super()的流程。重要提醒如果子类定义了__init__但忘记调用super().__init__()父类的初始化逻辑就不会执行。这在多层继承中尤其危险可能导致对象的某些属性从未被设置直到后面调用方法时才报错。6.2 菱形继承与 MROPython 支持多重继承一个类可以同时继承多个父类。多重继承会带来菱形继承问题# 文件路径mro_demo.py class A: def who(self): print(A) class B(A): def who(self): print(B) super().who() class C(A): def who(self): print(C) super().who() class D(B, C): def who(self): print(D) super().who()运行d D() d.who() print(D.__mro__)输出D B C A (class __main__.D, class __main__.B, class __main__.C, class __main__.A, class object)这个结果可能出乎某些人的直觉D继承自B和C而B、C都继承自A。为什么who()的输出顺序是D - B - C - A而不是D - B - A - C - A原因在于 Python 3 使用 C3 线性化算法计算 MRO。它的原则是每个父类的 MRO 必须保持相对顺序同时子类排在父类之前。D的 MRO 中B在C之前那么B的所有父类在 MRO 中的顺序也保持相对位置但C中不重复出现的父类不能提前。因此A只出现在一次且排在C的后面。如果你要写继承链非常深的类可以用ClassName.__mro__查看完整的解析顺序这是排查多继承问题最快的工具。7. 继承还是组合层次设计判断学完继承之后很多初学者会进入另一个极端认为凡是可以复用的地方都应该用继承。这是个需要警惕的误区。继承不是代码复用的默认方案它表达的是类型之间的“is-a”关系。7.1 is-a 与 has-a继承适合“子类是一个更具体的父类”的场景例如Penguin是Bird组合适合“一个类拥有另一个类”的场景例如Car拥有Engine。# 文件路径composition_demo.py class Engine: def start(self): return Engine started class Car: def __init__(self): self.engine Engine() # 组合Car 拥有 Engine def start(self): return self.engine.start()组合的好处是类与类之间的耦合更松更容易替换、测试和控制依赖。Car 替换掉 Engine 实现时不需要改动 Car 的继承关系只需要注入不同的对象。7.2 抽象类与普通类的区别在继承层次设计中抽象类非常常见。Python 的abc模块专门提供了抽象基类机制。# 文件路径abstract_demo.py from abc import ABC, abstractmethod class Shape(ABC): abstractmethod def area(self): pass class Circle(Shape): def __init__(self, radius): self.radius radius def area(self): return 3.14159 * self.radius ** 2 class Square(Shape): def __init__(self, side): self.side side def area(self): return self.side ** 2# shape Shape() # TypeError: Cant instantiate abstract class Shape circle Circle(2) print(circle.area()) # 12.56636维度普通类抽象类能否直接实例化可以不可以是否有抽象方法通常没有至少一个abstractmethod主要作用创建可用的对象定义接口规范要求子类实现子类的约束不强必须实现抽象方法否则子类也无法实例化抽象类和普通类最本质的区别是抽象类强制规定了子类的行为契约。当团队多人协作时这种约束能力非常有用——父类定义好哪些方法必须实现子类开发者只需要按接口完成具体逻辑而不是各自发挥。7.3 组合优于继承的典型场景实际项目中下面这些场景要优先考虑组合而不是继承类之间只是“能用某种能力”比如会飞、会游泳优先用组合或鸭子类型继承层次超过两层还要继续扩展时建议重新设计只想复用父类某几个方法但不需要父类的全部状态时继承会引入大量无用属性遇到菱形继承或多重继承时能避免就避免优先把公共逻辑抽到 Mixin 或独立函数中。8. 常见问题与排查思路写类和继承时初学者和高频开发都会遇到一些典型问题。下面汇总了我见过和排查过的最常见情况。问题现象可能原因排查方式解决方案TypeError: __init__() missing 1 required positional argument创建对象时参数没有传够查看类__init__方法签名检查实例化代码确认参数数量AttributeError: Employee object has no attribute name子类__init__没有调用super().__init__()查看子类__init__是否调用了父类初始化在子类__init__中调用super().__init__(...)print 对象显示__main__.Employee object at 0x...没有实现__str__或__repr__检查类中是否定义了特殊方法添加__repr__需要时添加__str__修改一个实例的类属性其他实例也变了类属性是可变对象且通过实例方法修改了它检查属性是类属性还是实例属性将可变状态放到__init__中TypeError: speak() missing 1 required positional argument: self直接通过类名调用实例方法但没有传实例检查调用方式是Bird.speak()还是bird.speak()绑定实例调用或使用staticmethod子类覆盖方法后想复用父类方法却找不到没有使用super()检查覆盖方法内部是否调用了super().method()在覆盖方法中使用super()调用父类方法TypeError: Cant instantiate abstract class抽象类内部还有抽象方法没有实现检查子类是否实现了所有abstractmethod为所有抽象方法补上实现多重继承时初始化顺序不符合预期不理解 MRO打印Class.__mro__或使用help(Class)用 MRO 推断执行顺序必要时简化层次结构排查思路也遵循一个固定顺序先看报错堆栈最底层定位哪个文件哪一行再看那个方法有没有super()、有没有正确的初始化调用最后检查属性是在类属性还是实例属性上。多数类和继承的问题都能在“属性查找顺序 MRO 顺序”这两条规则里找到答案。9. 最佳实践与后续学习建议最后把我认为最有工程价值的实践经验整理成清单供你在实际项目中使用。1. 每个自定义类都实现__repr__。无论你是否打算打印对象__repr__都会让调试日志清晰很多。一个能直接复制回代码的__repr__比一个看不出内容的地址字符串有价值得多。2.self永远是实例方法第一个参数。类方法和静态方法分别用classmethod和staticmethod装饰不要凭感觉混用。工程上最常用的还是实例方法因为大多数业务都需要访问对象状态。3. 子类只要定义了__init__就一定要考虑是否调用super().__init__()。如果父类初始化有参数子类不传几乎必然会导致后续属性缺失。4. 优先组合谨慎继承。“is-a” 是真的继承关系“can-do” 和“has-a” 则更适合组合或行为委托。写继承前问自己一句这个新类真的是一种父类吗如果犹豫就不要用继承。5. 抽象基类用于定义接口而非堆业务逻辑。团队协作时抽象方法就是约定它比文档更能约束实现。不过 Python 是动态语言过度使用 ABC 也会让代码变得僵硬建议在需要强约束的公共接口上使用。6. 养成打印__mro__的习惯。每次遇到多继承先print(Class.__mro__)这个列表就是 Python 解释器眼中的真实继承顺序所有同名方法调用顺序、初始化顺序都在这里。如果接着往下学推荐三条路径一是研究 Python 内置对象的实现比如列表、字典为什么是对象它们的特殊方法如何驱动语法二是去读一点collections、abc、dataclasses等标准库模块看官方库如何设计类层次三是试着把一个用函数写的旧脚本重构为带类设计和测试用例的工程结构。这一讲之后MIT 6.0001 的内容会开始进入算法、查找排序、复杂度和工程化类和继承是理解后续所有内容的基础。能在这一讲上多花时间后面省下的时间会更多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价