news 2026/9/18 15:48:14

Python面向对象编程:类与对象、self、继承与多态实战解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python面向对象编程:类与对象、self、继承与多态实战解析

简介:这份PPT课件面向零基础或刚接触编程的Python学习者,聚焦面向对象编程这一进阶主题,帮助读者从面向过程的线性思维过渡到以数据为核心的组织方式。课件共64页,围绕类与实例、属性与方法、封装、继承、多态三大原则展开,并用Dog类、动物分类层次等贴近现实的例子讲解抽象与实例化的关系,同时回扣算法、数据结构、函数、模块等前置基础。资源包内含1个pptx文件,约962KB,图文排版完整、知识点衔接清晰,适合课堂讲授、自学跟练或作为备课参考,可配合前序课程按章节推进。目前已有282人学习下载,读者可借助它理清类与对象的定义边界、方法调用的实现思路,掌握继承层次与多态接口的设计逻辑,为后续异常处理、模块化开发与项目实践打下面向对象的基础。

1. 从 64 页 Python 面向对象课件说起:类与对象到底解决什么问题

很多人把 python基础语法 里的列表、字典、循环、函数都跑通了,代码却还是越写越乱:一个字典里塞了七八个键,函数参数越传越长,改一个字段要全局搜索确认没人漏改。第 14 讲那份 64 页的 Python 面向对象编程 课件,正是冲着这道断层来的。它要回答的不是「类是什么」这种定义题,而是当一堆数据和操作它们的行为纠缠在一起时,怎么打包成一个整体,让调用方少记参数、让修改只发生在一处。

这份内容适合两类人。一类是刚装完 python,在 vscode python环境配置 或 pycharm配置python环境 里跑通了第一个print的入门者,需要从「写脚本」过渡到「写模块」;另一类是有其他语言背景、却被 Python 的self、「没有 interface」、「类变量怎么会串味」搞晕的人。下面把课件里最容易被一页 PPT 带过的部分拆开:self绑定的是谁、__init__何时执行、继承链上的属性查找顺序、以及为什么类属性会被所有实例共享。

2. Python 类与对象的最小可用写法

2.1 用 class 定义第一个类:init与 self 的参数含义

先看一段能直接跑的代码,在终端或编辑器里新建student.py即可,不需要额外依赖。

# student.py class Student: school = "示例中学" # 类属性:所有实例共享一份 def __init__(self, name, score): # self 就是被创建出来的那个实例 self.name = name # 实例属性:每个对象各自一份 self.score = score def is_pass(self, threshold=60): # 方法第一个参数永远是 self,调用时不用手动传 return self.score >= threshold s1 = Student("小林", 78) s2 = Student("小周", 52) print(s1.name, s1.is_pass()) # 小林 True print(s2.name, s2.is_pass()) # 小周 False print(s1.school, s2.school) # 示例中学 示例中学 s1.school = "另一所学校" # 注意:这不是修改类属性 print(s1.school, s2.school, Student.school) # 输出:另一所学校 示例中学 示例中学

逻辑说明:__init__不是「构造函数」,真正分配对象的是__new____init__只做初始化。self由解释器自动传入,s1.is_pass()完全等价于Student.is_pass(s1),这就是定义方法时必须留出第一个位置参数的原因。最后三行的输出值得盯一眼:给s1.school赋值,并不会改到Student.school,而是在s1自己的实例字典里新建了一个同名条目,把它「盖住」了。

参数说明:threshold=60是默认参数,调用时可以不传。如果想让及格线全局可配,可以写成类属性pass_line = 60,方法里读self.pass_line;代价是这个规则被绑死在类上,将来做多学校复用时不如显式传参灵活。

提示:self只是约定俗成的名字,写成this也能正常执行,但所有风格检查工具和代码阅读习惯都按self来,别标新立异。

2.2 从函数式写法迁移到对象写法:一张对照表

迁移前,常见写法是「字典 + 一堆函数」:

