类(二)与模块
Class II and Modules
复用与重写类的行为,理解 Python 的运算和容器协议。用实际多文件例子分析导入、名字绑定和入口,再区分运行环境、模块缓存与编译缓存。
本讲结束后你应当能
- 分析继承方法中的接收者、重写与必需状态
- 解释 super 沿方法查找顺序继续查找
- 为表示、算术、比较和下标规定明确协议
- 区分模块对象、导入名字与共享引用
- 用真实文件核对导入副作用及 main 入口
- 区分解释器环境、进程模块缓存与磁盘字节码缓存
本页目录
I. 继承与行为接口
基类与子类
L09 已经说明对象状态、方法接收者和公开接口。现在几个类可以复用共同实现,并定义不同的行为。
继承建立基类与子类的关系。程序是否适合采用继承,还要看子类能否满足调用者依赖的接口,不由现实概念的分类自动决定。
class Animal:
def __init__(self, name: str) -> None:
self.name = name
def speak(self) -> str:
return "..."
def introduce(self) -> str:
return self.name + ": " + self.speak()
class Dog(Animal):
def speak(self) -> str:
return "woof"
class Cat(Animal):
def speak(self) -> str:
return "meow"
for animal in [Animal("A"), Dog("D"), Cat("C")]:
print(animal.introduce())
Dog 没有新定义初始化,使用继承的初始化建立自己的 name。introduce 的函数来自 Animal,但执行时 self 仍为实际实例;self.speak 可以选择重写的方法。普通 Python 类的继承链最终到 object,这不表示任何对象都支持 len 等操作。
重写与初始化
重写是子类提供继承方法的另一实现。若重写 init,就不会因此自动执行基类初始化。以下例子故意展示缺字段:
class Base:
def __init__(self, value):
self.value = value
class Child(Base):
def __init__(self, value):
self.extra = value
print(Child(3).value)
继承查找能找到方法,不会复制父实例的数据。需要基类字段时,应协调初始化:
class Person:
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
def describe(self) -> str:
return f"{self.name}, {self.age}"
class Student(Person):
def __init__(self, name: str, age: int, school: str) -> None:
super().__init__(name, age)
self.school = school
def describe(self) -> str:
return super().describe() + ", " + self.school
s = Student("Alex", 18, "SJTU")
print(s.describe())
这里是单继承,super 找到 Person 的方法。一般规则是:在实际类型的方法查找顺序 MRO中,从当前类之后继续查找。它不是永远等同直接父类,也不自动执行所有父类方法。
调用时机取决于状态依赖,不是每个子类都机械地第一行调用 super。重写签名、返回、异常或状态规则后,仍需回归检查。
父方法内的 self 调用
class Base:
def word(self):
return "base"
def report(self):
return self.word()
class Child(Base):
def word(self):
return "child"
print(Child().report())
输出 child。Base.report 的函数代码仍然通过 Child 实例调用 word。执行父方法不会将 self 换成一个新 Base 对象。
多态与鸭子类型
同一操作可以由不同类型提供实现;调用方依赖的是行为接口。先保留有共同基类的完整版本,此例约定传入合法尺寸,不提供实验所需的全部验证:
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。共同基类记录接口,但下面相同调用也可不要求共同基类:
class Circle:
def area(self) -> float:
return 3.141592653589793
class Square:
def area(self) -> float:
return 4.0
total = 0.0
for shape in [Circle(), Square()]:
total += shape.area()
print(f"{total:.4f}")
这里两个类都提供返回面积的无参数方法。名字相同还不足够,参数、结果和副作用也必须符合约定。没有合适的 area 实现时,普通基类方法可抛 NotImplementedError;这种普通类仍能实例化,只在调用该方法时失败,并没有自动成为不可实例化的抽象类。
II. Python 操作协议
显示与调试表示
__str__ 和 __repr__ 都须返回字符串。前者服务于可读显示,后者帮助观察对象信息;repr 可争取无歧义或可重建,但不保证所有对象都能由该字符串重建。
class Point:
def __init__(self, x: float, y: float) -> None:
self.x, self.y = x, y
def __str__(self) -> str:
return f"({self.x}, {self.y})"
def __repr__(self) -> str:
return f"Point({self.x}, {self.y})"
p = Point(3, 4)
print(p)
print(repr(p))
print([p])
默认 str 可以退回 repr,反向不成立。列表显示元素使用 repr。默认表示中的地址与模块名不是固定答案。不要把未知 repr 文本交给 eval。
算术、反射与不支持的操作数
NotImplemented 是返回给分派机制的哨兵,表示这组操作数不支持;NotImplementedError 是异常,二者不同。不要用哨兵作布尔条件,也不要将它视为 False。下例的 math.hypot(x,y) 求两坐标组成向量的欧氏长度。
import math
class Vector:
def __init__(self, x: float, y: float) -> None:
self.x, self.y = x, y
def __repr__(self) -> str:
return f"Vector({self.x}, {self.y})"
def __add__(self, other: "Vector") -> "Vector":
if not isinstance(other, Vector):
return NotImplemented
x = self.x + other.x
y = self.y + other.y
return Vector(x, y)
def __sub__(self, other: "Vector") -> "Vector":
if not isinstance(other, Vector):
return NotImplemented
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar: float) -> "Vector":
if not isinstance(scalar, (int, float)):
return NotImplemented
return Vector(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar: float) -> "Vector":
return self * scalar
def __eq__(self, other: object) -> bool:
if not isinstance(other, Vector):
return NotImplemented
return self.x == other.x and self.y == other.y
def __abs__(self) -> float:
return math.hypot(self.x, self.y)
a=Vector(1,2)
b=Vector(3,4)
print(a+b, b-a)
print(a*3, 3*a)
print(abs(b), a==Vector(1,2))
本例加减返回新 Vector,不改输入;乘法支持 Vector×标量及标量×Vector,abs 返回长度。右侧参与反射分派,不能把运算符总说成简单调用左侧同名方法。右操作数子类型优先、同类型反射限制等细节见参考。
__eq__ 对不支持类型返回哨兵,最终 == 可以回退身份比较;不支持的排序或算术通常产生 TypeError。示例给出了有限、尺度适中的数值运算,并非完整数值库。
增强赋值与别名
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。本例 __iadd__ 修改 self 并返回 self;普通 __add__ 创建新对象。__iadd__ 也可以返回新对象,缺失或返回哨兵时可能回退普通加法,因此 += 不普遍保证原地或身份不变。
长度、下标、赋值与成员关系
class Playlist:
def __init__(self) -> None:
self._songs: list[str] = []
def add(self, song: str) -> None:
self._songs.append(song)
def __len__(self) -> int:
return len(self._songs)
def __getitem__(self, index: int) -> str:
return self._songs[index]
def __setitem__(self, index: int, value: str) -> None:
self._songs[index] = value
def __contains__(self, song: str) -> bool:
return song in self._songs
p = Playlist()
p.add("first")
p.add("second")
p[1] = "changed"
print(len(p), p[-1], "first" in p)
for song in p:
print(song)
长度须为非负整数;下标协议规定返回值和失败行为。这里委托列表,负下标有效,越界抛 IndexError,赋值返回 None,成员关系检查字符串。每个 Playlist 有独立列表。
没有 __iter__ 时,迭代可回退为从0开始连续请求 __getitem__,遇 IndexError 结束:
class Values:
def __init__(self):
self.items = ["A", "B"]
def __getitem__(self, index):
print("index", index)
return self.items[index]
print(list(Values()))
实际请求0、1、2,第三次越界后停止。显式 __iter__=None 不使用这种回退。完整迭代器在 L13 讲,本讲不提前使用 yield。
相等与身份
class Item:
def __init__(self, value):
self.value = value
def __eq__(self, other):
if not isinstance(other, Item):
return NotImplemented
return self.value == other.value
a = Item(2)
b = Item(2)
c = a
print(a == b, a is b, a is c)
输出 True False True:eq 比字段,a/b 是不同实例,c 是 a 的别名。is 可检查任意对象身份,不仅用于 None;数值或字符串的值比较用 ==,不依赖驻留/缓存。None 通常写 is None,不在没有测量时声称它必然最快。
type 的精确类型检查与 isinstance 的接受子类行为对应不同需求。内省可以辅助调试,不能代替公开接口文档。
III. 模块、绑定与入口
实际文件与模块对象
本讲自写模块是 .py 文件;Python 也有内置、扩展、冻结等模块,不把全部模块等同源文件。模块是具有命名空间的对象,import 绑定模块名,from 绑定选定对象的本地引用。
以下代码须保存为真实文件,在该目录以 Python 3.13 运行 main.py。网页单代码框不构成完整工程,不要求先运行另一页建立环境。
geometry.py
"""Geometry operations."""
def area(w, h):
return w*h
report.py
"""Formatting operations."""
def line(value):
return f"area={value}"
main.py
import geometry
import report
value = geometry.area(3, 4)
print(report.line(value))
实际 Python 3.13 输出:
area=12
文件职责与导入关系分开;一个模块被导入,不表示它的每个函数体同时执行。
显式导入与名字来源
import math
import math as m
from math import sqrt
from math import sqrt as root
print(math.sqrt(16), m.sqrt(16), sqrt(16), root(16))
as 改变导入者的访问名字,不修改原模块名。通常按标准库、第三方、本地分组并留空行;普通课程代码避免星号导入,以免覆盖名字、来源不明。星号仍有合法语法和 __all__ 等导出规则,不能描述为无条件引入所有名称。
导入引用不复制对象
config.py
count = 1
items = []
main.py
import config
from config import count, items
config.count = 2
config.items.append("x")
print(count, config.count, items)
实际 Python 3.13 输出:
1 2 ['x']
输出1、2、含x的列表。重新绑定 config.count 不会重绑已 from 导入的本地 count;列表仍是同一个对象,修改可见。把 from 当作对象拷贝或实时字段链接都不正确。
初始化与进程缓存
helper.py
print("module body")
def action():
print("function body")
main.py
import sys
import helper
import helper
same = sys.modules["helper"]
print("cached", same is helper)
helper.action()
实际 Python 3.13 输出:
module body
cached True
function body
初次普通导入运行模块顶层,def 建立函数而不执行函数体。相同进程同名普通重复导入通常复用 sys.modules;显式重载或新进程需另外分析。模块总数、目录列表与预加载状态随环境改变,不作为固定预期输出。
直接运行与被导入
tools.py
print("load")
if __name__ == "__main__":
print("demo")
main.py
import tools
import tools
print("main")
实际 Python 3.13 输出:
load
main
此处 tools 被导入时 __name__ 为tools,guard内demo不执行;顶层load仍执行。直接运行tools.py则进入该块。guard只控制自己的语句块,不能自动消除其他顶层副作用。
普通包与启动方式
mypackage/init.py
"""A regular teaching package."""
mypackage/geometry.py
def area(w, h):
return w*h
main.py
from mypackage import geometry
print(geometry.area(3, 4))
实际 Python 3.13 输出:
12
普通包使用 __init__.py;namespace package 是另一机制,不能把“可以省略”当作所有包的通用样板。包内相对导入须在合适的包上下文运行,常用 python -m 包.模块,直接执行文件路径可能失败。
IV. 搜索、环境与执行
找到的究竟是哪个模块
导入首先考虑缓存和查找器;顶层的路径式查找使用 sys.path,包的后续搜索也受包路径影响。普通脚本常将脚本目录放入搜索路径,但当前目录永远第一不是规则。启动方式、-I、环境变量、内置/冻结模块和自定义查找器都会影响结果。
random.py
print("local random")
main.py
import random
print(random.randint(1, 3))
实际 Python 3.13 输出:
local random
AttributeError: module 'random' has no attribute 'randint' (consider renaming '/privaterandom.py' since it has the same name as the standard library module named 'random' and prevents importing that standard library module)
这个受控目录与全新进程找到了本地random,因此没有randint,产生预期 AttributeError。不在学生真实项目里留下遮蔽文件。诊断可看 sys.executable、模块存在时的 __file__ 及 python -m pip —version;属性错误也可能另有原因。
虚拟环境和安装
python3 -m venv .venv
# macOS/Linux:
source .venv/bin/activate
# Windows PowerShell:
# .venv\Scripts\Activate.ps1
python -m pip --version
python -m pip install PACKAGE
python -m pip list
python -m pip install --upgrade PACKAGE
python -m pip uninstall PACKAGE
python -m pip install compatible_file.whl
PACKAGE与wheel名为占位示例,不实际安装或卸载未知包。venv隔离依赖,不是安全沙箱。wheel兼容Python/ABI/平台,旧NumPy 1.26.0不能当作本课3.13的正确pin。需要固定版本时选择与环境兼容的发行包,不要求学生一次安装所有库。
CPython与字节码
CPython是Python的一种实现,包含编译与执行过程。编译器可能产生机器码或中间表示,解释器与JIT也可结合,不能把两类简单等同一次转汇编与逐条转机器码。
import dis
def add(a, b):
return a + b
dis.dis(add)
按取值、运算、返回理解即可。具体指令和偏移是CPython版本细节,不要求背诵。
两层不同的缓存
sys.modules保存本进程已经初始化的模块;磁盘pyc保存编译代码缓存。新进程使用有效缓存可能减少编译,但仍运行顶层初始化:
helper.py
print("helper")
value = 3
main.py
import helper
print(helper.value)
实际 Python 3.13 输出:
Process 1:
helper
3
Process 2:
helper
3
两个新进程都打印helper及3。通常直接运行main.py不会为自身写缓存,导入helper可以产生带版本标签的缓存;权限、-B、有效性与实现都会改变缓存行为。首次导入大库慢可能包含依赖、加载和初始化成本,不只编译。
字节码不是跨全部版本通用格式,也不是源码保密手段。exec不直接把pyc二进制当源码。
参考:完整机制与往年例子
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}")
保留往年的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 采用明确十进制舍入。先说明接口: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,不恢复此前浮点运算丢失的信息。完整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等编译工具不全部归为解释器。旧稿Coco名称缺可靠定位,保留为待考证原稿项,不编造新介绍。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作为测试参考。
须考虑零、前导零、负数、不等长、负差、进位借位和竖式乘法。报告不超过一页,说明表示选择、符号处理及具体边界/随机对拍证据。旧稿20/20/20/10/15/5/10分项只作历史需求,7.5%与旧项目B时序不再作为现行评分发布。
收束与配套任务
L10实验的Shape族、账户、Money、Deck和真实模块工程使用固定接口,细节见Lab10。个人作业2素数五法占5%,第10周启动、截止来自solo2统一配置;L11继续讨论算法、复杂度、异常和测试。慢算法可以按合理输入规模计时,不换成另一算法冒充。
用继承或运算符以前要解释状态、支持类型、失败与副作用;用模块以前要分清对象引用、顶层运行和环境。组织方式不能替代验证。