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)的技巧来避免重复计算。我们一步步来看它的执行过程:

代码分析


    1. 基础情况(Base Case)
        • n == 0 时,由于 sum_cache = {0: 0} 在一开始就定义了,所以 if n in sum_cache 条件成立,直接返回 sum_cache[0],也就是 0。

        • 这就是递归的终止条件,虽然没有显式写成 if n == 0: return 0,但通过缓存实现了一样的效果。
    1. 递归步骤
        • 对于任意 n > 0,函数会计算 res = n + number_sum(n - 1)。

        • 计算完成后,结果会被存入 sum_cache[n],然后返回 res。
    1. 回溯过程
        • 回溯是递归的天然特性,函数从最深处(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}。


回答你的问题

    1. “递归到 n=0 时没有看到对应的取值是多少?”
        • n = 0 时,取值是 sum_cache[0],也就是 0。这是通过初始化的 sum_cache = {0: 0} 定义的。
    1. “怎么回溯呢?”
        • 回溯是从 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 是否真的减少了重复计算,以及它的作用是什么。

我的疑问是:sum_cache 似乎没有减少重复计算次数。让我们通过两种情况来分析它的作用——单次调用多次调用

单次调用场景(比如 number_sum(3))

假设我们第一次运行 number_sum(3):

    1. n = 3:
        • 检查 sum_cache,只有 {0: 0},3 不在其中。

        • 计算 res = 3 + number_sum(2)。
    1. n = 2:
        • 检查 sum_cache,2 不在其中。

        • 计算 res = 2 + number_sum(1)。
    1. n = 1:
        • 检查 sum_cache,1 不在其中。

        • 计算 res = 1 + number_sum(0)。
    1. n = 0:
        • 检查 sum_cache,0 在其中,返回 sum_cache[0] = 0。
    1. 回溯:
        • 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模式的核心是将子系统的多个组件组合起来,对外暴露一个统一的、高层次的接口。


特点

    1. 简化接口:Facade模式隐藏了子系统的复杂性,提供了一个简洁的入口点,降低客户端的使用成本。
    1. 松耦合:客户端与子系统之间的直接依赖被减少,Facade作为中介层,使得两者解耦。
    1. 职责隔离:子系统内部的实现细节对客户端不可见,客户端只需关注Facade提供的功能。
    1. 灵活性有限:Facade模式简化了操作,但可能会限制对子系统更细粒度控制的需求。
    1. 常见应用场景:适用于需要整合多个复杂模块的场景,如操作系统、数据库连接库或多层架构系统。

在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

为什么这个例子更具代表性?

    1. 复杂的子系统协作:家庭影院系统涉及多个独立组件(DVD播放器、功放、投影仪、灯光),它们需要按照特定顺序协调工作。这种复杂性正是Facade模式的典型应用场景。
    2. 客户端体验优化:客户端只需调用 watch_movieend_movie 两个方法,无需关心每个组件的开关和配置顺序,大大简化了操作。
    3. 现实场景贴合:家庭影院是一个生活中常见的例子,直观地展示了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)。
  • 逻辑:
      1. 打印 "__get__ is calling",表示方法被触发。

      1. 如果 instance 是 None(即通过类访问,如 MyClass.data),返回描述符对象本身 self。

    1. 如果 instance 不是 None(通过实例访问,如 obj.data):
        • 调用 self.func(instance),计算值。

        • setattr(instance, self.func.__name__, value) 将计算结果存储到实例的属性中,属性名是 self.func.__name__(即函数名 data)。

      • 返回计算结果 value。

 

定义 MyClass 类
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)
print(obj.data)
    1. 触发 __get__
        • 因为 data 是 MyClass 的类属性,且是一个描述符(定义了 __get__),访问 obj.data 会调用 LazyLoad.__get__。
        • 参数:
            • self:MyClass.data(LazyLoad 实例)。
            • instance:obj。
            • owner:MyClass。
        • 输出:"__get__ is calling"。
    1. 检查 instance
        • instance 是 obj,不是 None,所以进入主逻辑。
    1. 计算值
        • self.func 是原始的 data 方法。
        • self.func(instance) 调用 data(obj):
            • data 方法的 self 参数绑定到 obj。
            • 执行 data 方法,打印 "计算数据...",返回 [1, 2, 3]。
        • value = [1, 2, 3]。
    1. 缓存结果
        • 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,描述符机制被短路。
    1. 返回值
        • __get__ 返回 value,即 [1, 2, 3]。
        • print(obj.data) 输出 [1, 2, 3]。

此时输出:

__get__ is calling
计算数据...
[1, 2, 3]
第二次调用 print(obj.data)
print(obj.data)
    1. 不再触发 __get__
        • 第一次访问后,setattr 将 data 属性添加到了 obj.__dict__ 中(obj.data = [1, 2, 3])。
        • Python 的属性查找规则是:先找实例属性(obj.__dict__),再找类属性。
        • 因为 obj.data 现在是实例属性 [1, 2, 3],而不是类属性的 LazyLoad 描述符,所以不会再调用 __get__。
    1. 直接访问实例属性
        • obj.data 直接返回 obj.__dict__["data"],即 [1, 2, 3]。
    1. 输出
        • 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),但那样就失去了缓存功能。

 

 

 

 

 

 

 

 

 

 

 

posted @ 2025-02-27 14:16  AlphaGeek  阅读(6)  评论(0)    收藏  举报