# 迁移前:函数 + 字典 def add_score(record, value): record["scores"].append(value) def average(record): scores = record["scores"] return sum(scores) / len(scores) if scores else 0.0 stu = {"name": "小林", "scores": []} add_score(stu, 78) add_score(stu, 91) print(round(average(stu), 2)) # 84.5

迁移后,数据和操作数据的行为被收进同一个类:

# 迁移后:类 class ScoreBook: def __init__(self, name): self.name = name self.scores = [] def add(self, value): if isinstance(value, bool) or not isinstance(value, (int, float)): raise TypeError("分数必须是数字") if not 0 <= value <= 100: raise ValueError("分数应在 0 到 100 之间") self.scores.append(value) return self # 返回 self,支持链式调用 def average(self): return sum(self.scores) / len(self.scores) if self.scores else 0.0 book = ScoreBook("小林").add(78).add(91) print(book.name, round(book.average(), 2), book.scores) # 小林 84.5 [78, 91]

两种写法的差别用表格看更直观:

维度纯函数写法面向对象写法
数据存放字典 / 元组分散传递收敛到实例属性
行为归属散落在模块各处类内方法
参数个数每个函数都要重复传self自带上下文
扩展新字段改所有相关函数签名主要改__init__
状态校验靠每个调用方自觉可在入口统一拦截

逻辑说明:校验逻辑从「每个调用点都要写一遍」变成「写一次就拦在门口」。add返回self是常见手法,让多个操作串成一行;但链式调用在出错时堆栈会变长,业务分支复杂时反而降低可读性,不建议到处用。

参数说明:isinstance(value, (int, float))第二个参数传元组,表示「满足其中任一类型即可」。注意boolint的子类,True会被判定为数字,所以前面单独排除了isinstance(value, bool),否则add(True)会悄悄塞进 1 分。

2.3 实例属性、类属性与类型转换的常见组合

先记住三种属性写法各自的归属和影响范围:

写法归属读取顺序修改影响
self.x = 1实例实例字典优先只影响当前对象
Cls.x = 1实例找不到才回退到类影响所有未覆盖的实例
self.__x实例(名字被改写)同上只影响当前对象

查看一个对象身上到底挂了什么,最直接的办法是vars(obj)obj.__dict__,调试时比断点还快。下面这段是从配置字符串构造对象的常见场景,几乎每个做 python教程 练习的人都会踩一次类型转换的坑:

import json class Config: def __init__(self, raw): data = json.loads(raw) if isinstance(raw, str) else raw self.host = str(data.get("host", "127.0.0.1")) self.port = int(data.get("port", 8000)) # "9000" 会被转成 9000 self.debug = self._to_bool(data.get("debug", False)) @staticmethod def _to_bool(value): # bool("False") 的结果是 True,字符串必须单独处理 if isinstance(value, str): return value.strip().lower() in ("1", "true", "yes", "on") return bool(value) def __repr__(self): return f"Config(host={self.host!r}, port={self.port}, debug={self.debug})" print(Config('{"host": "0.0.0.0", "port": "9000", "debug": "False"}')) # Config(host='0.0.0.0', port=9000, debug=False)

逻辑说明:int()能安全处理数字字符串,但遇到"9000.0"会抛ValueError,生产代码里通常要包一层 try 或改用float()再取整。bool()只认空字符串、0None为假,字符串"False"是非空字符串,所以恒为True——这是 python类型转换 里最经典的一处陷阱。

参数说明:_to_bool@staticmethod声明,它不依赖实例状态,也不需要self。反序列化时给每个字段设默认值,比事后 try/except 补字段要省事得多;默认值本身也充当了一份「字段清单」。

3. 封装、继承与多态:三大机制在 Python 里怎么落地

3.1 单下划线与双下划线:Python 的「私有」到底有多私

Python 没有真正的private。单下划线是团队约定「外部别碰」,双下划线会触发名字改写(name mangling),在类体外访问时需要用改写后的名字:

