开篇先聊点实际的。我在教 Python 入门的朋友时,最常被问到的不是"什么是列表推导式",也不是"装饰器怎么用",反而是这个看起来不起眼的with语句。很多新手写了半年 Python,天天用with open(...) as f读文件,却从没想过这个with背后到底发生了什么。等到某天需要自己写一个支持with的对象,或者想在代码里管理数据库连接、线程锁、临时环境变量时,才发现脑子里对上下文管理器这个概念完全是一团浆糊。
说白了,上下文管理器(Context Manager)就是 Python 里用来处理"进入-退出"场景的一套协议。它解决的是资源管理的问题:文件要关、连接要断、锁要释放、临时状态要还原。以前我们靠try...finally硬扛,代码丑不说,还特别容易漏。有了上下文管理器,这些东西全都可以封装起来,用with一句搞定。这篇文章我不打算讲一堆虚的理论,而是从一个实际项目里踩过的坑出发,把上下文管理器的原理、实现方式、常见误区和进阶玩法全部捋一遍。无论你是刚学 Python 的新手,还是写了好几年代码想系统梳理一下的老手,这篇都值得你花十分钟看完。
1. 上下文管理器到底解决了什么问题
1.1 没有 with 的年代,代码长什么样
先来看一段很典型的"文件操作"代码。假设我要读取一个配置文件,解析里面的数据库连接参数,然后做后续处理。
f = None try: f = open("config.ini", "r") content = f.read() # 中间可能有一堆解析逻辑 config = parse_config(content) finally: if f: f.close()这段代码其实已经算写得规范的了。但问题在于:第一,parse_config如果抛异常,我虽然能保证文件被关闭,但异常信息会被finally里的逻辑干扰追踪;第二,代码里混入了大量"防御性"的关闭逻辑,真正的业务逻辑反而被挤到一边去了;第三,如果这个文件对象在多个地方需要关闭,重复代码就不可避免。
更麻烦的是锁和事务。你写一个多线程程序,需要给共享资源加锁:
lock.acquire() try: # 业务处理 pass finally: lock.release()事务也是这样,要么 commit,要么 rollback。这种模式写多了,你会发现自己总是在重复同一个套路:进入某种状态,做事情,最后无论如何都要恢复原状。上下文管理器就是把这个套路抽象出来的产物。
1.2 with 语句让代码回归本来面目
用上下文管理器重写上面的文件读取,长这样:
with open("config.ini", "r") as f: content = f.read() config = parse_config(content)注意看,少了try、少了finally、少了f.close()。这不是把关闭逻辑"隐藏"起来了,而是把"关闭"这件必然要做的事情,通过协议约定好,在合适的时机自动执行。业务逻辑成了代码的主体,资源管理的噪音被彻底隔离出去。
我个人的体会是,with最了不起的地方不是"省代码",而是改变了代码的阅读方式。你读with open(...) as f:这一行,脑子里自动就会理解:这个文件的生命周期到此结束,后面的代码和它无关了。这种"作用域感"是try...finally给不了的。
1.3 资源管理只是冰山一角
很多人以为上下文管理器只能管"资源"——文件、网络连接、数据库会话。其实它的用途广得多。举几个我自己在项目中用过的例子:
临时修改系统状态。有一次做图像处理,需要临时把 numpy 的打印选项改成不回行显示,处理完再还原。用上下文管理器,进入时设置np.set_printoptions(linewidth=200),退出时恢复原值,完美。
临时切换工作目录。脚本里要调用一个外部工具,这个工具只在特定目录下才能正常运行。用os.chdir切过去,跑完再切回来。如果中间抛了异常,目录就回不去了,后续逻辑全崩。用上下文管理器包一层,健壮得多。
忽略特定类型的异常。在某些场景下,比如清理临时文件、发送心跳包,失败其实无所谓。你不希望异常中断主流程,又不想用裸的except: pass(因为那会连KeyboardInterrupt也吞掉)。contextlib.suppress就是干这个的。
这些场景的共同点只有一个:在进入和退出之间,发生了一些必须保证成对出现的操作。识别出这个模式,就是学会使用上下文管理器的第一步。
2. 核心机制拆解:enter和exit是怎么配合的
2.1 协议的两端:进入与退出
Python 的上下文管理器协议由两个魔法方法组成:__enter__和__exit__。只要一个对象实现了这两个方法,它就可以被with语句使用。
具体执行流程是这样的:
- Python 先计算
with后面的表达式,得到上下文管理器对象。 - 调用对象的
__enter__()方法。如果with语句里有as xxx,__enter__()的返回值会被赋给xxx。 - 执行
with代码块里的语句。 - 无论代码块是正常执行完,还是抛出了异常,都会调用
__exit__(exc_type, exc_val, exc_tb)方法。
这里有个容易搞混的地方:__enter__的返回值不一定非得是对象自己。比如open()返回的就是一个全新的文件对象,而不是它内部的某个"管理器"。你在as f里拿到的,是__enter__返回什么就是什么。
2.2exit的三个参数和返回值之谜
__exit__方法接收三个参数:
exc_type:异常的类型(如果是正常退出,就是None)exc_val:异常实例exc_tb:异常的 traceback 对象
这三个参数让你能在退出时判断:刚刚执行的过程中到底有没有出问题?出了什么问题?
返回值则是一个布尔值,它的含义有点反直觉:返回True表示异常已经被处理掉了,Python 不会继续向上抛;返回False或者返回None(也就是啥也不返回),异常会继续传播。
这里有个绝大多数教程不会细讲的坑:如果你在__exit__里写了return True,那么原本的异常会被静默吞掉。这在某些场景下是好事(比如contextlib.suppress就是靠这个实现的),但如果你是无意间返回了True,你会发现自己调试了半天都找不到异常到底去哪了。我的习惯是:除非明确要吞异常,否则不要在__exit__里写return True,连return False都别写,直接让它自然结束就好。
2.3 执行顺序的完整示例
用一段代码来验证上面的执行顺序:
class DemoContext: def __enter__(self): print("1. 进入上下文") return "我是enter的返回值" def __exit__(self, exc_type, exc_val, exc_tb): print("2. 退出上下文") if exc_type: print(f" 检测到异常: {exc_type.__name__}: {exc_val}") return False with DemoContext() as value: print(f"3. 代码块内部, value = {value}") # 取消下面的注释看看异常传播 # raise ValueError("故意出错")正常执行的输出:
1. 进入上下文 3. 代码块内部, value = 我是enter的返回值 2. 退出上下文如果把raise ValueError那行取消注释,输出会是:
1. 进入上下文 3. 代码块内部, value = 我是enter的返回值 2. 退出上下文 检测到异常: ValueError: 故意出错 Traceback (most recent call last): ... ValueError: 故意出错注意__exit__里的打印依然执行了,而且traceback是在__exit__返回之后才打印的。这说明__exit__有机会在异常传播出去之前做拦截和处理。这是理解上下文管理器异常处理机制的关键。
2.4 一个小实验:try-finally 与 with 的等价关系
其实从语法层面看,with代码块的异常处理流程和你手动写try...finally是差不多的。区别在于,with把繁琐的"获取资源-释放资源"的模板代码给封装了。你可以把下面这两段代码在脑子里等价起来:
# 写法一:with with ctx() as x: do_something(x)# 写法二:手动 try-finally(伪代码) _manager = ctx() try: x = _manager.__enter__() do_something(x) finally: _manager.__exit__(*sys.exc_info())注意一个细节:__exit__接收的三个参数,来源是sys.exc_info()。这意味着即使在with代码块内部没有异常,__exit__依然会被调用,只是三个参数全是None。这是它和finally最大的不同——finally里你没法轻易知道是否有异常(除非你再用一层try),但__exit__天生就带着异常信息来的。
3. 实操:手写一个上下文管理器
3.1 最简单的类实现:MySQL 连接的自动提交与回滚
理论看再多,不写代码都是空中楼阁。我挑一个项目里真实遇到的场景来演示:写一个 MySQL 事务上下文管理器。
需求是这样的:每次操作数据库时,我希望代码块正常就commit,抛异常就rollback,并且不管怎样最终都要关闭连接、归还连接池。
import pymysql class MySQLTransaction: def __init__(self, conn): self.conn = conn def __enter__(self): # 进入时开启事务 self.conn.begin() return self.conn # 让代码块内部可以直接使用连接 def __exit__(self, exc_type, exc_val, exc_tb): try: if exc_type is None: # 没有异常,提交事务 self.conn.commit() else: # 有异常,回滚事务 self.conn.rollback() finally: # 不管提交还是回滚,最后都关闭连接 self.conn.close() # 返回 False 表示异常继续向上抛 return False使用时的效果:
conn = pymysql.connect(...) try: with MySQLTransaction(conn) as cursor: cursor.execute("UPDATE users SET balance = balance - 100 WHERE id = 1") cursor.execute("UPDATE users SET balance = balance + 100 WHERE id = 2") # 这两条语句要么都成功,要么都失败 except Exception: log.error("转账失败,事务已回滚")这个例子里有几个值得注意的细节:
__enter__里调用了begin()。这是为了让"开启事务"的逻辑也有一个明确的入口,而不是在with外面手动做。__exit__的try...finally确保close()一定会执行,即便commit()或rollback()本身抛了异常。- 最后
return False是必要的,因为事务失败通常意味着上层需要知道这个失败,不能让异常被静默吞掉。
写到这里顺便说一句:现在主流的 ORM(比如 SQLAlchemy)和连接池库早就把这种逻辑内置了,你不需要自己实现事务管理器。但自己做一遍能帮助你理解那层封装底下到底是什么。
3.2 进阶实现:支持异常类型筛选的上下文管理器
再上一个更灵活的版本。有时候我只想捕获特定类型的异常,比如网络超时,不想管业务逻辑本身出什么错。可以让上下文管理器接受一个"允许吞掉的异常类型"参数:
class IgnoreError: def __init__(self, *ignored_exceptions): self.ignored_exceptions = ignored_exceptions or () def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is None: return False if issubclass(exc_type, self.ignored_exceptions): print(f"已忽略异常: {exc_type.__name__}") return True return False用法:
with IgnoreError(TimeoutError, ConnectionError): # 这里面的网络请求失败了也无所谓 result = fetch_remote_data()注意我用了issubclass而不是exc_type in来比较,因为异常类型有继承关系。比如ConnectionResetError是ConnectionError的子类,如果你希望把相关的网络错误全吞掉,用issubclass更精确。
3.3 偷懒神器:@contextmanager 装饰器
实现__enter__和__exit__的类是正统做法,但很多时候写类太"重"了。Python 的contextlib模块提供了一个装饰器@contextmanager,它让你只需写一个生成器函数,就可以得到一个上下文管理器。
原理其实很巧妙:它是用生成器的yield把代码块"一切为二"。yield之前的部分是__enter__要执行的逻辑,yield之后的部分是__exit__要执行的逻辑。
from contextlib import contextmanager @contextmanager def mysql_transaction(conn): conn.begin() try: yield conn # 这行代码执行时,with 代码块正在运行 except: conn.rollback() raise # 重新抛出异常,保持语义 else: conn.commit() finally: conn.close()用起来和类版本一模一样:
with mysql_transaction(conn) as cursor: cursor.execute(...)这里的重点是理解yield的位置。with代码块里的所有内容,本质上都是在"暂停"在yield这一行执行的。代码块执行完(或抛出异常)后,生成器会从yield后面恢复执行。
几个必须注意的细节:
try...finally不能少。因为yield之后如果直接写清理逻辑(比如conn.close()),但with代码块里抛了异常,这个清理逻辑是能执行的(因为生成器内部抛的异常会停在yield处),但你没法区分"正常退出"和"异常退出"。所以要拿到异常信息,就得在生成器内部用try去捕获。- 如果
__exit__需要吞掉某些异常,你必须在生成器里except之后不重新抛出,而是正常返回。但这样会把异常吞掉,有时这不是你想要的效果。 yield返回的值就是with ... as后面拿到的值。如果你yield一个值,那代码块里就能直接用它;如果你只是想把首尾逻辑包起来,yield None也行,或者干脆yield不带值。
3.4 函数式实现的取舍
用类实现还是用@contextmanager实现,我个人的经验是:
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 需要被多个地方复用、逻辑复杂 | 类 | 状态清晰,便于继承和组合 |
| 只需要包一层"首尾"逻辑 | @contextmanager | 代码少,直观 |
| 需要精细控制异常返回值 | 类 | @contextmanager 的异常处理绕来绕去,容易懵 |
| 临时在函数内部用一下 | @contextmanager | 不用单独定义一个类 |
其实多写几次你就会发现,@contextmanager用起来虽然快,但它在"异常处理"上比类难把握。比如你想在__exit__里根据异常类型决定返回True还是False,在生成器版本里,你需要搞清楚:如果我不在except里raise,是不是就等于吞掉了异常?答案是肯定的。这往往会带来意料之外的静默失败。我的建议是:如果逻辑里涉及复杂的异常分支,优先用类实现,逻辑更直白、调试更顺手。
4. 标准库与内置工具:你不知道的 contextlib 宝藏
4.1 closing:告别手写 close
很多库的类并不实现上下文管理器协议,但它们有close()方法。closing就是为这种情况准备的:
from contextlib import closing import urllib.request with closing(urllib.request.urlopen("https://example.com")) as response: html = response.read() # response.close() 会在退出时自动调用说白了就是把你手写try...finally: thing.close()的活给干了。适合用在你自己写的、遗留的旧类上。
4.2 suppress:比 try-except-pass 更优雅的异常忽略
从 Python 3.4 开始,标准库引入了suppress。它是专门用来"忽略指定异常"的:
import os from contextlib import suppress # 删除文件,如果文件不存在就忽略错误 with suppress(FileNotFoundError): os.remove("temp.txt")对比传统写法:
try: os.remove("temp.txt") except FileNotFoundError: passsuppress写起来更紧凑,而且它的语义非常明确:我知道这个操作可能失败,但失败无所谓。注意,suppress可以接受多个异常类型,内部通过__exit__返回True来吞掉异常。所以如果你不确定某个异常要不要忽略,就别用suppress,宁可让它抛出来。
4.3 ExitStack:动态管理多个上下文管理器
ExitStack是我认为 contextlib 里最被低估的工具。它允许你在运行时动态地、一个接一个地压入多个上下文管理器,退出时按后进先出的顺序全部退出。
场景是这样的:一个函数需要打开多个文件、多个网络连接,但数量和顺序取决于配置。手写with嵌套会让代码变成"金字塔",而且没法在循环里动态加资源。
from contextlib import ExitStack def process_files(filenames): with ExitStack() as stack: files = [] for filename in filenames: f = open(filename, "r") stack.enter_context(f) # 关键:动态注册上下文管理器 files.append(f) # 所有文件都打开后,统一处理 for f in files: data = f.read() # 处理数据... # 退出 with 时,所有文件按逆序自动关闭ExitStack的能力不止于此。你还可以通过stack.callback(...)注册任意的退出回调函数,或者用stack.push(...)压入一个自定义的上下文管理器。它就像是一个"资源栈",你往里添加多少东西,退出时就帮你清理多少东西。写测试代码时尤其好用:很多monkeypatch的库底层就是靠它管理的。
4.4 redirect_stdout 和 redirect_stderr:临时改道输出流
调试库函数时,经常遇到库内部往stdout狂打印东西的情况。不想让终端被刷屏,又不想改库源码,用redirect_stdout:
import io from contextlib import redirect_stdout buf = io.StringIO() with redirect_stdout(buf): # 这里面所有的 print 都会写进 buf,而不是终端 print("secret output") noisy_library_function() # 事后可以检查 buf.getvalue()它的原理是在__enter__里把sys.stdout替换成一个临时的流对象,在__exit__里再还原。同理还有redirect_stderr。这个工具看似简单,但调试线上问题时救过我很多次。
4.5 组合拳:一次性搞定多个上下文
有了ExitStack,你可以把多个with嵌套给拍扁了。而 Python 3.10+ 还支持在with语句里直接并列多个上下文管理器:
# Python 3.10+ with open("a.txt") as fa, open("b.txt") as fb, open("c.txt") as fc: data_a = fa.read() data_b = fb.read() data_c = fc.read()这样写的好处是代码更扁平,可读性更好。但注意,如果列表长度是动态的,或者你想在循环里不断添加资源,ExitStack依然是唯一的选择。
5. 实战案例:把计时功能做成上下文管理器
5.1 需求描述和设计思路
在我维护的一个数据处理脚本里,经常需要监控某段代码的执行耗时。传统的做法是:
start = time.time() # 业务逻辑 elapsed = time.time() - start print(f"耗时: {elapsed:.4f}秒")但这样的代码有个坏处:业务逻辑里掺杂了计时噪音。如果某天想临时把某几段代码的计时代码拆掉,得小心翼翼地删除很多行。用上下文管理器,计时逻辑就可以优雅地"贴在"代码块外层。
5.2 第一版:简单计时器
import time from contextlib import contextmanager @contextmanager def timer(name="任务"): start = time.perf_counter() # 用 perf_counter 而不是 time.time() try: yield finally: elapsed = time.perf_counter() - start print(f"[{name}] 耗时 {elapsed:.4f} 秒")注意我这里用的是perf_counter而不是time.time()。原因是time.time()是墙钟时间,可能受系统时钟调整的影响;perf_counter专门用于测量短时间间隔,精度更高且不受系统时间回拨影响。这是写计时器的一个小常识,很多人不知道。
用法:
with timer("数据清洗"): df = pd.read_csv("raw.csv") df = df.dropna() df.to_parquet("clean.parquet")输出:
[数据清洗] 耗时 3.2145 秒5.3 第二版:支持记录多次计时结果的累加器
有时候代码块会执行多次,比如放在循环里,我想累计总耗时,而不是每次都打印一条。这需要让上下文管理器能接收外部状态:
class AccumulateTimer: def __init__(self, total_time_list): # 外部传入一个列表,这样上下文管理器可以把累计值写进去 self.total_time_list = total_time_list self._start = None def __enter__(self): self._start = time.perf_counter() return self def __exit__(self, exc_type, exc_val, exc_tb): elapsed = time.perf_counter() - self._start self.total_time_list[0] += elapsed return False # 使用 total = [0.0] for i in range(10): with AccumulateTimer(total): # 模拟耗时操作 time.sleep(0.1) print(f"总计: {total[0]:.2f}秒")为什么用列表传引用而不是直接传一个数字呢?因为 Python 的整数是不可变类型,在函数外部修改一个变量的值,不会反映到外部作用域。列表是可变对象,通过索引修改其中的值,外部能看到变化。当然,更优雅的方案是定义一个小的数据类(dataclass),但列表是更轻量的做法。
5.4 第三版:统计执行次数和异常情况
实际项目中,我经常需要同时统计"执行了几次""失败了几次""总耗时多少"。干脆做一个功能完整的统计上下文管理器:
from dataclasses import dataclass @dataclass class CodeBlockStats: """统计一个代码块的执行情况""" count: int = 0 ok_count: int = 0 fail_count: int = 0 total_time: float = 0.0 last_exception: Exception | None = None class BlockMonitor: def __init__(self, stats): self.stats = stats def __enter__(self): self._start = time.perf_counter() self.stats.count += 1 def __exit__(self, exc_type, exc_val, exc_tb): elapsed = time.perf_counter() - self._start self.stats.total_time += elapsed if exc_type is None: self.stats.ok_count += 1 else: self.stats.fail_count += 1 self.stats.last_exception = exc_val return False # 不要吞异常 def monitored_block(stats: CodeBlockStats): return BlockMonitor(stats)用起来:
stats = CodeBlockStats() for url in urls: with monitored_block(stats): fetch_and_process(url) print(f"总次数: {stats.count}, 成功: {stats.ok_count}, 失败: {stats.fail_count}, 总耗时: {stats.total_time:.2f}s")这个模式在我的数据采集任务里用过很多次。通过一个stats对象把监控数据带出来,既不干扰主逻辑,又能掌握全局的执行情况,比到处打日志强多了。
6. 常见问题和排查技巧实录
6.1exit里 return True 导致异常丢失
这是我见过最多的坑。有个同事写了一个清理资源的上下文管理器,为了图省事,__exit__直接return True,结果业务代码里抛出的ValueError全部被吞掉了,排查了半天才发现是这个原因。
排查方法:在__exit__里加一行print(exc_type),看看传入的异常类型是否为空。如果业务代码明明报了错,但exc_type是None,那说明异常在进入__exit__之前就被吞了。
正确做法:除非明确要实现suppress的效果,否则永远return False或直接落空。如果需要在__exit__里做清理但不想处理异常,写return False。
6.2 with 代码块里的 yield 和生成器冲突
@contextmanager装饰的函数本身是一个生成器函数。如果你在with代码块里又用了yield(也就是你在用上下文管理器包着一个生成器),你可能会遇到StopIteration或行为怪异的情况。原因在于嵌套的生成器不会像你想象的那样"暂停"上下文管理器。
场景:
@contextmanager def my_ctx(): try: yield finally: print("cleanup") def gen(): with my_ctx(): yield 1 # 这里的 yield 会导致上下文管理器退出时机的变化解决方案:不要在with代码块里直接yield,把with放进内层函数,或者用yield from把内部生成器委托出去。
6.3 上下文管理器的线程安全问题
如果你的上下文管理器是在多线程环境下使用的,要注意__enter__和__exit__中间的逻辑可能被多个线程同时执行。比如我在写一个连接池组件时,最初用了模块级的上下文管理器来管理连接,结果多个线程同时进入时,连接被抢来抢去,最后乱成一团。
经验教训:上下文管理器本身并不保证线程安全。如果你要在多线程中使用,有两种思路:
- 每个线程创建自己的上下文管理器实例(推荐,互不干扰)。
- 在
__enter__里加锁,保证同一时间只有一个线程进入代码块。
6.4 with 块内嵌套 with:缩进地狱怎么破
嵌套三层以上的with会让代码缩进越来越深。除了已经在 4.5 里说过的并列写法,还有一个更实用的技巧:用ExitStack把多个上下文管理器合并成一个。这样做的好处是代码层级不变,而且可以动态调整。
with ExitStack() as stack: f1 = stack.enter_context(open("a.txt")) f2 = stack.enter_context(open("b.txt")) g1 = stack.enter_context(create_generator()) # 所有资源都准备好后统一处理6.5 常见问题速查表
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 异常消失了 | __exit__返回了True | 检查返回值,改为False |
| 资源没有释放 | 没实现__exit__,或__exit__里没写清理逻辑 | 用with的类必须实现__exit__ |
with代码块里的异常没法捕获 | __exit__里的清理逻辑又抛了新的异常 | 清理逻辑用try...except保护 |
@contextmanager装饰器用不了 | 函数里没有yield关键字 | @contextmanager必须装饰生成器函数 |
with作用域里定义的变量外面用不了 | Python 没有块级作用域,这其实是错觉 | 变量在with外面确实能访问,但如果想在外部使用,要在with之前先定义 |
6.6 性能考虑:上下文管理器的开销有多大
有人担心用with会有额外的性能开销。实测下来,单纯进入和退出一个上下文管理器,耗时大约是亚微秒级别(百万次循环大约零点几秒),跟业务逻辑相比完全可以忽略。真正需要注意的是不要在__enter__和__exit__里做重活,比如__exit__里写复杂的日志格式化、开网络请求等,这会让上下文管理器的"清爽"优势荡然无存。
7. 写在最后的实操心得
上下文管理器是我在 Python 里用得最顺手、也是最先推荐给团队同事的特性之一。它的学习曲线不算陡峭,但要从"会用"到"会写",再到"懂得在什么场景用它替代手写逻辑",需要一段时间的积累。
我自己用下来,最大的感受是:上下文管理器不仅是一种语法机制,更是一种思维模式。当你开始用"进入-退出"的视角审视代码时,会发现很多逻辑都可以用这个模式重写——配置文件的临时加载、环境变量的暂时修改、缓存的预取与失效、数据库事务和连接的管理、甚至 A/B 测试中临时切换特性开关。认清这个模式,代码的模块化程度会明显提升。
如果你刚接触这个主题,我的建议是先做三件事:第一,把自己代码里所有手写的try...finally清理逻辑找出来,看能不能用内置的上下文管理器替换;第二,挑一个重复出现的资源管理场景,用类实现一个属于自己的上下文管理器;第三,写一个基于@contextmanager的计时器,用在你日常的函数里。三步走完,你对上下文管理器的理解会比看十篇教程都深。
最后再提一个细节:Python 3.10 之后,with语句里可以不加括号地并列多个上下文管理器,但为了代码风格统一,我一般还是会用反斜杠或者括号显式地分行。小习惯养成好,大项目才能少踩坑。
这个特性看似简单,背后的设计思想却很值得玩味。希望这篇文章能帮你把with真正变成自己的工具,而不是停留在"照抄别人代码"的阶段。