Chapter4 Structural Design Patterns
1、如下代码的函数,一直会往下递归,递归到n=0时,但是没有看到对应的取值是多少啊?那这个怎么回溯呢?
sum_cache = {0: 0} def number_sum(n): """Returns the sum of the first n numbers""" assert n >= 0, "n must be >= 0" if n in sum_cache: return sum_cache[n] res = n + number_sum(n - 1) # Add the value to the cache sum_cache[n] = res return res if __name__ == "__main__": from timeit import Timer t = Timer( "number_sum(300)", "from __main__ import number_sum", ) print("Time: ", t.timeit())
- 当 n == 0 时,由于 sum_cache = {0: 0} 在一开始就定义了,所以 if n in sum_cache 条件成立,直接返回 sum_cache[0],也就是 0。
- 这就是递归的终止条件,虽然没有显式写成 if n == 0: return 0,但通过缓存实现了一样的效果。
我来帮你分析这个递归函数的运行过程和回溯机制。
这个函数 number_sum(n) 的作用是计算从 0 到 n 的所有整数之和,使用了记忆化(memoization)的技巧来避免重复计算。我们一步步来看它的执行过程:
代码分析
- 基础情况(Base Case):
- 当 n == 0 时,由于 sum_cache = {0: 0} 在一开始就定义了,所以 if n in sum_cache 条件成立,直接返回 sum_cache[0],也就是 0。
- 这就是递归的终止条件,虽然没有显式写成 if n == 0: return 0,但通过缓存实现了一样的效果。
- 递归步骤:
- 对于任意 n > 0,函数会计算 res = n + number_sum(n - 1)。
- 计算完成后,结果会被存入 sum_cache[n],然后返回 res。
- 回溯过程:
- 回溯是递归的天然特性,函数从最深处(n = 0)开始逐步返回结果,层层累加。
以 n = 3 为例,手动推导
假设调用 number_sum(3):
- sum_cache = {0: 0}(初始状态)
- n = 3:
- 3 不在 sum_cache 中,计算 res = 3 + number_sum(2)
- 调用 number_sum(2)
- n = 2:
- 2 不在 sum_cache 中,计算 res = 2 + number_sum(1)
- 调用 number_sum(1)
- n = 1:
- 1 不在 sum_cache 中,计算 res = 1 + number_sum(0)
- 调用 number_sum(0)
- n = 0:
- 0 在 sum_cache 中,返回 sum_cache[0] = 0
现在开始回溯:
- n = 1:
- res = 1 + number_sum(0) = 1 + 0 = 1
- sum_cache[1] = 1
- 返回 1
- n = 2:
- res = 2 + number_sum(1) = 2 + 1 = 3
- sum_cache[2] = 3
- 返回 3
- n = 3:
- res = 3 + number_sum(2) = 3 + 3 = 6
- sum_cache[3] = 6
- 返回 6
最终结果是 6,sum_cache 变成 {0: 0, 1: 1, 2: 3, 3: 6}。
回答你的问题
- “递归到 n=0 时没有看到对应的取值是多少?”
- 当 n = 0 时,取值是 sum_cache[0],也就是 0。这是通过初始化的 sum_cache = {0: 0} 定义的。
- “怎么回溯呢?”
- 回溯是从 n = 0 开始,层层返回计算结果。每返回一层,就把当前层的 n 加到结果上,并缓存下来,最终得到总和。
- 比如 n = 3,回溯过程是:
- number_sum(0) = 0
- number_sum(1) = 1 + 0 = 1
- number_sum(2) = 2 + 1 = 3
- number_sum(3) = 3 + 3 = 6
为什么不会无限递归?
- 因为有 sum_cache 缓存和 n = 0 的基础情况,递归会逐步减小 n 直到 0,然后通过缓存和返回值的累加完成计算。
验证结果
这个函数计算的是 0 到 n 的和,等价于数学公式 n * (n + 1) // 2:
- n = 3 时,3 * 4 // 2 = 6,与递归结果一致。
- n = 300 时,300 * 301 // 2 = 45150,你可以用代码验证一下。
代码运行时间
你用 Timer 测试了 number_sum(300) 的性能。因为有缓存,第一次计算会填充 sum_cache,后续调用会直接返回缓存结果,时间复杂度从 O(n) 降到 O(1)(如果忽略填充缓存的初次计算)。
2、但这个代码,我感觉并没有通过sum_cache减少计算重复计算次数啊?请举例具体说明下
这是一个很关键的问题:我将仔细分析一下这个代码,并通过例子来说明 sum_cache 是否真的减少了重复计算,以及它的作用是什么。
单次调用场景(比如 number_sum(3))
假设我们第一次运行 number_sum(3):
- n = 3:
- 检查 sum_cache,只有 {0: 0},3 不在其中。
- 计算 res = 3 + number_sum(2)。
- n = 2:
- 检查 sum_cache,2 不在其中。
- 计算 res = 2 + number_sum(1)。
- n = 1:
- 检查 sum_cache,1 不在其中。
- 计算 res = 1 + number_sum(0)。
- n = 0:
- 检查 sum_cache,0 在其中,返回 sum_cache[0] = 0。
- 回溯:
- n = 1:res = 1 + 0 = 1,sum_cache[1] = 1,返回 1。
- n = 2:res = 2 + 1 = 3,sum_cache[2] = 3,返回 3。
- n = 3:res = 3 + 3 = 6,sum_cache[3] = 6,返回 6。
最终 sum_cache = {0: 0, 1: 1, 2: 3, 3: 6}。
分析:
- 在这个单次调用的过程中,sum_cache 的检查 if n in sum_cache 只在 n = 0 时命中。
- 对于 n = 1, 2, 3,每次都是递归计算,没有直接从缓存中取值。
- 结论:在单次递归调用中,sum_cache 没有减少递归的计算次数,因为每个 n 的值只计算一次,回溯时不会重复计算相同的值。
你的感觉是对的:在这种单线程、单次调用的场景下,sum_cache 的作用非常有限。它只是存储了结果,但没有避免递归的展开。
多次调用场景(缓存的真正作用)
现在假设我们多次调用 number_sum,比如在一个程序中:
print(number_sum(3)) # 第一次调用 print(number_sum(2)) # 第二次调用 print(number_sum(3)) # 第三次调用
第一次调用 number_sum(3):
- 和上面一样,递归计算:
- n = 3 → 3 + number_sum(2)
- n = 2 → 2 + number_sum(1)
- n = 1 → 1 + number_sum(0)
- n = 0 → 返回 0
- 回溯后,sum_cache = {0: 0, 1: 1, 2: 3, 3: 6}。
- 输出:6。
第二次调用 number_sum(2):
- n = 2:
- 检查 sum_cache,发现 2 已在缓存中,直接返回 sum_cache[2] = 3。
- 没有递归,直接返回 3。
- 输出:3。
第三次调用 number_sum(3):
- n = 3:
- 检查 sum_cache,发现 3 已在缓存中,直接返回 sum_cache[3] = 6。
- 没有递归,直接返回 6。
- 输出:6。
分析:
- 第一次调用时,递归计算填充了 sum_cache。
- 第二次和第三次调用时,因为结果已经缓存,直接从 sum_cache 获取,避免了重复的递归计算。
- 结论:sum_cache 的作用是在多次调用时避免重复计算,而不是在单次递归调用中减少计算量。
3、Facade设计模式的概念和特点总结
概念
Facade(外观模式)是一种结构型设计模式,旨在为复杂的子系统提供一个简化的接口。
它通过封装子系统的复杂性,使得外部客户端可以更容易地与系统交互,而无需直接处理底层的细节。
Facade模式的核心是将子系统的多个组件组合起来,对外暴露一个统一的、高层次的接口。
特点
- 简化接口:Facade模式隐藏了子系统的复杂性,提供了一个简洁的入口点,降低客户端的使用成本。
- 松耦合:客户端与子系统之间的直接依赖被减少,Facade作为中介层,使得两者解耦。
- 职责隔离:子系统内部的实现细节对客户端不可见,客户端只需关注Facade提供的功能。
- 灵活性有限:Facade模式简化了操作,但可能会限制对子系统更细粒度控制的需求。
- 常见应用场景:适用于需要整合多个复杂模块的场景,如操作系统、数据库连接库或多层架构系统。
在facade.py代码中,OperatingSystem 类作为一个Facade,封装了 FileServer 和 ProcessServer 的操作,
客户端通过 OperatingSystem 的简单方法即可完成文件的创建和进程的管理,而无需直接与底层服务器类交互。
一个更具代表性的Facade模式示例
上述例子虽然展示了Facade模式的基本思想,但它过于简单,且子系统之间的协作不够明显。
下面是一个更贴近现实、更具代表性的例子:模拟一个家庭影院系统。
示例代码
# 子系统组件 class DVDPlayer: def on(self): print("DVD Player is on") def play(self, movie): print(f"DVD Player is playing '{movie}'") def off(self): print("DVD Player is off") class Amplifier: def on(self): print("Amplifier is on") def set_volume(self, level): print(f"Amplifier volume set to {level}") def off(self): print("Amplifier is off") class Projector: def on(self): print("Projector is on") def set_input(self, source): print(f"Projector input set to {source}") def off(self): print("Projector is off") class Lights: def dim(self, level): print(f"Lights dimmed to {level}%") def on(self): print("Lights are on") # Facade类 class HomeTheaterFacade: def __init__(self): self.dvd = DVDPlayer() self.amp = Amplifier() self.projector = Projector() self.lights = Lights() def watch_movie(self, movie): print("\nGetting ready to watch a movie...") self.lights.dim(10) self.projector.on() self.projector.set_input("DVD") self.amp.on() self.amp.set_volume(5) self.dvd.on() self.dvd.play(movie) def end_movie(self): print("\nShutting down the theater...") self.dvd.off() self.amp.off() self.projector.off() self.lights.on() # 客户端代码 def main(): home_theater = HomeTheaterFacade() home_theater.watch_movie("Inception") home_theater.end_movie() if __name__ == "__main__": main()
Getting ready to watch a movie... Lights dimmed to 10% Projector is on Projector input set to DVD Amplifier is on Amplifier volume set to 5 DVD Player is on DVD Player is playing 'Inception' Shutting down the theater... DVD Player is off Amplifier is off Projector is off Lights are on
为什么这个例子更具代表性?
- 复杂的子系统协作:家庭影院系统涉及多个独立组件(DVD播放器、功放、投影仪、灯光),它们需要按照特定顺序协调工作。这种复杂性正是Facade模式的典型应用场景。
- 客户端体验优化:客户端只需调用 watch_movie 和 end_movie 两个方法,无需关心每个组件的开关和配置顺序,大大简化了操作。
- 现实场景贴合:家庭影院是一个生活中常见的例子,直观地展示了Facade模式如何将繁琐的操作封装为简单的接口。
这个例子相比原来的代码更能体现Facade模式的核心价值:通过一个统一的接口管理复杂的子系统交互,同时保持代码的可读性和可维护性。
4、如何理解惰性加载(延迟加载)设计模式?如下代码详细的理解,为什么能够实现惰性加载lazy loading。
class LazyLoad: def __init__(self, func): self.func = func def __get__(self, instance, owner): print(f'__get__ is calling') if instance is None: print(f'self: {self}') return self value = self.func(instance) setattr(instance, self.func.__name__, value) return value class MyClass: @LazyLoad def data(self): print("计算数据...") return [1, 2, 3] obj = MyClass() print(obj.data) # 第一次调用触发 data 方法计算 print(obj.data) # 直接返回缓存值(不再计算)
# 返回结果 __get__ is calling 计算数据... [1, 2, 3] [1, 2, 3]
接下来, 我来逐行详细解释这段代码的逻辑,以及为什么会得到这样的输出结果。
这段代码实现了一个“延迟加载”(lazy loading)的描述符模式,通过 LazyLoad 类和 __get__ 方法来控制属性的计算和缓存。
class LazyLoad: def __init__(self, func): self.func = func
我们会一步步拆解代码的执行流程。
- LazyLoad 是一个描述符类,__init__ 方法接收一个函数 func 作为参数,并将其存储在实例的 self.func 属性中。\
- 这个 func 稍后会被用作延迟计算的逻辑。
def __get__(self, instance, owner): print(f'__get__ is calling') if instance is None: print(f'self: {self}') return self value = self.func(instance) setattr(instance, self.func.__name__, value) return value
- __get__ 是描述符的核心方法,当访问属性时被调用。
- 参数:
- self:LazyLoad 的实例(描述符对象)。
- instance:调用描述符的实例(这里是 MyClass 的实例 obj)。
- owner:拥有描述符的类(这里是 MyClass)。
- 逻辑:
- 打印 "__get__ is calling",表示方法被触发。
- 如果 instance 是 None(即通过类访问,如 MyClass.data),返回描述符对象本身 self。
- 如果 instance 不是 None(通过实例访问,如 obj.data):
- 调用 self.func(instance),计算值。
- 用 setattr(instance, self.func.__name__, value) 将计算结果存储到实例的属性中,属性名是 self.func.__name__(即函数名 data)。
- 返回计算结果 value。
class MyClass: @LazyLoad def data(self): print("计算数据...") return [1, 2, 3]
- @LazyLoad 是一个装饰器,等价于 data = LazyLoad(data)。
- 这里:
- data 是一个方法,定义了计算逻辑:打印 "计算数据..." 并返回 [1, 2, 3]。
- @LazyLoad 将 data 方法传入 LazyLoad.__init__,创建了一个 LazyLoad 实例,并将其赋值给类属性 data。
- 所以,MyClass.data 不再是原始的 data 方法,而是一个 LazyLoad 对象(描述符)。
obj = MyClass()
- 创建 MyClass 的实例 obj。
- 此时,obj 没有任何实例属性(obj.__dict__ 是 {}),但类 MyClass 有一个类属性 data,它是 LazyLoad 的实例。
print(obj.data)
- 触发 __get__:
- 因为 data 是 MyClass 的类属性,且是一个描述符(定义了 __get__),访问 obj.data 会调用 LazyLoad.__get__。
- 参数:
- self:MyClass.data(LazyLoad 实例)。
- instance:obj。
- owner:MyClass。
- 输出:"__get__ is calling"。
- 检查 instance:
- instance 是 obj,不是 None,所以进入主逻辑。
- 计算值:
- self.func 是原始的 data 方法。
- self.func(instance) 调用 data(obj):
- data 方法的 self 参数绑定到 obj。
- 执行 data 方法,打印 "计算数据...",返回 [1, 2, 3]。
- value = [1, 2, 3]。
- 缓存结果:
- self.func.__name__ 是 "data"(函数的名字)。
- setattr(instance, "data", value) 等价于 obj.data = [1, 2, 3]。
- 这将 [1, 2, 3] 存入 obj.__dict__,现在 obj.__dict__ = {'data': [1, 2, 3]}。
- 关键点:实例属性 obj.data 覆盖了类属性 MyClass.data,描述符机制被短路。
- 返回值:
- __get__ 返回 value,即 [1, 2, 3]。
- print(obj.data) 输出 [1, 2, 3]。
此时输出:
__get__ is calling 计算数据... [1, 2, 3]
print(obj.data)
- 不再触发 __get__:
- 第一次访问后,setattr 将 data 属性添加到了 obj.__dict__ 中(obj.data = [1, 2, 3])。
- Python 的属性查找规则是:先找实例属性(obj.__dict__),再找类属性。
- 因为 obj.data 现在是实例属性 [1, 2, 3],而不是类属性的 LazyLoad 描述符,所以不会再调用 __get__。
- 直接访问实例属性:
- obj.data 直接返回 obj.__dict__["data"],即 [1, 2, 3]。
- 输出:
- print(obj.data) 输出 [1, 2, 3]。
- 没有 "__get__ is calling" 或 "计算数据...",因为计算只在第一次发生,结果已被缓存。
此时累计输出:
__get__ is calling 计算数据... [1, 2, 3] [1, 2, 3]
为什么第二次不计算?
- LazyLoad 的设计是“延迟加载并缓存”:
- 第一次访问时,计算值并用 setattr 缓存到实例属性中。
- 之后,实例属性覆盖了描述符,属性查找不会再触发 __get__,实现了“只计算一次”的效果。
如果想每次都触发 __get__,可以避免使用 setattr,例如直接返回 self.func(instance),但那样就失去了缓存功能。

浙公网安备 33010602011771号