class Wallet: def __init__(self, balance): self._balance = balance # 约定:仅内部使用 self.__pin = "1234" # 被改写为 _Wallet__pin def pay(self, amount): if amount <= 0: raise ValueError("金额必须为正") if amount > self._balance: raise ValueError("余额不足") self._balance -= amount return self._balance w = Wallet(100) print(w.pay(30)) # 70 print(w._balance) # 70,语法上能访问,但违背约定 print(w.__dict__) # {'_balance': 70, '_Wallet__pin': '1234'}

逻辑说明:名字改写的目的是防止子类无意中覆盖父类的内部属性,而不是安全防护。w._Wallet__pin依然能读到,所以真正的敏感信息不要只靠下划线来保护。子类里定义同名的__pin不会被改写冲突,因为改写的名字里带了各自的类名。

参数说明:pay里两个检查顺序会影响报错信息——先判断金额是否为正,再判断余额,能避免pay(-5)报出「余额充足」这种误导性结果。金额如果是浮点数,累减后可能出现99.69999999999999,涉及钱款的场景建议用decimal.Decimal,或者统一按「分」存整数。

3.2 继承与 super():什么时候该用组合代替继承

继承要解决的是「is-a」关系,而组合解决「has-a」。一个储蓄账户是账户,所以继承合理:

class Account: def __init__(self, owner, balance=0): self.owner = owner self.balance = balance def deposit(self, amount): self.balance += amount return self.balance class SavingsAccount(Account): def __init__(self, owner, balance=0, rate=0.03): super().__init__(owner, balance) # 公共字段交给父类初始化 self.rate = rate def add_interest(self): self.balance *= (1 + self.rate) return round(self.balance, 2) s = SavingsAccount("小林", 1000, 0.03) print(s.deposit(500), s.add_interest(), isinstance(s, Account)) # 1500 1545.0 True

逻辑说明:super()不是「调用父类」,而是「沿 MRO(方法解析顺序)找下一个类」。查看顺序用SavingsAccount.__mro__,输出里会依次列出SavingsAccountAccountobject。多继承场景下这个区别很关键:super()会在 MRO 链上继续往后走,而不是跳回「亲爹」。

参数说明:子类__init__里如果忘了调super().__init__(),父类的属性根本不会创建,调用deposit时就会抛AttributeError。反过来,如果一个类只是「用了」另一个类的功能,比如订单需要记录日志,就不要为了复用几行代码去继承 Logger,直接把 logger 作为参数传进来组合更合适。

3.3 多态与鸭子类型:不写 interface 也能统一调用

只要对象有对应方法,就能被同一段代码使用——这就是鸭子类型。三种约束方式对比如下:

方式需要共同基类检查时机典型用途
鸭子类型调用时日志输出、序列化、插件
抽象基类 ABC实例化时框架规定的插件接口
typing.Protocol静态检查器类型提示 + 解耦
from typing import Protocol class Sink(Protocol): def write(self, text: str) -> None: ... class FileSink: def write(self, text): with open("out.txt", "a", encoding="utf-8") as f: f.write(text + "\n") class BufferSink: def __init__(self): self.items = [] def write(self, text): self.items.append(text) def emit(sink: Sink, lines): for line in lines: sink.write(line) buf = BufferSink() emit(buf, ["alpha", "beta"]) print(buf.items) # ['alpha', 'beta']

逻辑说明:Protocol定义了一份「形状」,FileSinkBufferSink都不用显式继承它,静态检查器在传参时按形状匹配即可。运行时不做强制校验,跑错对象要到方法调用那一刻才报AttributeError,所以关键入口处补一句hasattr检查或isinstance判断是常见做法。

参数说明:emitsink参数只依赖write一个方法,这是接口最小化的好处——将来接入网络上报、内存队列、控制台打印,都不用改emit一行代码。用Protocol时记得它不是运行时基类,别拿它去做isinstance判断(除非加@runtime_checkable,但那种判断只检查方法名,不检查签名)。

