接上次,但是回顾一下闭包,为什么呢,这个平时基本见不到,但是多次尝试,这个解法结合实例方法却是一次通过。
当然走类静态方法也可以,需要重新构造一下类。这也是我的第一版做法,把类相关的信息传过去,消费的时候冲洗构造实例。 同时还要维护一个实例缓存,避免每次都重新从meta参数转实例。
一、回顾一下闭包
1.数据隐藏与状态保持(轻量级的类)
如果你需要记录一些状态(比如计数),通常会写一个类。但如果逻辑很简单,用类有点"杀鸡用牛刀",闭包是更好的选择。
场景:统计代码运行了多少次。
code****Python
plain
def make_counter():
count = 0 # 这是被"记忆"的变量
def adder():
nonlocal count # 关键!告诉 Python 我们要修改外部变量,而不是定义新的
count += 1
return count
return adder
# 实例化一个计数器
c1 = make_counter()
print(c1()) # 输出 1
print(c1()) # 输出 2
print(c1()) # 输出 3
# 实例化另一个,互不干扰
c2 = make_counter()
print(c2()) # 输出 1
对比用类(Class)的写法:
code****Python
plain
class Counter:
def __init__(self):
self.count = 0
def __call__(self):
self.count += 1
return self.count
# 闭包写法比类写法更简洁,且不需要理解 self
2.延迟执行(Lazy Evaluation)
有时候我们定义了逻辑,但不想立刻执行,而是想把逻辑和参数打包好,等需要的时候再执行。
这在你的 spider.py 原始代码中体现得很明显:
code****Python
plain
# 原始代码逻辑简化版
def get_task(url):
print("准备任务...")
def task():
print(f"正在下载: {url}") # url 被打包在里面了
return task
# 1. 此时只生成了任务,没有下载
t = get_task("www.baidu.com")
print("干点别的...")
# 2. 需要的时候才执行
t()
3.闭包里的坑:nonlocal 关键字
在闭包中,读取 外部变量是没问题的(如例1),但如果要修改外部变量(如例2的计数器),必须使用 nonlocal。
错误示范:
code****Python
plain
def outer():
x = 10
def inner():
x = 20 # 这里的 x 被 Python 认为是 inner 内部定义的新局部变量,而不是修改外部的 x
print("内部:", x)
inner()
print("外部:", x)
outer()
# 输出:
# 内部: 20
# 外部: 10 <-- 外部的 x 并没有被改变!
正确示范:
code****Python
plain
def outer():
x = 10
def inner():
nonlocal x # 声明:我要操作的是外面那层函数的 x
x = 20
inner()
print("外部:", x)
outer()
# 输出:
# 外部: 20 <-- 成功修改
4.闭包的终极形态:装饰器(Decorator)
你经常看到的 @boost,@staticmethod,其实本质上就是闭包!
装饰器就是:接受一个函数,返回一个闭包。
让我们手写一个最简单的装饰器,用来计算函数运行时间:
code****Python
plain
import time
# 这是一个装饰器函数
def timer(func):
# func 是被装饰的函数(比如下面的 work)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs) # 执行真正的函数
end = time.time()
print(f"函数 {func.__name__} 耗时: {end - start} 秒")
return result
return wrapper # 返回闭包
# 使用装饰器
@timer
def work(n):
time.sleep(n)
return "完成"
# 等价于:work = timer(work)
print(work(1))
# 输出:
# 函数 work 耗时: 1.00xxxxx 秒
# 完成
二、举个栗子
plain
def _get_crawl_consumer_function(self):
# 外部环境:self (Engine实例) 就在这里
# self._worker 也是在这里通过 self 访问到的
def consumer_function(url, ...):
# 闭包内部:
# 虽然这函数由 funboost 在根本不知道 Engine 存在的角落里调用
# 但它依然"记得" self,所以能调用 self._worker.process_crawl
return self._worker.process_crawl(...)
return consumer_function
plain
这里的参数是怎么来的?
生成阶段:
当你调用 _get_crawl_consumer_function() 时,它不接收 url 这些参数。它只是生产并返回了 consumer_function 这个函数对象。这个函数对象"只要"被调用,就需要传入 url 等参数。
调用阶段(由框架触发):
funboost 框架在后台监听 Redis 消息队列。当 Redis 里来了一条消息(比如 {"url": "baidu.com", "meta": {}})时,框架内部会这样调用你生成的函数:
code
Python
# 假设 msg_dict 是从 Redis 取出的字典
# funboost 内部伪代码:
# 1. 拿到你之前返回的那个闭包函数
func = engine._get_crawl_consumer_function()
# 2. 这里的 func 就是 consumer_function
# 3. 框架把消息里的字段拆开传进去
func(url="baidu.com", callback_name="parse", ...)
好了 ,基本就能这么解决了
更多文章,敬请关注gzh: 零基础爬虫第一天~

next