application framework 通过复用已有 class、并 override method 来定制 behavior,从而构建新的 application。 framework 的核心是 GoF Design Patterns 中的 Template Method: 它是定义在 base class 中的一个 method, 通过调用其他 base-class method 来驱动 application, 其中一些 method 由你 override。
Python 自己的 unittest 就是这种 application framework。 你 subclass TestCase,并提供 setUp()、自己的 test_* method, 以及 tearDown()。 TestCase.run() 就是 template method。 它先调用 setUp(),然后调用你的 test method,最后调用 tearDown()。 仅仅构造一个 TestCase 不会运行任何东西。 test runner 会在完整 object 上调用 run()。
Template Method 会把 algorithm 的整体形状锚定在 base class 中。 subclass 负责提供各个具体 step。 typing.final decorator, 在 Making a Class Final 中用于 class, 同样也能用于单独的 method。 它会锁住 template method,使 subclass 不能改变 flow。 这里,给 run() 加上 @final 后,任何 override 它的 subclass 都会被拒绝:
# template_method.py
from typing import final, override
class ApplicationFramework:
@final
def run(self) -> None:
for _ in range(2):
self.customize1()
self.customize2()
def customize1(self) -> None: ...
def customize2(self) -> None: ...
# Create an application by filling in the steps:
class MyApp(ApplicationFramework):
@override
def customize1(self) -> None:
print("Nudge, nudge, wink, wink!")
@override
def customize2(self) -> None:
print("Say no more, say no more!")
MyApp().run()
#: Nudge, nudge, wink, wink!
#: Say no more, say no more!
#: Nudge, nudge, wink, wink!
#: Say no more, say no more!
client 在 derived class 中提供 customize1() 和 customize2()。 run() 启动驱动整个 application 的 engine。
base class 会调用以后才写出来的代码,有时甚至是几年以后才写。 framework 作者把这称为 Hollywood Principle:“don’t call us, we’ll call you.” 这种反转的一般名称是 Inversion of Control: framework 定义 control flow,并调用你的代码, 而不是由你的代码去调用一个 library。
只有 type checker 会强制执行 @final。 runtime 中,这个 decorator 只是在 function 上设置 __final__ = True, interpreter 里没有任何东西读取这个 attribute。 如果希望 interpreter 自己拒绝 override, Making a Class Final 里的 __init_subclass__() technique 同样可以用于 method: 在发现 "run" in cls.__dict__ 时抛异常。
这些 step method 默认都是 ..., 因此 subclass 只需要 override 它关心的 step, 漏掉的 step 会悄无声息地什么也不做。 这种 optional step 称为 hook。 开头例子里的 setUp() 和 tearDown() 就是 hook: TestCase 提供 do-nothing version, 所以不需要 setup 的 test class 可以直接跳过它们。 这种 silent behavior 会掩盖拼写错误:def customise1()(s 而不是 z) 会添加一个新 method,同时 base 里的 do-nothing version 仍然保留。 这就是为什么这些 listing 中每个 step override 都带有 @override: 这样 type checker 会拒绝一个实际上没有 override 任何东西的 method。
checker 看到的只有 decorator。 如果 misspelled method 没写 @override,checker 会把它当作新 method 并接受。 没有任何 typing construct 能禁止 subclass 添加 method, 所以 checker 捕捉不到这种情况。 interpreter 则可以。 base class 的 __init_subclass__() 会在每个 subclass 的 class statement 执行, standard library 的 difflib 则用来找出近似匹配的 name:
# near_miss.py
from difflib import get_close_matches
from typing import final, override
class ApplicationFramework:
@final
def run(self) -> None:
for _ in range(2):
self.customize1()
self.customize2()
def customize1(self) -> None: ...
def customize2(self) -> None: ...
def __init_subclass__(cls) -> None:
super().__init_subclass__()
hooks = {
name
for base in cls.__mro__[1:]
for name in vars(base)
if not name.startswith("__")
}
for name in vars(cls):
if name in hooks or name.startswith("__"):
continue
if near := get_close_matches(name, hooks):
raise TypeError(
f"{cls.__name__}.{name}: "
f"did you mean{near[0]}?"
)
class MyApp(ApplicationFramework):
@override
def customize1(self) -> None:
print("one")
def report(self) -> None: ...
try:
class Typo(ApplicationFramework):
def customise1(self) -> None:
print("never runs")
except TypeError as e:
print(e)
#: Typo.customise1: did you mean customize1?
hooks 收集 base class 定义的每个 non-dunder name。 如果 subclass 的 name 与其中某个完全相同,那就是 override; 如果它与任何 name 都不相似,例如 report(), 那就是普通的新 method。 这两种情况都会通过。 只有 near miss 才会产生 TypeError, 而 error message 会指出作者很可能本来想写的 method。 class Typo statement 会直接抛 TypeError,无法完成定义, 所以拼写错误会在 import time 就失败, 而不是等到 framework 真正运行后,某个 step 才静悄悄地什么也不做。 当然,禁止所有新 method 也能抓住 typo, 但那样也会禁止 report(); 一个不允许 subclass 添加 helper method 的 framework 限制太强。
如果每个 subclass 都必须提供某个 step, 可以继承 ABC,并给那个 step 加 @abstractmethod, 如 Rethinking Objects 所示。 这样 runtime 会拒绝实例化忘记实现它的 subclass。
下面的 test 提供一个 recording subclass,并验证 anchored flow:
# test_template_method.py
from typing import override
from template_method import ApplicationFramework
def test_template_method_runs_steps_in_order() -> None:
calls: list[str] = []
class Recorder(ApplicationFramework):
@override
def customize1(self) -> None:
calls.append("one")
@override
def customize2(self) -> None:
calls.append("two")
Recorder().run() # The client starts the engine
# Loop runs twice
assert calls == ["one", "two", "one", "two"]
启动 engine 的应该是 client,而不是 ApplicationFramework。 framework 当然可以 在自己的 constructor 中调用 run(), 但这样一来,定义了自己 __init__() 的 subclass 就会掉进陷阱。 因为 run() 会调用 subclass 提供的 method, subclass 必须先完成自己的 setup,才能调用 super().__init__()。 如果按 Python programmer 通常习惯的写法先调用 super().__init__(), engine 就会运行在一个只初始化到一半的 object 上:
# premature_engine.py
from typing import final, override
class Framework:
def __init__(self) -> None:
self.run()
@final
def run(self) -> None:
self.step()
def step(self) -> None: ...
class Greeter(Framework):
def __init__(self, name: str) -> None:
# With the usual style, the engine calls run()
super().__init__()
self.name = name # ...before this line runs
@override
def step(self) -> None:
print(f"Hello,{self.name}!")
try:
Greeter("Robin")
except AttributeError as e:
print(e)
#: 'Greeter' object has no attribute 'name'