关于我用gemini的一些心得体会
最近刚刚白嫖了Gemini的15个月学生会员,于是开始用这个号称目前top1的大模型,并且在使用过程中,有了一些自己的使用心得。
用Gemini进行文档总结
在碰到需要阅读的外文文档的时候,如果是公网资源,可以直接交给AI链接,然后让他总结并娓娓道来详细讲给你听。
这里我自认为是一个技巧,可以让他讲解的更易于理解。
这是21年CS61A的关于记忆化的一节课课件,我在听教授讲的时候听的很迷糊(当然没有说教授讲的不好,只是我没适应 ),于是直接把把链接给Gemini让他帮我总结:
1 | 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)
的调用过程:
fib_memo(5)
:缓存里没有,计算fib_memo(4) + fib_memo(3)
。fib_memo(4)
:缓存里没有,计算fib_memo(3) + fib_memo(2)
。fib_memo(3)
:缓存里没有,计算fib_memo(2) + fib_memo(1)
。fib_memo(2)
:缓存里没有,计算fib_memo(1) + fib_memo(0)
。fib_memo(1)
:返回 1,并存入缓存{1: 1}
。fib_memo(0)
:返回 0,并存入缓存{0: 0, 1: 1}
。fib_memo(2)
完成计算 (1+0=1),存入缓存{0:0, 1:1, 2:1}
。fib_memo(3)
需要fib_memo(1)
,直接从缓存读取,计算 (1+1=2),存入缓存{..., 3:2}
。fib_memo(4)
需要fib_memo(2)
,直接从缓存读取,计算 (2+1=3),存入缓存{..., 4:3}
。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
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)
。这种方式的好处是:
- 代码整洁:
fib_decorator
函数内部只关心核心的计算逻辑。- 高度复用:你可以把
@memoize
这个装饰器应用到任何其他需要记忆化的、符合条件的函数上。四、什么时候应该使用记忆化?
记忆化虽好,但并非万能。它在以下场景中最为有效:
- 函数是“纯”的(Pure Function):这是最重要的前提。纯函数意味着对于相同的输入,永远返回相同的输出,并且没有可观察的副作用(比如修改全局变量、打印日志等)。斐波那契函数就是典型的纯函数。
- 存在大量重叠子问题(Overlapping Subproblems):就像我们的斐波那契例子,递归过程中反复求解相同的子问题。如果一个问题可以被分解成子问题,但子问题之间毫无关联,那么记忆化就派不上用场。
- 计算成本较高:如果一个函数本身的计算非常快,那么为它增加缓存和查找的开销可能得不偿失。
总结
今天,我们通过斐波那契数列的例子,深入了解了记忆化这一强大的性能优化技术。
- 核心痛点:递归调用中存在大量的重复计算,导致指数级的时间复杂度。
- 解决方案:使用缓存(如字典)存储计算结果,避免重复劳动,实现“用空间换时间”。
- 最佳实践:将记忆化逻辑封装成一个通用的装饰器,以保持业务代码的纯粹和整洁。
记忆化是动态规划(Dynamic Programming)思想的重要组成部分,是每个程序员都应该掌握的基本功。希望通过这篇详细的讲解,你已经彻底理解了它的原理和应用。现在,去检查一下你的代码,看看有没有哪个“健忘”的函数正在等待你赋予它“记忆”吧!
读这种文章的感觉就和读大佬写的博客文一样酣畅淋漓!然后我又让他解释了一下其中装饰器的作用,还是这种模式的回复:
当然可以!很高兴你对这个话题有如此浓厚的兴趣。装饰器是 Python 中一个非常强大且优雅的特性,一旦你理解了它,就会发现它无处不在。
让我们一步一步,把“
@memoize
等同于fib_decorator = memoize(fib_decorator)
”这块硬骨头彻底啃下来。第 1 步:理解 Python 的核心前提——函数是“一等公民”
在 Python 中,函数(function)和我们常见的数字(
123
)、字符串("hello"
)、列表([1, 2, 3]
)一样,是一种对象。这意味着你可以像对待任何其他数据一样对待它。这具体体现在三个方面:
可以被赋值给一个变量
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!可以作为参数传递给另一个函数
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),因为它接受了另一个函数作为参数。可以作为另一个函数的返回值
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)
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 解释器眼里的执行流程是:
- 解释器看到了
def fib_decorator...
,于是它在内存中创建了一个斐波那契计算函数,我们称之为**原始fib_decorator
**。- 接着,解释器看到了它头顶的
@memoize
。- 它立刻执行
memoize(原始_fib_decorator)
。memoize
函数返回了wrapper
函数。(这个wrapper
函数内部通过闭包func
记住了**原始fib_decorator
**,并且还创建了一个私有的cache
字典)。- 解释器用返回的
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) -> 函数作为一等公民(可以被传递和返回)
希望这个从底层原理到语法糖的逐层剖析,能让你彻底明白装饰器的运作方式!