CS1602计算导论
第 10 讲Part 4 抽象与组织AI Level 1

类(二):继承、特殊方法与模块

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实例:

  1. ScoutRover初始化接收新实例和三个参数,尚未建立字段。
  2. super调用进入Rover初始化,两帧中的self指向同一个实例。
  3. 基类依次写入x、y,然后返回None。
  4. 子类继续写入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作为测试参考。

须考虑零、前导零、负数、不等长、负差、进位借位和竖式乘法。报告不超过一页,说明表示选择、符号处理及具体边界/随机对拍证据。后续任务的具体接口和评分以届时题面为准。