0%

关于我用gemini的一些心得体会

​ 最近刚刚白嫖了Gemini的15个月学生会员,于是开始用这个号称目前top1的大模型,并且在使用过程中,有了一些自己的使用心得。

用Gemini进行文档总结

​ 在碰到需要阅读的外文文档的时候,如果是公网资源,可以直接交给AI链接,然后让他总结并娓娓道来详细讲给你听。

​ 这里我自认为是一个技巧,可以让他讲解的更易于理解。

image-20250625211529087

​ 这是21年CS61A的关于记忆化的一节课课件,我在听教授讲的时候听的很迷糊(当然没有说教授讲的不好,只是我没适应 ),于是直接把把链接给Gemini让他帮我总结:

1
2
3
https://web.archive.org/web/20210506190608/https://cs61a.org/assets/slides/21-Memoization_4pp.pdf

帮我总结并娓娓道来

但我发现他总结的很精简,我需要的是那种能学到东西的文章,于是改了一下prompt:

1
请你给出代码示例,像写一个完整的博客文档那样详细的讲解这节课的所有内容

然后我就得到了我想要的:

好的,没问题!让我们像撰写一篇详细的博客文章一样,深入探讨“记忆化(Memoization)”的奥秘。


博客文章:用“记忆”给代码加速——深入解析“记忆化(Memoization)”

你好,我是你的AI助手 Gemini。在编程的世界里,我们总在追求两件事:正确性效率。今天,我们将探讨一种能够极大提升代码效率,特别是递归函数效率的强大技术——记忆化(Memoization)

想象一下,你让计算机解决一个问题,它却像一个健忘的学徒,一遍又一遍地计算着早已知道答案的子问题。这不仅浪费了宝贵的计算资源,也让你的程序运行得像蜗牛一样慢。记忆化,就是赋予我们程序“记忆”能力的魔法。

一、问题的根源:重复的计算

让我们从一个经典得不能再经典的例子开始:斐波那契数列(Fibonacci Sequence)

它的定义很简单:

  • fib(0) = 0
  • fib(1) = 1
  • fib(n) = fib(n-1) + fib(n-2) (当 n > 1 时)

用 Python 来实现这个定义,最直观的方式就是写一个简单的递归函数:

版本一:朴素的递归实现

Python

1
2
3
4
5
6
7
8
9
10
def fib(n):
"""计算斐波那契数列的第 n 项(朴素递归法)"""
if n <= 1:
return n
return fib(n-1) + fib(n-2)

# 让我们来测试一下
print(f"fib(5) = {fib(5)}")
print(f"fib(10) = {fib(10)}")
# print(f"fib(40) = {fib(40)}") # 尝试运行这行,你会等到天荒地老...

这段代码看起来简洁优美,完美地翻译了数学定义。但它的性能如何呢?

让我们来追踪一下 fib(5) 的计算过程,你会看到一棵像这样的“调用树”:

1
2
3
4
5
6
7
8
9
                   fib(5)
/ \
fib(4) fib(3)
/ \ / \
fib(3) fib(2) fib(2) fib(1)
/ \ / \ / \
fib(2) fib(1) fib(1) fib(0) fib(1) fib(0)
/ \
fib(1) fib(0)

发现了问题所在吗?

  • fib(3) 被计算了 2 次。
  • fib(2) 被计算了 3 次。
  • fib(1) 被计算了 5 次。

仅仅是计算 fib(5),就有如此多的冗余。当 n 增长到 40 或 50,重复计算的次数将呈指数级爆炸式增长(时间复杂度为 O(2^n)),足以让现代计算机陷入漫长的等待。

这就是我们需要“记忆化”来拯救世界的时刻。

二、核心思想:记住你算过的

记忆化(Memoization)的核心思想异常简单:

如果一个计算你已经执行过,就把结果存起来。下次再遇到同样的计算,直接拿出存好的结果,不要再算一遍。

这本质上是一种“用空间换时间”的策略。我们开辟一小块内存空间(我们的“记忆”),用来缓存计算结果,从而避免了大量耗时的重复计算。

我们如何为上面的 fib 函数植入“记忆”呢?通常,我们会使用一个字典(dictionary)哈希表(hash map)来作为我们的缓存(cache)。

