类(二):继承、特殊方法与模块
Classes II: Inheritance, Special Methods and Modules
继续扩展Rover探险项目,通过继承和重写复用角色行为,让自定义对象支持Python操作,再用真实文件理解导入、入口与运行环境。
本讲结束后你应当能
- 跟踪子类初始化、super与方法调用中的实际接收者
- 区分继承、重写、组合及兼容的行为接口
- 实现表示、加法、相等和容器协议并解释副作用
- 用真实多文件工程区分模块对象、导入绑定和入口
- 检查解释器与模块来源,区分进程和磁盘缓存
本页目录
从L9继续:扩展同一个探险队
上一讲的Rover能保存坐标和金币、移动、付款和拾取物品。现在增加侦察与运输角色:保留已有操作,添加角色信息,并在同一循环中显示不同角色的状态。随后让位置支持加法、背包支持常用容器操作,再将项目拆成可复用模块。
下载包中的 01_roles/、02_protocols/、03_expedition/ 对应三个阶段。进入相应目录,运行 python main.py。示例使用Python3.13和标准库,无需安装图形库;HTML演示用JavaScript显示对应的角色状态。课堂最小例子只保留当时需要的字段,完整项目保留坐标、金币与背包。
I. 扩展角色:继承与方法选择
从重复实现到基类与子类
继承让子类通过基类关系查找和复用属性与方法。语法是 class ScoutRover(Rover):。先不增加新实现,观察子类怎样使用已有操作:
class Rover:
def __init__(self, x, y):
self.x, self.y = x, y
def move(self, dx, dy):
self.x += dx
self.y += dy
class ScoutRover(Rover):
pass
s = ScoutRover(3, 4)
s.move(1, 0)
print(s.x, s.y)
print(isinstance(s, Rover))
4 4
True
s是ScoutRover实例。子类没有定义初始化或move,查找得到Rover中的函数;执行这些函数时,self仍指向s。初始化中的赋值建立s自己的字段,move更新同一实例。isinstance(s, Rover)接受子类实例;type(s) is Rover检查精确类型,会得到False。
方法定义的复用与实例数据的存储应分别理解。继承关系不会把父实例复制到子实例内部。这里讨论普通Python方法;描述符与自定义属性访问还有更完整的查找规则。
重写初始化需要协调已有状态
子类定义自己的 __init__ 后,不会自动再执行基类初始化。这个候选只建立scan_range,读取坐标时报错:
class Rover:
def __init__(self, x, y):
self.x = x
self.y = y
class ScoutRover(Rover):
def __init__(self, x, y, scan_range):
self.scan_range = scan_range
s = ScoutRover(3, 4, 2)
print(s.x, s.y, s.scan_range)
AttributeError: 'ScoutRover' object has no attribute 'x'
调用 super().__init__(x, y) 复用已有初始化,再增加子类字段:
class Rover:
def __init__(self, x, y):
self.x = x
self.y = y
class ScoutRover(Rover):
def __init__(self, x, y, scan_range):
super().__init__(x, y)
self.scan_range = scan_range
s = ScoutRover(3, 4, 2)
print(s.x, s.y, s.scan_range)
3 4 2
跟踪执行时,始终只有一个ScoutRover实例:
- ScoutRover初始化接收新实例和三个参数,尚未建立字段。
- super调用进入Rover初始化,两帧中的self指向同一个实例。
- 基类依次写入x、y,然后返回None。
- 子类继续写入scan_range,再返回None。
super()沿实际类型的方法查找顺序(MRO),从当前类之后继续查找。本例的顺序是ScoutRover、Rover、object,因此找到Rover初始化;合作式多继承中可能先经过其他类。调用时机应满足字段依赖。实例属性也能在其他方法里建立,初始化负责给新实例准备初始状态。
基类方法中的self仍是实际接收者
重写是子类提供同名方法的另一份实现。report从Rover复用,symbol由ScoutRover重写:
class Rover:
def symbol(self):
return "R"
def report(self):
return self.symbol()
class ScoutRover(Rover):
def symbol(self):
return "S"
s = ScoutRover()
print(s.report())
S
先找到Rover.report,再以s为self执行。遇到 self.symbol() 时,从实际ScoutRover实例查找,调用ScoutRover.symbol并返回S。进入基类函数没有创建新的Rover接收者。显式写 Rover.report(s) 也把s传给该函数,因此结果仍为S。
子类也可以用super扩展已有方法的结果:
class Rover:
def describe(self):
return "at (3, 4)"
class ScoutRover(Rover):
def describe(self):
base = super().describe()
return base + ", scan range 2"
s = ScoutRover()
print(s.describe())
at (3, 4), scan range 2
多态、鸭子类型与组合
同一接口调用可以使用不同类型提供的实现:
class Rover:
def symbol(self):
return "R"
def report(self):
return self.symbol()
class ScoutRover(Rover):
def symbol(self):
return "S"
class CargoRover(Rover):
def symbol(self):
return "C"
team = [Rover(), ScoutRover(), CargoRover()]
for rover in team:
print(rover.report())
R
S
C
循环通过report得到R、S、C。公共流程不需要为新增角色复制一份。继承是否合适,应检查子类能否满足调用者依赖的行为;现实概念之间有分类关系,并不自动保证软件接口兼容。
兼容的调用也可以出现在没有共同自定义基类的对象之间:
class ScoutRover:
def report(self):
return "S: ready"
class Beacon:
def report(self):
return "B: online"
for device in [ScoutRover(), Beacon()]:
print(device.report())
print(issubclass(Beacon, ScoutRover))
S: ready
B: online
False
这里两个对象都能无参数调用report并返回描述字符串。鸭子类型依赖实际支持的操作,方法名相同仍需核对参数、结果和副作用。
组合是把另一个对象保存为成员并使用它。完整项目的Rover在初始化时创建 self.bag = Backpack();侦察角色是一种Rover,背包则是Rover拥有的组件。使用一个组件的功能,不必让持有者继承该组件的完整接口。
II. 让对象支持常用Python操作
先设计显示的含义
__str__和__repr__都必须返回字符串。前者提供易读显示,后者提供便于检查的对象表示。列表展示元素时使用repr;!r表示对插入的值取repr。
class Rover:
def __init__(self, name):
self.name = name
def __str__(self):
return "Rover " + self.name
def __repr__(self):
return f"Rover({self.name!r})"
s = Rover("Ada")
print(s)
print(repr(s))
print([s])
Rover Ada
Rover('Ada')
[Rover('Ada')]
未定义str时可以回退repr;相反方向没有对应回退保证。repr可以争取无歧义或可重建,但不能据此把未知字符串交给eval。完整项目的str调用describe,所以不同角色仍选择各自的描述实现。
位置加法:支持什么,修改什么?
定义x、y字段还没有规定加法。我们约定两个Position坐标逐项相加,返回新Position,输入保持原样:
class Position:
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other):
if not isinstance(other, Position):
return NotImplemented
x = self.x + other.x
y = self.y + other.y
return Position(x, y)
def __repr__(self):
return f"Position({self.x}, {self.y})"
p = Position(3, 4)
q = p + Position(1, 0)
print(p, q, p is q)
Position(3, 4) Position(4, 4) False
遇到不支持的操作数,方法返回NotImplemented哨兵,让运算分派继续处理;所有适用途径仍不支持时,表达式通常抛TypeError。NotImplemented和异常NotImplementedError作用不同。
class Position:
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other):
if not isinstance(other, Position):
return NotImplemented
x = self.x + other.x
y = self.y + other.y
return Position(x, y)
def __repr__(self):
return f"Position({self.x}, {self.y})"
p = Position(3, 4)
print(p.__add__("east") is NotImplemented)
print(p + "east")
True
TypeError: unsupported operand type(s) for +: 'Position' and 'str'
隐式特殊操作通常通过类型及其继承关系查找,不是任意给实例添加同名字段就会支持该操作。反射方法、右操作数子类型优先和增强赋值回退见后面的选读。这里只为本例规定新对象语义,不概括所有加法都复制。
坐标相等与对象身份
class Position:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
if not isinstance(other, Position):
return NotImplemented
return (self.x == other.x
and self.y == other.y)
a = Position(3, 4)
b = Position(3, 4)
c = a
print(a == b, a is b, a is c)
True False True
eq比较坐标,is检查是否同一对象。修改b的坐标可能改变eq的结果;c继续绑定a,身份关系保持。定义eq的普通可变类通常变为不可哈希;若另行定义hash,必须与相等规则一致。主线中的Position没有承诺可作字典键。
背包的长度、下标和成员关系
把已有列表保存到Backpack内部,再明确支持哪些操作:
class Backpack:
def __init__(self, items):
self._items = list(items)
def __len__(self):
return len(self._items)
def __getitem__(self, index):
return self._items[index]
def __contains__(self, item):
return item in self._items
bag = Backpack(["map", "apple"])
print(len(bag), bag[0])
print("apple" in bag)
2 map
True
len使用 __len__,长度应是非负整数;bag[i]使用 __getitem__;in使用本例的 __contains__。这里委托内部列表,支持负下标,越界抛IndexError。完整项目另有add方法供Rover.pick调用。
本讲暂不展开迭代器。没有 __iter__ 时可存在从零开始调用getitem、遇IndexError结束的序列回退,完整规则和切片等扩展按类的接口约定,在选读中查阅。
III. 把当前项目整理成模块
先拆两个真实文件
在同一目录保存以下文件,运行main.py。模块是具有命名空间的对象;本讲自写模块是py文件,Python也支持内置、扩展等模块。
rover.py
class Rover:
def symbol(self):
return "R"
def report(self):
return self.symbol()
class ScoutRover(Rover):
def symbol(self):
return "S"
main.py
from rover import ScoutRover
s = ScoutRover()
print(s.report())
python main.py
S
import rover在导入者绑定模块对象,通过 rover.ScoutRover 访问类;from rover import ScoutRover在导入者绑定选定类对象的引用。as改变本地访问名。普通课程代码使用显式导入,避免名称来源不清。星号导入有合法语法和__all__等规则,不等于无条件取得全部名称。
为什么导入类会启动演示?
模块初始化时运行顶层语句,顶层的演示调用也会运行:
rover.py
print("mission demo")
class Rover:
pass
main.py
import rover
print("main")
python main.py
mission demo
main
用 if __name__ == "__main__": 保护演示入口:
rover.py
print("loading rover")
class Rover:
pass
if __name__ == "__main__":
print("mission demo")
main.py
import rover
import rover
print("main")
python main.py
loading rover
main
直接运行rover.py时输出loading rover和mission demo;被main导入时,rover的name为rover,demo不执行。块外的loading仍执行,guard只控制该条件块。class、def在执行时建立类与函数,函数体要到调用时才执行。
跟踪一次导入和重复导入
动画来自真实文件在Python3.13中的行事件:main遇到import并等待;rover执行顶层、建立Rover类、跳过入口块;回到main完成绑定。第二次相同进程、同名普通导入使用sys.modules中的模块对象,不重复打印顶层消息。
模块初始化期间对象可以已经进入sys.modules,但其名称尚未全部建立。不要把缓存中存在模块理解为所有初始化工作已经完成。新进程、显式reload或缓存修改需要另外分析。
from导入保留对象引用
这是L9“重新绑定与修改对象”的同一套机制:
settings.py
count = 1
items = []
main.py
import settings
from settings import count, items
settings.count = 2
settings.items.append("apple")
print(count, settings.count, items)
python main.py
1 2 ['apple']
main的count保留对一的引用,settings.count改绑到二;两个items仍到同一列表,所以apple可见。from不复制对象,也不让名字变成模块字段的实时别名。
完整平铺项目与包版本
第三阶段目录如下。这里只改变源码组织,角色、坐标和背包的行为继续由相同实现规定。
03_expedition/
main.py
rover.py
geometry.py
backpack.py
展开完整四文件项目与实际输出
geometry.py
class Position:
def __init__(self, x=0, y=0):
self.x, self.y = x, y
def __add__(self, other):
if not isinstance(other, Position):
return NotImplemented
x = self.x + other.x
y = self.y + other.y
return Position(x, y)
def __eq__(self, other):
if not isinstance(other, Position):
return NotImplemented
return (self.x == other.x
and self.y == other.y)
def __repr__(self):
return f"Position({self.x}, {self.y})"
backpack.py
class Backpack:
def __init__(self, items=()):
self._items = list(items)
def add(self, item):
self._items.append(item)
def __len__(self):
return len(self._items)
def __getitem__(self, index):
return self._items[index]
def __contains__(self, item):
return item in self._items
def __repr__(self):
return f"Backpack({self._items!r})"
rover.py
from geometry import Position
from backpack import Backpack
class Rover:
def __init__(self, name, x=0, y=0):
self.name = name
self.x, self.y = x, y
self._coins = 100
self.bag = Backpack()
def move(self, dx, dy):
self.x += dx
self.y += dy
def pick(self, item):
self.bag.add(item)
def spend(self, amount):
if type(amount) is not int or amount <= 0 or amount > self._coins:
return False
self._coins -= amount
return True
def coins_left(self):
return self._coins
def position(self):
return Position(self.x, self.y)
def symbol(self):
return "R"
def describe(self):
return f"{self.symbol()} {self.name} at ({self.x}, {self.y})"
def __str__(self):
return self.describe()
def __repr__(self):
return f"{type(self).__name__}({self.name!r}, {self.x}, {self.y})"
class ScoutRover(Rover):
def __init__(self, name, x=0, y=0, scan_range=2):
super().__init__(name, x, y)
self.scan_range = scan_range
def symbol(self):
return "S"
def describe(self):
return super().describe() + f", scan range {self.scan_range}"
class CargoRover(Rover):
def symbol(self):
return "C"
def describe(self):
return super().describe() + f", items {len(self.bag)}"
main.py
from rover import Rover, ScoutRover, CargoRover
from geometry import Position
def main():
team = [Rover("Ada"), ScoutRover("Lin", 3, 4), CargoRover("Sam", 5, 1)]
team[1].move(1, 0)
team[2].pick("apple")
for rover in team:
print(rover)
print(team[1].position() + Position(0, 1))
print(len(team[2].bag), "apple" in team[2].bag)
if __name__ == "__main__":
main()
python main.py
R Ada at (0, 0)
S Lin at (4, 4), scan range 2
C Sam at (5, 1), items 1
Position(4, 5)
1 True
包版本位于04_package/expedition,包含__init__.py、main.py和三个模块。包内写 from .rover import ScoutRover。进入04_package运行 python -m expedition,由包上下文支持相对导入,输出与平铺版本一致。普通包和namespace package是不同机制。直接按路径执行包内文件可能缺少相对导入上下文。
IV. 检查运行环境
实际导入的模块来自哪里?
以下独立例子用同目录的random.py展示来源问题:
random.py
label = "local file"
main.py
import random
from pathlib import Path
print(Path(random.__file__).name)
print(random.randint(1, 3))
python main.py
random.py
AttributeError: module 'random' has no attribute 'randint' (consider renaming '<project>/random.py' since it has the same name as the standard library module named 'random' and prevents importing that standard library module)
先查看 sys.executable,再查看模块有 __file__ 时的来源;终端用 python -m pip --version 确认安装工具与解释器对应。VSCode的Python解释器选择也应核对。启动方式、缓存和查找器会影响来源,当前工作目录永远第一并不是通用规则。
创建项目依赖环境
python -m venv .venv
# macOS / Linux:
source .venv/bin/activate
# Windows PowerShell:
# .venv\Scripts\Activate.ps1
python -m pip --version
选择环境的解释器后,用python -m pip安装实际需要的库。本项目不需要额外依赖。PACKAGE或wheel文件名的占位命令需要替换为真实依赖,并核对Python和平台兼容性。venv隔离依赖,不是安全沙箱;PowerShell若阻止激活,可直接选择或运行环境内的python,不必为本课修改全局执行策略。
进程模块缓存与磁盘编译缓存
sys.modules保存当前进程的模块对象。CPython还可以将编译代码缓存到__pycache__中的pyc文件,减少有效缓存条件下的重新编译。新进程仍需要执行模块初始化;使用pyc不等于跳过顶层代码。
CPython把源码编译为代码对象/字节码,再执行。具体指令、版本标签和缓存有效性属于实现细节。权限、启动参数和源码变化会影响缓存。字节码不是源码保密方式,exec不能把裸pyc文件字节当源代码直接执行。
回顾与实验迁移
继承调用先找实际接收者,再找实现;初始化通过实际执行的赋值建立状态。对象协议要说明结果、支持的输入和副作用。模块导入要区分顶层运行、函数调用、对象引用与程序入口。
课堂三题分别检查显式基类函数调用、相等与身份、普通导入与入口保护。第二题修改b.x后输出False、False、True;第三题在全新进程中输出load、main。各题完整源码可在课件运行或从下载包复现。
Lab10通过Shape、Rover子类、Money和Deck练习继承与特殊方法,再复用图形代码完成三文件报告工程。实验骨架提供尺寸检查、金额转换和牌组初始化;模块题提供自动打包脚本。两个文字卡记录关键机制和实际结果,AI使用说明在HW4末尾统一填写。个人作业2仍为素数计数五法,截止以课程日历和提交页为准。L11继续算法工作量、异常与测试。
进一步阅读
下面保留较完整机制和其他任务的例子。按需要选择,不要求课堂逐项讲完。
展开:多继承、协议边界、其他类例子与导入扩展
迁移到Shape接口
用同一套继承与多态机制,可以组织不同图形的area。示例约定尺寸有效,只演示方法组织;实验的输入校验仍按题面完成。普通Shape可以创建,调用其area时才抛NotImplementedError。
import math
class Shape:
def area(self) -> float:
raise NotImplementedError("area")
class Circle(Shape):
def __init__(self, r: float) -> None:
self.r = r
def area(self) -> float:
return math.pi * self.r ** 2
class Square(Shape):
def __init__(self, side: float) -> None:
self.side = side
def area(self) -> float:
return self.side ** 2
class Rectangle(Shape):
def __init__(self, w: float, h: float) -> None:
self.w, self.h = w, h
def area(self) -> float:
return self.w * self.h
shapes = [Circle(1), Square(2), Rectangle(2, 3)]
total = 0.0
for shape in shapes:
total += shape.area()
print(f"{total:.4f}")
13.1416
增强赋值与别名
本例的__iadd__修改self并返回self,普通加法产生新对象。其他实现可以选择新结果;缺失或返回NotImplemented时,增强赋值还可能回退到普通加法。
class Value:
def __init__(self, n: int) -> None:
self.n = n
def __add__(self, other: "Value") -> "Value":
return Value(self.n + other.n)
def __iadd__(self, other: "Value") -> "Value":
self.n += other.n
return self
a = Value(1)
b = a
a += Value(2)
print(a.n, b.n, a is b)
a = a + Value(3)
print(a.n, b.n, a is b)
3 3 True
6 3 False
Person与Manager
class Person:
def __init__(self, name: str, job: str, pay: float) -> None:
self.name, self.job, self.pay = name, job, pay
def get_last_name(self) -> str:
return self.name.split()[-1]
def give_raise(self, fraction: float) -> None:
self.pay *= 1 + fraction
def __repr__(self) -> str:
return f"Person({self.name!r}, {self.job!r}, {self.pay:.2f})"
class Manager(Person):
def __init__(self, name: str, pay: float) -> None:
super().__init__(name, "manager", pay)
def give_raise(self, fraction: float, bonus: float = 0.1) -> None:
super().give_raise(fraction + bonus)
m = Manager("Alex Chen", 100)
m.give_raise(0.05)
print(m.get_last_name(), f"{m.pay:.2f}")
Person保存name、job、pay,提供姓氏查询和薪资更新;Manager重写薪资更新方法。这里的fraction和bonus约定用于教学,业务有效域应另外定义。同一命名空间第二个同名def会重新绑定名称,不是按参数重载。
合作式多继承与C3
class Root:
def visit(self):
return ["Root"]
class Left(Root):
def visit(self):
return ["Left"] + super().visit()
class Right(Root):
def visit(self):
return ["Right"] + super().visit()
class Combined(Left, Right):
def visit(self):
return ["Combined"] + super().visit()
print([c.__name__ for c in Combined.__mro__])
print(Combined().visit())
Combined/Left/Right/Root形成线性化顺序,super沿它继续,因此Left后面可以是Right而非直接父Root。只安排查找顺序不会自动调用所有方法,合作式实现必须协调super。
class A: pass
class B(A): pass
class C: pass
class D(B, C): pass
print([c.__name__ for c in D.__mro__])
结果D/B/A/C/object表明不是BFS。下面相互冲突的基类先后约束使类创建失败:
class X: pass
class Y: pass
class A(X, Y): pass
class B(Y, X): pass
class C(A, B): pass
继承选择还涉及行为替换:一个可独立更新宽高的Rectangle接口,未必适合Square子类保持相等边长。组合与委托也是有效方式。
名称改写与子类签名
class Mapping:
def __init__(self, values):
self.items = []
self.__update(values)
def update(self, values):
self.items.extend(values)
__update = update
class Child(Mapping):
def update(self, keys, values):
self.items.extend(zip(keys, values))
m = Child([1, 2])
m.update(["a"], [3])
print(m.items)
基类__update实际为_Mapping__update,保留基类函数,子类不同签名的update不破坏初始化。名称改写仍可显式外部访问,不是权限控制。
内省及其适用范围
class Animal: pass
class Dog(Animal): pass
d = Dog()
d.name = "Rex"
print(isinstance(d, Animal), type(d) is Animal)
print(issubclass(Dog, Animal))
print(d.__dict__)
print([c.__name__ for c in Dog.__mro__])
print([n for n in dir(d) if not n.startswith("_")])
还可查看实例__class__、类__name__、__bases__与__mro__。不是全部对象都有__dict__,dir也不保证完整支持接口。L09普通字段的实例优先模型有数据描述符等例外;本讲只说明边界,不展开描述符实现。
def add(a, b):
return a + b
print(add.__name__)
print(add.__code__.co_varnames)
print(add.__code__.co_names)
print(add.__code__.co_consts)
函数是对象,可绑定名字或作为参数。co_varnames为局部名,co_names为非局部引用名,co_consts为常量;它们是code object属性,不代表整个运行状态。
比较与特殊方法分派
| 操作 | 方法 |
|---|---|
| <、<=、==、!=、>、>= | __lt__、__le__、__eq__、__ne__、__gt__、__ge__ |
| +、-、*、abs | __add__、__sub__、__mul__、__abs__及适用反射方法 |
| += | __iadd__及普通加法回退 |
| len、[]、[]=、in | __len__、__getitem__、__setitem__、__contains__ |
| str、repr | __str__、__repr__ |
右操作数是子类型且提供不同反射实现时可能优先;同类型不保证再次尝试反射。eq没有__req__形式,通常可尝试对方eq,两方哨兵后回退身份。定义eq的可变类通常变不可哈希,若设计hash要与相等规则一致。
隐式特殊操作通常通过类型及继承链查找,而不是普通实例字段:
class Box: pass
def length():
return 3
b = Box()
b.__len__ = length
print(b.__len__())
print(len(b))
直接调用b.__len__打印3,但len(b)预期TypeError。这与普通属性调用有区别。
几何、复数与同构字符串
class Triangle:
def __init__(self, a: float, b: float, c: float) -> None:
if min(a, b, c) <= 0 or not (a+b>c and a+c>b and b+c>a):
raise ValueError("invalid edges")
self.edges = [a, b, c]
def area(self) -> float:
a, b, c = self.edges
s = (a+b+c)/2
return (s*(s-a)*(s-b)*(s-c))**0.5
def __lt__(self, other: "Triangle") -> bool:
if not isinstance(other, Triangle):
return NotImplemented
return self.area() < other.area()
def __getitem__(self, index: int) -> float:
return self.edges[index]
def __setitem__(self, index: int, value: float) -> None:
candidate = self.edges.copy()
candidate[index] = value
Triangle(*candidate)
self.edges = candidate
a, b = Triangle(3, 4, 5), Triangle(3, 3, 3)
print(b < a, a[0])
a[0] = 4
print(a[0])
本例明确以面积定义Triangle的<,下标选三边,非法边赋值保留旧边。不能将任意Triangle相加或大小关系称为普遍几何定义。
class ComplexNumber:
def __init__(self, real: float, imag: float) -> None:
self.real, self.imag = real, imag
def __add__(self, other: "ComplexNumber") -> "ComplexNumber":
if not isinstance(other, ComplexNumber):
return NotImplemented
return ComplexNumber(self.real + other.real, self.imag + other.imag)
def __repr__(self) -> str:
return f"ComplexNumber({self.real}, {self.imag})"
print(ComplexNumber(1, 2) + ComplexNumber(3, -1))
Point2D记录两个坐标,nDPoint可存一份标量坐标列表,维度需要明确;L09的Vector、方向角及HappyNumber保留完整定义。本讲二维Vector运算协议与上一讲n维向量接口分开命名。
def isomorphic(s: str, t: str) -> bool:
if len(s) != len(t):
return False
forward, reverse = {}, {}
for a, b in zip(s, t):
if a in forward and forward[a] != b:
return False
if b in reverse and reverse[b] != a:
return False
forward[a], reverse[b] = b, a
return True
print(isomorphic("egg", "add"))
print(isomorphic("foo", "bar"))
正反两张表共同保证一一对应:既不能让同一字符映到不同字符,也不能让不同字符映到同一字符。
重载、循环导入与future
settings.py
print("load")
value = 1
main.py
import importlib
import settings
from settings import value
settings.value = 9
print(value, settings.value)
importlib.reload(settings)
print(value, settings.value)
实际 Python 3.13 输出:
load
1 9
load
1 1
使用importlib.reload,不使用已过时的imp。重载重新执行模块代码,先前from的引用不自动改绑;它不是生产并发热更新保证。
a.py
import b
value = 1
b.py
from a import value
main.py
import a
实际 Python 3.13 输出:
ImportError: cannot import name 'value' from 'a' (consider renaming '/privatea.py' if it has the same name as a library you intended to import)
两个模块循环访问尚未建立的字段可以报错。调整职责或延后访问比“换IDE”更有意义,不能把Notebook缓存行为当语义标准。
from __future__ import annotations
class Node:
def copy(self) -> Node:
return Node()
print(Node().copy().__class__.__name__)
future是编译指令,不是任意普通导入;允许合法的文档/注释等前置,不要求物理第一行。annotations从3.7可选,在3.13仍须显式启用,不提供类型校验。
金额转换的标准库规则
Lab10 的 Money 骨架已提供 to_cents(),本段供希望了解辅助函数的同学选读。金额采用明确十进制舍入:Decimal 从字符串建立十进制值,quantize(Decimal(“1”)) 量化到整数单位;ROUND_HALF_UP 在恰好半个单位时向远离零的方向舍入。这里单位已经乘成分,不把浮点乘法再截断。
from decimal import Decimal, ROUND_HALF_UP
for yuan in [1.005, -1.005]:
value = Decimal(str(yuan)) * 100
cents = value.quantize(Decimal("1"), rounding=ROUND_HALF_UP)
print(int(cents))
实际101、−101分。规则针对传入值的str,不恢复此前浮点运算丢失的信息。实验通过已提供的 to_cents() 完成转换,学生重点实现 Money 的表示、运算和比较。
包、路径和发行工具
路径查找可能涉及PYTHONPATH、site/.pth、site-packages及zip路径;sys.path项不全都是普通目录。__all__影响星号导出,普通包、namespace包、相对导入和-m是不同机制。
NumPy提供数组、广播及数值操作;Matplotlib绘图;SciPy提供科学算法;pandas处理表格;pytest用于测试。Scrapy、Beautiful Soup用于网页处理,Pygame用于游戏程序。官网用途不要求本讲安装这些依赖。
CPython、PyPy、Jython为不同实现;Cython和Nuitka等编译工具不全部归为解释器。CPython源码阅读使用当前仓库,不照抄旧版本文件行号。
显式编译与可信执行
source = "answer = 2 + 3"
code = compile(source, "trusted.py", "exec")
namespace = {}
exec(code, namespace)
print(namespace["answer"])
compile得到code object,再在指定命名空间exec,仅执行课程自己写的可信源码。
python -m py_compile module_a.py module_b.py
python -m compileall selected_project_directory
只选择自己的教学目录,不扫描整个机器;编译不保证缓存可写或跨版本适用。dis/缓存/源码阅读是观察工具,不是新的安全边界。
大整数:第12周的主题预告
本周按统一日历启动素数计数五法,大整数将在第12周启动。大整数可以采用如下表示与接口:从十进制字符串构造BigInt,单独符号与分组数字,str/repr、eq/lt、add/sub/mul;不能把整个输入转int来代替实现,但可以用int作为测试参考。
须考虑零、前导零、负数、不等长、负差、进位借位和竖式乘法。报告不超过一页,说明表示选择、符号处理及具体边界/随机对拍证据。后续任务的具体接口和评分以届时题面为准。