4. 面向对象编程实例:把图书借阅需求写成可运行的类

4.1 需求拆解:三个类各自负责什么

一份 python面向对象编程实例 想讲清楚,最忌讳把所有逻辑堆进一个大类。按职责拆成三块最稳:

职责关键属性关键方法
Book单本书的库存与借出记录isbntotal_borrowedborrowgive_backoverdue
Reader读者的借阅上限与在借清单namelimitholdingcan_borrow
Library汇总操作入口books(字典)addlendtake_back

拆分的判断标准是:谁的数据谁负责改。让Library.lend去直接改Book._borrowed,短期少写两个方法,长期就会出现两处代码操作同一份状态。

4.2 完整代码:图书借阅最小系统

from datetime import date, timedelta class Book: def __init__(self, isbn, title, total=1): self.isbn = isbn self.title = title self.total = total self._borrowed = {} # {读者名: 到期日} @property def available(self): return self.total - len(self._borrowed) def borrow(self, reader_name, days=14): if self.available <= 0: raise RuntimeError(f"《{self.title}》已全部借出") self._borrowed[reader_name] = date.today() + timedelta(days=days) def give_back(self, reader_name): self._borrowed.pop(reader_name, None) # 不存在也不报错 def overdue(self, today=None): today = today or date.today() return [r for r, due in self._borrowed.items() if due < today] class Reader: def __init__(self, name, limit=3): self.name = name self.limit = limit self.holding = [] def can_borrow(self): return len(self.holding) < self.limit class Library: def __init__(self): self.books = {} # {isbn: Book} def add(self, book): self.books[book.isbn] = book return self def lend(self, isbn, reader, days=14): book = self.books.get(isbn) if book is None: raise KeyError(f"查无此书:{isbn}") if not reader.can_borrow(): raise RuntimeError(f"{reader.name} 已达上限 {reader.limit}") book.borrow(reader.name, days) reader.holding.append(isbn) return self def take_back(self, isbn, reader): self.books[isbn].give_back(reader.name) reader.holding.remove(isbn) return self lib = Library().add(Book("978-7", "Python 入门", total=1)) lin = Reader("小林") lib.lend("978-7", lin) print(lin.holding, lib.books["978-7"].available) # ['978-7'] 0 try: lib.lend("978-7", Reader("小周")) except RuntimeError as e: print("拦截:", e) # 拦截:《Python 入门》已全部借出 lib.take_back("978-7", lin) print(lib.books["978-7"].available) # 1

逻辑说明:@propertyavailable变成只读计算属性,调用时写book.available而不是book.available(),外部无法给它赋值,避免了「库存数」和「借出记录」两个数据源不一致。_borrowed用字典而非列表,是为了在give_back时按读者名直接定位,省去遍历。

参数说明:give_backpop(key, None)而不是pop(key),还书操作因此具备幂等性,重复归还不会抛异常。lend里用books.get(isbn)加显式判断,比self.books[isbn]直接取更友好,能抛出自定义的「查无此书」而不是一个光秃秃的KeyErrordays=14Book.borrowLibrary.lend两处都出现,实际项目里应只保留一处,把它提到配置里。

4.3 运行验证与常见报错对照

跑之前先确认解释器版本,python --version输出 3.8 以上都行(Protocol需要 3.8+)。常见报错和对应原因整理如下:

报错常见原因排查动作
AttributeError: 'Book' object has no attribute '_borrowed'子类或改写版本忘了调super().__init__()检查__init__首行
TypeError: 'int' object is not callable把 property 当方法调了,又或用了同名内置名去掉括号,或给变量改名
KeyError: '978-7'借书前没先add,或 isbn 拼写不一致打印lib.books.keys()
ValueError: too many values to unpackfor里解包字典只写了键名改成.items()或单变量接收

验证时建议把每一步的中间状态打出来,比如借出前后各打一次available,归还后再打一次。比起一次性运行完整流程再看结果,逐步打印更容易定位是状态没变,还是变了但读错了属性。

