# 从代码整洁到版本适配聊聊pyupgrade那些事Python这门语言有个有趣的特点它的更新换代总是带着一种“永远在变”的气质。从2到3的剧变再到3.x里那些新增的语法糖每一次升级都像给开发者送了个小礼物。不过礼物虽好整理旧代码却成了苦差事。这就是pyupgrade登场的地方。pyupgrade到底是什么如果你把Python代码想象成一本小说不同版本就像不同时期的写作风格。可能在Python 2时代盛行的手法到了Python 3.6以后就变得啰嗦。pyupgrade就是个自动帮你把旧式写法转换成新版语法的工具它不处理代码逻辑只管语法层面的升级。有点像一个精通所有版本风格的校对员看到“Old Style”的写法就默默推一推眼镜给你改成当下最简洁的Python表达。它不是linter那样挑毛病而是直接动手改代码。它能做什么说几个实际情况。比如你有一段代码是keys list(dict.keys())pyupgrade会直接改成keys list(dict)。你写{}.format(name)它会改成f{name}。你还在用__future__的with_statement导入pyupgrade会提醒你这早就可以拿掉了。更微妙的是类型注释的处理。早期Python 3.5支持的类型注解方式在3.9以后的版本里可以有更简洁的表达。比如List[int]会变成list[int]Optional[str]会变成str | None。这些改动看似微不足道但当项目膨胀到几十万行代码时每一处简化都在减少认知负担。还有个挺实用的场景是处理super()的调用。老代码里经常看到super(ClassName, self).__init__()pyupgrade会把它精简为super().__init__()。这种改动背后其实反映了Python对MRO机制理解的演进。怎么使用基础用法像喝水一样简单。通过pip装好之后pipinstallpyupgrade pyupgrade *.py如果你只想处理特定文件能指定路径。但要小心它默认只是输出改动建议真正想让它动手改得加上--keep-runtime-typing这类选项。不习惯直接在原文件上改的话配合--diff参数能看到具体改了哪些东西。实际项目里更常见的用法是配合pre-commit钩子。在.pre-commit-config.yaml里加一段-repo:https://github.com/asottile/pyupgraderev:v3.3.1hooks:-id:pyupgradeargs:[--py38-plus]这个--py38-plus参数很关键它告诉pyupgrade你基于哪个Python版本做升级。比如你目标版本是3.8它就不会强制改成3.10才支持的match语法。这种版本感知能力避免了“过度升级”导致的不兼容。最佳实践实际操作下来有几个经验值得分享。团队采用pyupgrade时最好先跑一次全量扫描但不直接改代码。把改动建议生成patch文件提交前让每个开发者都看一眼。因为pyupgrade会对代码风格有冲击比如把老旧但没问题的# type: ignore注释重排可能引发CI流水线的警报。另一个建议是把它放在持续集成的lint阶段而不是作为单独的“代码改造日”活动。小步快跑的方式更稳妥。比如每次提交新代码时pyupgrade会检查新增的改动是否符合当前Python版本的写法而不是一股脑改掉整个项目的历史包袱。还有个小技巧搭配black使用。pyupgrade改完后代码格式可能会乱比如f{name}这种字符串变成f-string后长度变化了。让black帮忙格式化一下能避免代码风格不统一的问题。和同类技术对比说到同类工具大家第一个想到的是2to3。但2to3像一把大锤它处理的是Python 2到3的跨时代迁移目标宏大但过于笨重。pyupgrade更像是手术刀只处理从旧版到新版Python的语法优化而且能指定目标版本。另一个常用的是flynt它专注于f-string的转换。如果只是想把.format()改成f-stringflynt很合适。但pyupgrade的覆盖面更广它还会处理super()、类型注解、__future__导入等。autoflake和pyupgrade有时会重叠比如自动移除未使用的导入。但pyupgrade更偏向“写法升级”autoflake更侧重“代码清理”。实践中两者搭配使用效果不错pyupgrade先改写法autoflake再清理冗余。ruff这个新兴工具也包含了列规则检查比如UP开头的规则就和pyupgrade功能重叠。但ruff的重点是linter不直接修改文件pyupgrade是自动修复工具。如果项目中已经用了ruff做lint可以只借助pyupgrade做自动修改避免重复工作。# Pycln一个让你告别无用import的工具写Python代码久了你会发现一个特别常见的问题代码越写越长import语句越来越多有些是早期调试时加的有些是从别处复制过来忘记清理的。这些多余的import留在那里虽然不会让程序崩溃但会让读代码的人疑惑——这个模块到底用在哪如果你和我一样是个有轻微代码洁癖的人可能会花时间手动检查每个import的用途。但项目大了以后这种手动检查基本不现实。这就引出了我们今天要聊的工具——Pycln。Pycln是什么简单说Pycln是一个Python源代码清理工具它的本职工作只有一个找出代码中没有被使用的import语句然后帮你删掉。它不像有些工具那样追求大而全而是专注于解决“无用import”这样一个具体问题。它的工作方式很有意思不是简单地用正则表达式去匹配而是通过解析Python的抽象语法树AST来精确判断每个import是否真的被用到。这意味着它不会因为一个函数名恰好和导入的模块名相同就误判也不会被注释或者字符串中的内容干扰。它能做什么拿生活中一个场景来说明。假设你写了一个用于处理用户数据的脚本importosimportjsonimportsysimportrefromdatetimeimportdatetimefromcollectionsimportdefaultdictdefload_users(filepath):ifnotos.path.exists(filepath):return[]withopen(filepath)asf:returnjson.load(f)defformat_timestamp(ts):returndatetime.fromtimestamp(ts).strftime(%Y-%m-%d)这段代码里sys、re、defaultdict三个import完全没有被用到。小项目里这种问题可能不明显但经历过一个数百行的模块眼看着顶部的import列表长得像购物清单你就知道多难受了。Pycln跑一遍这些多余的import会被精准地标记出来你可以选择让它自动删除。它支持三种import形式import module这种常规导入from module import name这种部分导入import module as alias这种带别名的导入有些工具在处理条件导入或动态导入时容易出问题Pycln在这一点上处理得比较好它会保留那些虽然看起来没被直接使用但实际上可能通过反射或动态方式用到的import。当然这不是绝对的有时候需要你手动排除一些文件。怎么使用安装就是常规操作pipinstallpycln最基础的使用方法pycln your_file.py这会在终端里显示哪些import会被移除但不会真正修改文件。如果你确定没问题想让它直接改pycln your_file.py--yes处理整个项目pycln your_project/--yes我比较喜欢的一个参数是--diff它会把即将做的修改以diff形式展示出来pycln your_file.py--diff这有点像你在提交代码前先看一遍diff心里有个底。另外如果你有些文件是生成出来的或者不应该被修改可以用--ignore排除pycln your_project/--yes--ignoremigrations/*如果你在用pre-commit可以把Pycln配置进去-repo:https://github.com/hadialqattan/pyclnrev:v2.4.0hooks:-id:pycln这样每次commit之前Pycln都会自动清理一遍。最佳实践用了一段时间后我摸索出几个让Pycln更好用的习惯。第一个是配合isort使用。isort负责整理import的排序Pycln负责清理无用的import。顺序很重要先跑Pycln清理掉没用的import再跑isort整理剩下的——这样就不会出现isort给一个本来要删除的import调了格式然后Pycln再删掉白白浪费计算时间。第二个是不要一次性对整个超大项目跑。虽然Pycln速度不慢但如果在几万行代码的大项目里全量跑万一有误判排查起来很痛苦。更好的做法是先在改动过的文件上跑慢慢形成习惯让这种清理成为编码流程的一部分。第三个是关于第三方库的import。有些库导入后不会显式调用而是作为钩子或者注册机制存在。比如某个库的import只是为了让它的信号处理器注册到系统中。这种情况我会在文件顶部加一个注释importsome_library# noqa: F401Pycln会识别# noqa开头的行跳过对这些import的处理。第四个实践是结合测试。如果项目有完善的测试跑完Pycln后执行一遍测试基本能保证没有因为误删import导致的问题。这也是为什么我建议在CI流程中而不是开发环境中跑Pycln删除操作。和同类技术对比Python生态里做类似事情的工具主要有这几个Flake8的F401规则。Flake8会警告未使用的import但它只负责报告不负责修复。你看到了警告还是要手动去删或者用autoflake这类工具来配合。Flake8的优势在于它是一个综合的lint工具不止检查import而Pycln是个单一用途的工具。autoflake。这是和Pycln最接近的竞品。两者都能自动删除无用import。区别在于autoflake还可以做其他事情比如删除未使用的变量。Pycln则更专一。从实际体验看Pycln对某些边缘情况的处理要细致一些——比如在处理__all__变量中引用的import时或者在处理类型注解中使用的import时Pycln的误判率更低。当然这只是我个人的感受不代表所有人的体验。Pylint的W0611。Pylint也会报告未使用的import但同样需要配合其他工具来删除。而且Pylint用起来相对重一些如果你只是为了清理import专门装一个Pylint不如直接用Pycln。isort。很多人把isort和Pycln搞混。isort只处理import的排列顺序和分组不管import有没有被用到。两者是互补关系不是竞争关系。有项目用isort加Pycln的组合就能同时解决import的整理和清理两个问题。从性能上看Pycln因为专注在import这一个点上它的代码路径优化得比较好。对大型项目的扫描速度我自己的测试是比autoflake快一些的。但这也不是绝对的取决于具体的项目结构和运行环境。选哪个工具归根结底看你的需求。如果你只想要一个趁手的工具专门解决无用import的问题Pycln是个不错的选择。如果你需要的是一个能同时做多种清理的工具autoflake可能更合适。没有绝对的好坏只有适不适合。说到底工具的选用要看团队的具体处境。如果项目刚从2.x迁到3.xpyupgrade很合适如果是维护一个已经跑在3.9的项目但想逐步采用新版语法那它的--target-version参数就很有价值。而如果只是偶尔重构几段代码手写f-string可能比引入新依赖更划算。选择哪个工具就像挑一把趁手的螺丝刀——关键不是工具多高级而是你能不能把它用顺手。