版本二:带有记忆化的递归实现

Python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 创建一个缓存字典,用来存储已经计算过的结果
memo_cache = {}

def fib_memo(n):
"""计算斐波那G契数列的第 n 项(带记忆化的递归)"""
# 1. 在计算之前,先检查“记忆”里有没有
if n in memo_cache:
return memo_cache[n] # 如果有,直接返回结果

# 2. 如果没有,再进行计算(和之前的逻辑一样)
if n <= 1:
result = n
else:
result = fib_memo(n-1) + fib_memo(n-2)

# 3. 在返回结果之前,把这次的结果存入“记忆”
memo_cache[n] = result
return result

# 让我们来测试一下性能的飞跃
print(f"fib_memo(40) = {fib_memo(40)}")
print(f"fib_memo(50) = {fib_memo(50)}")
# 这一次,结果几乎是瞬间出现的!

让我们再次分析 fib_memo(5) 的调用过程:

  1. fib_memo(5):缓存里没有,计算 fib_memo(4) + fib_memo(3)
  2. fib_memo(4):缓存里没有,计算 fib_memo(3) + fib_memo(2)
  3. fib_memo(3):缓存里没有,计算 fib_memo(2) + fib_memo(1)
  4. fib_memo(2):缓存里没有,计算 fib_memo(1) + fib_memo(0)
  5. fib_memo(1):返回 1,并存入缓存 {1: 1}
  6. fib_memo(0):返回 0,并存入缓存 {0: 0, 1: 1}
  7. fib_memo(2) 完成计算 (1+0=1),存入缓存 {0:0, 1:1, 2:1}
  8. fib_memo(3) 需要 fib_memo(1)直接从缓存读取,计算 (1+1=2),存入缓存 {..., 3:2}
  9. fib_memo(4) 需要 fib_memo(2)直接从缓存读取,计算 (2+1=3),存入缓存 {..., 4:3}
  10. fib_memo(5) 需要 fib_memo(3)直接从缓存读取,计算 (3+2=5),存入缓存 {..., 5:5}

看!fib_memo(3)fib_memo(2) 现在只被完整地计算了一次。所有后续的调用都直接从缓存中获取了结果。通过这种方式,我们将时间复杂度从 O(2^n) 奇迹般地降到了 **O(n)**,因为从 0 到 n 的每个斐波那契数都只被计算了一次。

三、更优雅的实现:Python装饰器

虽然上面的方法很有效,但它把缓存逻辑和业务逻辑(斐波那契计算)混在了一起。如果我有很多个函数都需要记忆化,难道要给每个函数都写一套缓存逻辑吗?

当然不!这正是 Python 中装饰器(Decorator)大显身手的时刻。装饰器允许我们在不修改函数代码本身的情况下,为函数添加额外的功能。

我们可以编写一个通用的 memoize 装饰器。

版本三:使用装饰器实现记忆化

Python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def memoize(func):
"""一个通用的记忆化装饰器"""
cache = {}
def wrapper(*args):
# 注意:这里我们使用 *args 作为键,使其能处理多个参数的函数
# 但要确保参数是可哈希的(比如数字、字符串、元组)
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper

@memoize
def fib_decorator(n):
"""应用了记忆化装饰器的斐波那契函数"""
if n <= 1:
return n
return fib_decorator(n-1) + fib_decorator(n-2)

# 测试一下,同样飞快!
print(f"fib_decorator(50) = {fib_decorator(50)}")
print(f"fib_decorator(100) = {fib_decorator(100)}")

这里的 @memoize 语法糖,完全等同于 fib_decorator = memoize(fib_decorator)

这种方式的好处是:

  1. 代码整洁fib_decorator 函数内部只关心核心的计算逻辑。
  2. 高度复用:你可以把 @memoize 这个装饰器应用到任何其他需要记忆化的、符合条件的函数上。

四、什么时候应该使用记忆化?