5. 进阶与排错:slots、类变量陷阱与调试手法

当类的字段固定且实例数量很大时,给类加__slots__可以省掉每个实例的__dict__,内存占用明显下降,属性访问也略快:

class Point: __slots__ = ("x", "y") # 只允许这两个属性 def __init__(self, x, y): self.x = x self.y = y p = Point(1, 2) p.z = 3 # AttributeError: 'Point' object has no attribute 'z'

代价是失去了动态加属性的能力,也和多重继承、__dict__反射、functools.cached_property有冲突。数据量大且结构固定的场景(比如解析百万行记录)值得用,普通业务类加上只会给自己添麻烦。

__slots__更常见也更隐蔽的是可变类变量陷阱:

class Cart: items = [] # 错误示范:所有实例共享同一个列表 def add(self, name): self.items.append(name) a, b = Cart(), Cart() a.add("apple") print(b.items) # ['apple'] —— b 也被污染了

正确的写法是在__init__里初始化:

class Cart: def __init__(self): self.items = [] # 每个实例一份

规则很简单:类属性只放不可变常量或共享的配置,可变容器一律放进__init__。用dataclasses时也有同样的问题,可变默认值必须写成field(default_factory=list),不能直接写= []

调试阶段值得顺手记几个手段:vars(obj)看实例字典、type(obj).__mro__看继承链、dir(obj)列出全部可访问名字、obj.__class__.__name__在日志里打类型名。要查某个属性到底来自实例还是类,直接"x" in vars(obj)判断即可,比反复读代码快得多。

如果模块里要同时处理多个相互独立的对象集合,且每个对象的初始化都涉及阻塞 I/O,考虑用concurrent.futuresmultiprocessing把实例构造分发出去;这时要注意对象不一定可跨进程序列化,带锁、文件句柄、数据库连接的实例传不过去,通常的做法是在子进程里各自重建对象,只传构造参数。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 15:47:30

风电场并网潮流计算:节点类型、RX模型与MATLAB实现

简介&#xff1a;该资源是一份用牛拉法&#xff08;Newton-Raphson法&#xff09;实现含风电场电力系统潮流计算的程序文档&#xff0c;面向电力系统方向的研究人员、工程师及电气类课程学习者&#xff0c;用于处理风电并网后输出随机、不确定条件下的电网电压与功率分布分析。…

作者头像 李华
网站建设 2026/9/18 15:46:16

微信聊天记录导出指南:WeChatMsg 如何把对话变成可搜索的本地文档

微信聊天记录导出指南&#xff1a;WeChatMsg 如何把对话变成可搜索的本地文档 【免费下载链接】WeChatMsg 提取微信聊天记录&#xff0c;将其导出成HTML、Word、CSV文档永久保存&#xff0c;对聊天记录进行分析生成年度聊天报告 项目地址: https://gitcode.com/GitHub_Trendi…

作者头像 李华
网站建设 2026/9/18 15:44:47

Storybook 侧边栏 Single-Story Hoisting 单 Story 提升机制解析与跨框架写法

Storybook 侧边栏 Single-Story Hoisting 单 Story 提升机制解析与跨框架写法 Storybook 的侧边栏默认按 title 的 / 分段生成「分组 → 组件 → Story」三层结构。但当某个组件只包含一个与组件同名的 Story 时&#xff0c;Storybook 会把这个 Story 自动"提升"&am…

作者头像 李华
网站建设 2026/9/18 15:44:04

通达信指数强度副图指标源码详解与参数调优

简介&#xff1a;通达信用户常有对比个股与大盘强弱的需求&#xff0c;这份《指数强度副图指标》教程文档正对应此类场景。内容从源码拆解入手&#xff0c;逐一说明 Z、X、T1&#xff5e;T3 等变量含义&#xff0c;讲解如何基于 N 日高低点区间计算个股相对强度&#xff0c;并绘…

作者头像 李华