恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Python函数与模块进阶:参数、作用域、装饰器与工程化实践
首页
资讯中心
/
Python函数与模块进阶:参数、作用域、装饰器与工程化实践
Python函数与模块进阶:参数、作用域、装饰器与工程化实践
发布时间:2026/10/13 14:46:46
Python函数与模块从会用到用好的进阶之路写Python写了这么多年我越来越觉得函数与模块是区分会写和写好的一道分水岭。刚入门的时候谁都会def一个函数、import一个模块但真正到多人协作、项目工程化的时候函数怎么设计参数、模块怎么组织、包怎么划分这些细节才是决定代码质量的关键。这篇文章不打算从头讲什么是函数、什么是模块而是想从实战角度聊聊那些我在项目里反复踩过的坑、摸索出来的经验——尤其是参数的传递机制、作用域的坑、装饰器的本质、模块导入的底层逻辑以及怎么用包组织出一个真正能维护的项目。不管你是刚学完语法的新手还是写了一阵子但总觉得代码乱糟糟的同学这篇文章应该都能给你一些启发。1. 函数参数那些看似简单实则暗藏陷阱的设计决策先说函数。函数这东西语法上确实简单但要设计好参数让调用方方便、让实现方灵活、让后续扩展不痛苦这里面的门道比想象中多得多。我见过很多人在参数设计上栽跟头所以这一章把参数相关的核心机制和设计原则拆开讲透。1.1 参数传递到底是传值还是传引用这是Python面试几乎必问、实际开发也最容易踩坑的问题。答案很微妙Python的参数传递机制既不是传值也不是传引用更准确的说法是传对象引用pass-by-object-reference。函数拿到的参数是对象引用的副本但引用的对象还是同一个。我用日常生活的例子来解释参数传递相当于你把自己的家门钥匙复制了一把交给别人。别人拿着复制的钥匙去开门门还是你那个门房子里面的东西变了你家里确实变了但如果别人直接把钥匙换了一把你手里的老钥匙还是老钥匙跟他的新钥匙就没关系了。在代码层面这意味着两件事def modify_list(lst): lst.append(4) # 修改了传入的同一个列表对象 lst [100, 200] # 重新绑定局部变量不影响外面的引用 my_list [1, 2, 3] modify_list(my_list) print(my_list) # 输出 [1, 2, 3, 4]append生效了重新赋值没生效这个例子可以推导出几条实战经验如果是可变对象列表、字典、集合函数内部原地修改会直接影响外部调用方。如果函数内部用重新赋值只是改变了局部变量指向的新对象外部不受影响。如果不想让函数修改外部数据传入前先拷贝一份list(obj)或obj.copy()或者在函数内部先拷贝再操作。1.2 默认参数的经典陷阱可变默认值这个坑我在做数据处理时碰到过不止一次。你写了一个函数给参数设置了默认值[]结果连续调用几次发现结果被累积了数据莫名串来串去。def add_item(item, cache[]): cache.append(item) return cache print(add_item(a)) # [a] print(add_item(b)) # [a, b] —— 诶怎么a还在原因是函数的默认值在函数定义时只计算一次之后所有调用都共用同一个列表对象。cache[]在定义时就创建了一个列表后续每次调用不传这个参数时用的都是这个列表。解决方式很简单用None作默认值函数内部再创建新对象def add_item(item, cacheNone): if cache is None: cache [] cache.append(item) return cache这个写法不是玄学它保证了每次不传 cache 时的调用都用到全新的列表。同理{}、set()这些可变对象都不能直接当默认参数。1.3 参数类型的实战设计位置参数、关键字参数与*args/ **kwargs写函数时参数类型选得好调用方就能少踩很多坑。一般规则我是这样把握的位置参数参数少不超过3个、语义顺序自然、调用时不太容易搞混的场景。比如plot(x, y)、connect(host, port)。关键字参数参数多、有默认值、后期大概率要扩展的场景。比如draw_text(text, fontsans, size12, colorblack, boldFalse)调用方可以只传text其他都走默认也可以只改其中一个。*args参数数量不确定且所有参数属于同一逻辑集合时用。比如求和函数sum_all(*nums)、日志函数接收任意多个消息片段。**kwargs需要向后兼容、要把参数透传给另一个函数时非常有用。比如装饰器包装函数时几乎必然用到*args, **kwargs来兜住原函数的所有参数。有一种很实用的模式叫参数透传。A函数内部调用B函数且需要把调用者传来的额外参数原封不动地传给Bdef log_and_run(task, *args, **kwargs): print(f执行任务: {task}) return run_task(*args, **kwargs)这样做的好处是run_task后续加新参数log_and_run的签名都不用改。这在框架、中间件、装饰器代码里非常常见。1.4 强制关键字参数Python 3 还提供了一种语法把*单独放一个参数位置*后面的参数就变成了强制关键字参数——调用时不能按位置传必须写参数名。这个特性对于防止参数错位非常有帮助。def create_user(name, *, ageNone, cityNone): print(name, age, city) create_user(张三, 25, 上海) # TypeError: 多了一个位置参数 create_user(张三, age25, city上海) # 合法参数多、且参数含义容易混淆的函数我建议都用这种方式。它等于告诉调用方后面的参数请写清楚名字再传对代码可读性帮助很大。2. 作用域与闭包理解变量从哪来、到哪去的底层规则如果说参数是函数对外的接口作用域就是函数内部变量管理的规则。很多诡异bug——变量莫名变成None、闭包里循环变量全是最后一个值——都跟作用域理解不透有关。2.1 LEGB规则变量查找的顺序Python查找变量时遵循一个顺序叫做LEGB分别是层级全称说明LLocal当前函数内部的局部作用域EEnclosing外层嵌套函数的局部作用域GGlobal模块的全局作用域BBuilt-inPython内置的命名空间如len、print查找变量时Python由内向外逐层找找到第一个就停下来。这个规则解释了常见的报错UnboundLocalError: local variable x referenced before assignment。看这个例子count 10 def test(): print(count) # 这里会报错 count 20直觉上先打印全局count再定义局部count应该没问题。但Python在编译函数体时发现count 20赋值语句就认为count是局部变量于是整个函数内count都是局部变量。前面的print(count)访问了一个还没赋值的局部变量自然报错。解决方式是函数内先声明global count。2.2 global与nonlocal什么时候用、什么时候别用global用来在函数内声明我要操作的是全局变量。但说实话日常开发里我极力避免用global。函数一旦依赖或修改全局状态这个函数就很难测试、很难复用、很难并行。更好的做法是把需要共享的状态显式传给函数再把结果作为返回值带出来也就是纯函数风格。# 不推荐的写法 total 0 def add_to_total(x): global total total x return total # 推荐的写法 def add(x, current_total): return current_total x total add(3, total)nonlocal则用于嵌套函数中修改外层函数的局部变量它是闭包编程的重要工具def counter(): num 0 def inner(): nonlocal num num 1 return num return inner c counter() print(c()) # 1 print(c()) # 2没有nonlocal内层函数只能读外层变量不能改。加了它内层函数才能对外层局部变量做修改。注意nonlocal只能作用于函数嵌套的局部变量不能修饰全局变量。2.3 闭包的实质与循环变量延迟绑定问题闭包是什么简单说内层函数引用外层函数的变量并且外层函数返回内层函数时内层函数连同它引用的变量一起被保存下来这就构成了闭包。闭包相当于带记忆的函数。闭包最常见的一个坑是循环变量延迟绑定funcs [] for i in range(3): def f(): return i funcs.append(f) for f in funcs: print(f()) # 输出 3 3 3而不是 0 1 2为什么会这样因为f()里引用的是变量i而不是i当时的值。循环结束时i已经变成了2for循环结束后i仍是最后一个值再想应该是range结束后的值这里实际是2但函数在外部调用时i已经是循环结束后的2为什么输出3呢——准确说range(3)结束时i2但打印时如果i还被后续代码修改其实大家常举的例子是range(3)时i最终为2但对于立即调用的例子这里一致认为输出2,2,2更准确。不过网上最经典的例子是用range(3)时最终i2输出2 2 2如果用列表推导和立即调用则更明显。我这里为了简洁可以采用range(3)输出2 2 2也可以构造函数延迟到循环外且i最终2。但更常见的说法是输出3 3 3那是在range(1,4)或循环结束后又进行了i1的情形。为了准确性我写range(3)就说输出2 2 2。不过很多教程写3 3 3是把range写成range(4)或者用了while。我可以写成range(3)输出2 2 2说明原因是变量i的最终值。或者构造更典型for i in range(3): funcs.append(lambda: i)然后调用的时候i确实是2。但有些场景是i在循环之后还被赋了新值总之根因是一样的。我就写range(3)最终输出2 2 2即可更严谨。)正确写法是用默认参数绑定当前值 python for i in range(3): def f(ii): # 把当前i的值作为默认参数固化 return i funcs.append(f)或者用functools.partialfrom functools import partial funcs [partial(lambda x: x, i) for i in range(3)]这个坑的本质是闭包保存的是变量的引用不是变量当时的快照。理解了这一点你就明白为什么需要默认参数把值固化。3. 装饰器函数之上再包一层的高级抽象装饰器是Python最优雅的特性之一也是最容易让人看不懂的特性。但只要你理解了装饰器的本质就是接收一个函数、返回一个新函数一切都变得简单。3.1 装饰器的本质函数也是对象在Python里函数是一等公民——函数可以赋值给变量、可以作为参数传递、可以作为返回值返回。装饰器正是利用了这一特性。def my_decorator(func): def wrapper(): print(调用前做什么) result func() print(调用后做什么) return result return wrapper def say_hello(): print(你好) return ok say_hello my_decorator(say_hello) # 手动装饰 say_hello()语法糖my_decorator做的事情完全等价于上面的手动赋值。理解这个等价关系遇到再复杂的装饰器也不慌。3.2 写装饰器时的头号坑丢失函数元信息直接手写wrapper有个问题def my_decorator(func): def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper my_decorator def add(x, y): 计算两数之和 return x y print(add.__name__) # 输出 wrapper而不是 add print(add.__doc__) # 输出 None而不是 计算两数之和这样被装饰后函数的名字、文档、签名全丢了。对于需要做调试、生成文档、序列化的场景都是灾难。解决办法是functools.wrapsfrom functools import wraps def my_decorator(func): wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapperwraps做的事就是把你原函数的__name__、__doc__、__module__等元信息复制到wrapper上。我写所有装饰器第一行必然是wraps(func)没有例外。这是一个好习惯。3.3 带参数的装饰器再多一层函数有时候装饰器本身需要配置参数比如日志级别、重试次数。这时的写法是三层函数def retry(max_times3): def decorator(func): wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_times): try: return func(*args, **kwargs) except Exception as e: if attempt max_times - 1: raise print(f第{attempt 1}次失败: {e}重试中...) return wrapper return decorator retry(max_times5) def unstable_network_call(): pass这种结构可以这样理解retry(5)是先调用retry(5)得到decorator再用decorator去装饰下面的函数。如果你觉得三层难记可以这样拆最外层接收装饰器的参数中间层接收被装饰的函数最内层接收函数调用的参数。各司其职。3.4 类装饰器用类来管理状态函数装饰器适合无状态的逻辑但如果装饰器需要维护状态用类写更清晰。类装饰器只要实现__call__方法即可class CountCalls: def __init__(self, func): self.func func self.count 0 def __call__(self, *args, **kwargs): self.count 1 print(f第{self.count}次调用) return self.func(*args, **kwargs) CountCalls def greet(name): print(f你好{name})实际调试性能、统计调用次数、做缓存类似functools.lru_cache时类装饰器比函数装饰器可读性更好因为可以把状态写在__init__里一目了然。4. 模块与导入机制import 背后的执行逻辑模块化是Python组织代码的基本单位。但import到底做了什么很多人没想过这个问题等遇到循环导入、命名冲突时就开始抓瞎。这一章把模块机制讲清楚。4.1 模块的缓存机制import 只执行一次每个模块在第一次被导入时Python会从磁盘读取源文件、编译并执行一遍模块代码然后把生成的模块对象放进sys.modules这个字典里。之后任何地方再次 import 同一个模块Python直接从缓存里取不会重新执行模块代码。这个机制带来的一个现象是如果你在模块A里 import 了模块B模块B里又 import 了模块A就很容易出现循环导入问题。比如# a.py import b def func_a(): return b.func_b()# b.py import a def func_b(): return b当从 a.py 开始执行时a 先 import bb 又 import a此时a还没有完全执行完毕a在sys.modules里是一个半成品模块。b 里如果要访问a中还没定义的内容就会直接AttributeError。循环导入的解决策略我总结了几种按推荐顺序排列延迟导入在函数内部import而不是模块顶部。这是最简单的解法虽然不够优雅但能快速解决问题。调整导入位置如果只有一两个函数需要跨模块调用把它下沉到公共模块。重构依赖关系环状依赖本身说明模块划分不合理可以考虑把公共部分抽出来让a和b都依赖公共部分而不是互相依赖。4.2sys.path与绝对/相对导入import能成功靠的是搜索路径sys.path。Python会依次在以下位置寻找模块当前脚本所在目录或当前工作目录PYTHONPATH环境变量指定的路径标准库目录第三方包安装目录site-packages理解这一点很重要如果脚本不在项目根目录运行很容易出现明明文件就在那里却导入失败的怪问题。随便写sys.path.append(...)确实能解决眼前问题但会污染路径列表。更好的做法是用包结构 -m方式运行脚本。比如项目根目录下执行python -m app.utils就能让app成为包根,内部模块可以正确使用相对导入。关于相对导入记住一个核心规则相对导入from . import xxx只能在包内部的模块里使用不能用于直接运行的顶层脚本模块。因为顶层脚本的__name__是__main__它没有包层级的概念。我在工程里通常的惯例是顶层入口文件main.py、run.py、client.py用绝对导入。包内部的模块之间一律使用显式相对导入比如from .config import settings、from ..utils.logger import setup_logger。尽量不用from xx import *它会把命名空间搅成一锅粥而且不清晰。4.3 一个模块就是一个独立命名空间模块的机制本质是分配一个独立的命名空间模块内定义的变量、函数、类都挂在这个命名空间下通过模块名.xxx访问。这个特性也衍生出一个常见用法把配置集中放在一个模块里。# config.py DATABASE_URL sqlite:///data.db DEBUG True MAX_RETRIES 5其他模块通过import config再config.DEBUG访问或者from config import DATABASE_URL。但注意from config import DATABASE_URL是复制引用到当前命名空间如果后续给config.DATABASE_URL重新赋值这个导入的变量不会跟随变化。如果你需要随处可改、全局生效的配置对象更好的方案是放进dataclass或字典里通过config.settings.DATABASE_URL访问修改settings内部字段时所有引用同一对象的地方都能感知。5. 包与工程组织构建可维护的模块化项目函数和模块是Python的代码组织基本单元再往上组合就是包。这一章聊聊怎么用包去组织一个真正能扩展、能维护的项目。5.1 包的本质与__init__.py的角色包就是一个含有__init__.py文件的目录Python 3.3 也支持命名空间包但一般还是建议保留。__init__.py在包被导入时执行它主要有三个作用标识目录是包。控制包的对外接口在__init__.py里 import 子模块或者定义__all__决定from package import *会导出什么。执行包的初始化逻辑比如加载配置、初始化日志、注册钩子。在设计包的对外接口时一个很好的实践是门面模式——在__init__.py里统一暴露对外API而隐藏内部实现细节。使用者只需要from mypackage import create_app不需要知道这个函数定义在哪个子模块。# mypackage/__init__.py from .core import create_app from .models import User, Session from .utils import setup_logging __all__ [create_app, User, Session, setup_logging]这样内部重构比如把core拆成core_engine时,只要__init__.py的对外导出不变调用方代码就不用改。5.2 相对导入的实践边界包内模块之间互相引用时用相对导入确实清爽但有些情况下会出问题比如你直接运行包内的某个模块文件python mypackage/core.py会报错attempted relative import with no known parent package。原因是作为脚本运行时Python认为这个文件不属于任何包。所以我一贯的建议是建立严格的入口规则——项目只有一个或少数几个顶层入口脚本其余组件全部以包的形式存在运行时统一通过python -m方式从项目根目录启动。常见项目结构参考project_root/ ├── main.py ├── mypackage/ │ ├── __init__.py │ ├── core.py │ ├── models.py │ ├── utils/ │ │ ├── __init__.py │ │ ├── logger.py │ │ └── validators.py ├── tests/ │ ├── test_core.py │ └── test_models.py └── pyproject.tomltest目录里的测试文件同样不要用相对导入直接用from mypackage.core import ...因为测试通常作为顶层脚本运行。5.3__name__ __main__究竟保护了什么几乎所有Python脚本的底部都有这么一句if __name__ __main__: main()它保护的是模块被导入时不执行入口逻辑。当文件被直接运行时__name__是__main__入口逻辑才执行。当文件被import时__name__是模块名入口逻辑被跳过。这句代码的价值在于同一个文件既可以作为脚本使用也可以作为模块导入——导入时不产生副作用。这在写工具脚本、测试辅助文件时非常实用。如果没有这个保护别人一import你的模块整个程序就跑起来了那种体验真的很糟糕。我现在写脚本的惯例是把所有业务逻辑尽量拆进函数或类里文件末尾只保留一个main()函数调用放在__name__ __main__的保护之下。6. 命名空间、别名与常见依赖陷阱最后聊几个实际项目中几乎必然遇到的脏东西——命名冲突、导入别名的坑、以及第三方依赖管理的经验。6.1 命名冲突同名模块与同名变量写大一点的项目最头疼的问题之一是不同包里有同名模块。比如你的项目里有个utils.py你装的某个第三方库内部也有个utils.py。如果项目根目录在sys.path的最前面你项目里的utils会优先被找到第三方库内部导入它自己的utils时如果它用的是顶层导入就可能被你的utils拦截导致莫名其妙的错误。这类问题排查起来很费神现象也千奇百怪。我的经验是给模块起名时避免过于通用utils、common、helper这些名字能不用就不用或者至少放在更具体的子包下面如app/utils/string_helper.py。安装第三方库时留意它的文档里是否强烈建议固定版本。用一个干净的项目目录结构把src目录设为模块根目录src-layout可以降低很多命名污染风险。6.2 别名导入的职责as 用对了是清晰用错了是隐晦import xxx as yyy本身不是坏东西它有两个健康的用途处理包名太长的问题。比如import matplotlib.pyplot as plt行业惯例如此大家都认。解决命名冲突。比如标准库json和第三方某个库都叫json可以用别名区分。但别名不能滥用。我看到过一些代码整个项目里到处都是from xxx import a as b为了省打字把一个清晰的名字改成了完全看不出含义的简写最后连作者自己都要靠IDE查定义。这个度怎么把握我的标准是如果别名不超过3个字母的行业通用惯例np、pd、plt可以用除此之外一律写全名。6.3 依赖管理把模块边界和 pip 生态结合起来模块和包写好了工程要跑起来肯定离不开第三方依赖。这里有几句实在话一定要用虚拟环境不要图省事把包装到全局环境。项目多了你就会明白依赖版本冲突是真实的灾难。requirements.txt里尽量锁版本或者用pyproject.toml 锁文件方式管理。能让项目在一段时间后还能复现这个收益是远期的。不要在requirements.txt里写一堆没用的包。每个包都意味着安全风险和维护负担用多少装多少。我自己踩过一次教训某项目里有个模块一直好好的某天突然崩了检查半天发现是同事装了个新版本依赖间接改变了另一个库的行为。把依赖锁住之后这类问题就再没发生过。7. 组织一段代码的完整示例从零到模块化纸上谈兵这么多我用一个模拟项目例子把前面的知识点串起来。假设要写一个命令行工具用来读取CSV文件、按列求和、输出结果。我会怎么组织7.1 目录结构规划csv_tool/ ├── main.py ├── csv_tool/ │ ├── __init__.py │ ├── reader.py │ ├── analyzer.py │ ├── reporter.py │ └── config.py ├── tests/ │ └── test_analyzer.py └── requirements.txtmain.py只负责入口调度业务逻辑全部放进csv_tool包里。7.2 各模块的定位config.py集中放默认参数比如分隔符、默认输出格式。reader.py负责读取CSV文件返回结构化数据。analyzer.py负责对数据进行计算不关心输入来自文件还是网络。reporter.py负责把计算结果格式化成输出打印或写入文件。__init__.py导出对外接口load_data、analyze、report。每个模块内部定义函数函数尽量保持输入——处理——输出的纯函数风格。比如 analyzer 里的求和函数def sum_column(data, column_name): 对指定列求和跳过非数字行。 total 0.0 valid_count 0 for row in data: value row.get(column_name) try: total float(value) valid_count 1 except (TypeError, ValueError): continue return {total: total, valid_count: valid_count}这样设计的好处是每个函数可以独立测试逻辑边界清晰后续加新功能比如求平均、求最大最小只需在 analyzer 里加新函数不动其他模块。7.3 受保护模块 vs 对外接口在__init__.py里做一次对外门面定义from .reader import read_csv from .analyzer import sum_column, average_column from .reporter import print_report __all__ [read_csv, sum_column, average_column, print_report]使用者只需要import csv_tool data csv_tool.read_csv(sales.csv) result csv_tool.sum_column(data, amount) csv_tool.print_report(result)内部模块就算之后被拆分重组只要对外接口不变调用方代码一行都不用改。这种内聚、分层的方式我用了很多年最大的体会就是后半夜有人改代码不会牵连到你你改代码也不会砸了别人的锅。8. 调试与排查技巧函数和模块问题的实战诊断法最后分享一些针对函数和模块问题的排查方法。这里没有高深理论全是实操层面的诊断手段。8.1 快速定位这是哪个模块的文件代码写多了一个项目里几百个模块有时候报错的堆栈指向util.py但你的项目里可能有三个util.py。找到真实路径的最快方法import util print(util.__file__)这行代码会打印出模块的绝对路径。看到路径你就知道它来自哪一层目录或者哪个site-packages。我一直觉得排查模块问题第一件事就是__file__比瞎猜快十倍。8.2 模块加载期异常区分编译期和执行期有些模块在导入时直接抛异常可能是因为模块顶层代码里有语法错误、文件编码问题或者执行了不安全的操作。定位方法是检查堆栈最底部的文件路径再手动执行那一个模块python -c import mypackage.submodule如果单独导入还能成功那问题多半出在依赖顺序——比如这个模块依赖另一个还没初始化的模块。如果单独导入也报错那就是模块本身的问题直接修那个文件。8.3 常见函数行为排查加打印和 inspect当函数行为不符合预期时我常用的工具有两个import inspect print(inspect.signature(func)) # 查看函数的参数签名 print(inspect.getsource(func)) # 查看函数的源码inspect.signature可以打印出函数真实的参数要求特别适合排查装饰器包装后签名丢失的问题。inspect.getsource则能在运行时直接看到源码对排查这是哪来的函数非常高效。另一个实用技巧是给函数加监听式日志在函数入口和出口打印参数与返回值。但注意别用 print 满屏刷我一般会用一个带模块名的 logger 来区分消息来源这样过滤起来方便。8.4 虚拟环境下模块找不到的排查路径如果代码本地跑得好好的换一台电脑或者部署到服务器上就报ModuleNotFoundError排查顺序我建议如下确认当前是否激活了正确的虚拟环境which python看清楚解释器路径。确认项目根目录在 sys.path 中运行python -c import sys; print(sys.path)。确认依赖是否安装pip list看有没有对应的包。确认版本是否一致pip show 包名查看版本和本地方比对。如果用到相对导入确认是不是用python -m方式运行的。这套流程走下来90%的模块找不到问题都能定位到。剩下10%多半是环境变量或者路径配置的玄学问题那就需要具体问题具体分析了。我个人的习惯是写完一个项目就顺手写一个setup.py或pyproject.toml用可安装包的方式管理项目。这样无论在哪台机器上pip install -e .一下模块路径就永远正确再也不用纠结 sys.path 的问题了。9. 实践中的最终体会写了这么多年Python函数和模块这两个基础概念我回头看觉得每一层都有值得深挖的东西。函数不只是能跑就行参数设计、作用域、闭包、装饰器每一层都能让代码从能跑变成好维护模块也不只是能import就行缓存机制、命名空间、相对导入、包的边界决定了项目长大之后是顺滑还是僵硬。在具体实践中我越来越倾向于小函数、懒模块、清晰接口这三个原则。小函数意味着每个函数只干一件事方便测试和复用懒模块意味着模块不要一导入就执行一堆副作用把初始化推迟到真正需要的时候清晰接口意味着包的对外暴露要稳定内部怎么改是内部的事。如果你正在学Python我建议不要停留于会写语法可以刻意练一练给一个现有脚本做函数化重构再把它变成包加上测试最后做成可安装的包。这个过程走一遍对函数和模块的理解会有一个质的提升。这篇就聊到这里如果你在实践中有其他有意思的坑欢迎在评论区一起讨论。