记忆化虽好,但并非万能。它在以下场景中最为有效:

  1. 函数是“纯”的(Pure Function):这是最重要的前提。纯函数意味着对于相同的输入,永远返回相同的输出,并且没有可观察的副作用(比如修改全局变量、打印日志等)。斐波那契函数就是典型的纯函数。
  2. 存在大量重叠子问题(Overlapping Subproblems):就像我们的斐波那契例子,递归过程中反复求解相同的子问题。如果一个问题可以被分解成子问题,但子问题之间毫无关联,那么记忆化就派不上用场。
  3. 计算成本较高:如果一个函数本身的计算非常快,那么为它增加缓存和查找的开销可能得不偿失。

总结

今天,我们通过斐波那契数列的例子,深入了解了记忆化这一强大的性能优化技术。

  • 核心痛点:递归调用中存在大量的重复计算,导致指数级的时间复杂度。
  • 解决方案:使用缓存(如字典)存储计算结果,避免重复劳动,实现“用空间换时间”。
  • 最佳实践:将记忆化逻辑封装成一个通用的装饰器,以保持业务代码的纯粹和整洁。

记忆化是动态规划(Dynamic Programming)思想的重要组成部分,是每个程序员都应该掌握的基本功。希望通过这篇详细的讲解,你已经彻底理解了它的原理和应用。现在,去检查一下你的代码,看看有没有哪个“健忘”的函数正在等待你赋予它“记忆”吧!

读这种文章的感觉就和读大佬写的博客文一样酣畅淋漓!然后我又让他解释了一下其中装饰器的作用,还是这种模式的回复:

当然可以!很高兴你对这个话题有如此浓厚的兴趣。装饰器是 Python 中一个非常强大且优雅的特性,一旦你理解了它,就会发现它无处不在。

让我们一步一步,把“@memoize 等同于 fib_decorator = memoize(fib_decorator)”这块硬骨头彻底啃下来。

第 1 步:理解 Python 的核心前提——函数是“一等公民”

在 Python 中,函数(function)和我们常见的数字(123)、字符串("hello")、列表([1, 2, 3])一样,是一种对象。这意味着你可以像对待任何其他数据一样对待它。

这具体体现在三个方面:

  1. 可以被赋值给一个变量

    Python

    1
    2
    3
    4
    5
    6
    7
    8
    9
    def say_hello():
    print("Hello!")

    # 将函数 say_hello 赋值给变量 greet
    greet = say_hello

    # 现在调用 greet 和调用 say_hello 的效果完全一样
    greet() # 输出: Hello!
    say_hello() # 输出: Hello!
  2. 可以作为参数传递给另一个函数

    Python

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    def simple_operation(func, x, y):
    # 调用传进来的函数 func
    return func(x, y)

    def add(a, b):
    return a + b

    def subtract(a, b):
    return a - b

    # 把 add 函数作为参数传进去
    result1 = simple_operation(add, 10, 5)
    print(f"Result of addition: {result1}") # 输出: 15

    # 把 subtract 函数作为参数传进去
    result2 = simple_operation(subtract, 10, 5)
    print(f"Result of subtraction: {result2}") # 输出: 5

    这里的 simple_operation 就是一个高阶函数(Higher-Order Function),因为它接受了另一个函数作为参数。

  3. 可以作为另一个函数的返回值

    Python

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    def get_greeter(name):
    # 定义一个内部函数
    def greeter():
    print(f"Hello, {name}!")
    # 返回这个内部函数
    return greeter

    # 调用 get_greeter,它返回了 greeter 函数
    greet_bob = get_greeter("Bob")
    greet_alice = get_greeter("Alice")

    # greet_bob 现在是一个函数了!
    greet_bob() # 输出: Hello, Bob!
    greet_alice() # 输出: Hello, Alice!

理解了“函数是一等公民”这个核心概念,我们就为理解装饰器铺平了道路。

第 2 步:手动实现一个装饰器

装饰器本质上就是一个高阶函数,它接收一个函数作为输入,并返回一个新的函数作为输出。这个新的函数通常会在执行原始函数的前后,添加一些额外的功能。

让我们来手动实现这个过程,不使用 @ 语法。

假设我们有一个简单的函数:

Python

1
2
def ordinary_function():
print("正在执行核心业务...")

现在,我们想在不修改 ordinary_function 源码的情况下,在它执行前后打印日志。我们可以写一个“装饰函数”:

Python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def logging_decorator(original_func):
# original_func 就是我们想装饰的函数,比如 ordinary_function

# 1. 定义一个新的“包装”函数
def wrapper():
print(f"【日志】函数 '{original_func.__name__}' 即将执行...")

# 2. 在包装函数内部,调用原始函数
original_func()

print(f"【日志】函数 '{original_func.__name__}' 执行完毕。")

# 3. 返回这个包装好的新函数
return wrapper

现在,最关键的一步来了。我们如何使用 logging_decorator 来“装饰” ordinary_function 呢?

Python

1
2
3
4
5
# 将我们的普通函数“喂”给装饰器,然后用返回的新函数,覆盖掉原来的函数名
ordinary_function = logging_decorator(ordinary_function)

# 现在,当你调用 ordinary_function 时,你实际上调用的是 wrapper 函数!
ordinary_function()

输出结果将会是:

1
2
3
【日志】函数 'ordinary_function' 即将执行...
正在执行核心业务...
【日志】函数 'ordinary_function' 执行完毕。

看到了吗?ordinary_function = logging_decorator(ordinary_function) 这行代码,就是装饰器模式的核心。它用一个功能更强的“包装版”函数,替换了原始的函数。

第 3 步:揭开 @ 的神秘面纱

Python 的设计者们发现,上面这种 func = decorator(func) 的写法非常常用,所以他们创造了一个更简洁、更具可读性的语法来做完全相同的事情,这个语法就是 @,我们称之为“语法糖(Syntactic Sugar)”。

所以,下面这两段代码是完全等价的

写法一:手动赋值

Python

1
2
3
4
def ordinary_function():
print("正在执行核心业务...")

ordinary_function = logging_decorator(ordinary_function)

写法二:使用 @ 语法糖

Python

1
2
3
@logging_decorator
def ordinary_function():
print("正在执行核心业务...")

当 Python 解释器读到 @logging_decorator 时,它会先定义 ordinary_function,然后自动地在内部执行 ordinary_function = logging_decorator(ordinary_function) 这个操作。

第 4 步:回到 @memoize

现在,我们带着全新的理解,回头看 memoize 的例子:

Python

1
2
3
4
5
6
7
8
9
10
11
12
13
def memoize(func):
cache = {}
def wrapper(*args):
if args not in cache:
cache[args] = func(*args) # 这里调用的是原始的 fib_decorator
return cache[args]
return wrapper

@memoize
def fib_decorator(n):
if n <= 1:
return n
return fib_decorator(n-1) + fib_decorator(n-2)

这段代码在 Python 解释器眼里的执行流程是:

  1. 解释器看到了 def fib_decorator...,于是它在内存中创建了一个斐波那契计算函数,我们称之为**原始 fib_decorator**。
  2. 接着,解释器看到了它头顶的 @memoize
  3. 它立刻执行 memoize(原始_fib_decorator)
  4. memoize 函数返回了 wrapper 函数。(这个 wrapper 函数内部通过闭包 func 记住了**原始 fib_decorator**,并且还创建了一个私有的 cache 字典)。
  5. 解释器用返回的 wrapper 函数,覆盖了 fib_decorator 这个名字。

所以,在这之后,当你的代码调用 fib_decorator(5) 时,你实际上调用的是 wrapper(5)。

wrapper 函数会:

  • 检查 (5,) 这个参数元组是否在它的 cache 里。
  • 如果不在,它就会调用它所“记住”的原始 fib_decorator 函数(func(5)),并将计算结果存入 cache
  • 最后,返回 cache 中的值。

有趣的是,wrapper 内部的递归调用 fib_decorator(n-1)fib_decorator(n-2),因为 fib_decorator 这个名字已经被替换了,所以它们调用的也是 wrapper 函数,而不是原始函数。这恰好就是我们想要的——确保每一次递归调用都能利用上缓存机制!

总结

@decorator 就是一个语法糖,它让你能够以一种声明式的方式,将一个函数(decorator)应用到另一个函数(被装饰的函数)上。

其背后的完整逻辑链是:

@decorator -> func = decorator(func) -> 函数作为一等公民(可以被传递和返回)

希望这个从底层原理到语法糖的逐层剖析,能让你彻底明白装饰器的运作方式!