<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>命名冲突 on Zero Day Notes</title>
        <link>https://blog.5772447.xyz/tags/%E5%91%BD%E5%90%8D%E5%86%B2%E7%AA%81/</link>
        <description>Recent content in 命名冲突 on Zero Day Notes</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh</language>
        <lastBuildDate>Mon, 09 Nov 2026 09:00:00 +0800</lastBuildDate><atom:link href="https://blog.5772447.xyz/tags/%E5%91%BD%E5%90%8D%E5%86%B2%E7%AA%81/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>email.py 撞名标准库：smtplib 报错</title>
            <link>https://blog.5772447.xyz/posts/6bd44f06/</link>
            <pubDate>Mon, 09 Nov 2026 09:00:00 +0800</pubDate>
            <guid>https://blog.5772447.xyz/posts/6bd44f06/</guid>
            <description>&lt;h2 id=&#34;问题背景&#34;&gt;&lt;a href=&#34;#%e9%97%ae%e9%a2%98%e8%83%8c%e6%99%af&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;问题背景&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;运维组有一台跳板机，&lt;code&gt;/opt/ops/&lt;/code&gt; 下放着一批 Python 脚本：备份、巡检、日报。日报脚本每天定时把 Excel 报表用 &lt;code&gt;smtplib&lt;/code&gt; 发到管理层邮箱，跑了两年多，一直稳定。&lt;/p&gt;&#xA;&lt;p&gt;上周同事写了一个发告警邮件的小工具，图省事直接命名为 &lt;code&gt;email.py&lt;/code&gt;，放在同一个目录里，供其他脚本 &lt;code&gt;import email&lt;/code&gt; 调用。当天下午的日报任务失败，第二天早上的巡检也没发出来。登录机器手动执行，报错一模一样；把脚本拷到 &lt;code&gt;/tmp&lt;/code&gt; 下运行，又正常了。&lt;/p&gt;&#xA;&lt;h2 id=&#34;故障现象&#34;&gt;&lt;a href=&#34;#%e6%95%85%e9%9a%9c%e7%8e%b0%e8%b1%a1&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;故障现象&#xD;&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;日报脚本执行到发信步骤抛异常，退出码非零，cron 没有发出任何邮件。&lt;/li&gt;&#xA;&lt;li&gt;报错核心是 &lt;code&gt;ModuleNotFoundError: No module named &#39;email.message&#39;; &#39;email&#39; is not a package&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;同一个脚本在 &lt;code&gt;/tmp&lt;/code&gt; 目录下运行正常，在 &lt;code&gt;/opt/ops/&lt;/code&gt; 下必现。&lt;/li&gt;&#xA;&lt;li&gt;Python 版本、依赖包、邮箱配置都没有变过；最近唯一的变更就是新增了 &lt;code&gt;email.py&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;其他目录下的 Python 脚本发信正常，只有 &lt;code&gt;/opt/ops/&lt;/code&gt; 里的脚本受影响。&lt;/li&gt;&#xA;&lt;li&gt;报错栈指向 &lt;code&gt;smtplib&lt;/code&gt; 的导入语句，看起来像是标准库坏了，很容易往“Python 环境损坏”方向排查。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;排查过程&#34;&gt;&lt;a href=&#34;#%e6%8e%92%e6%9f%a5%e8%bf%87%e7%a8%8b&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;排查过程&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;第一步读完整报错。异常栈里 &lt;code&gt;smtplib.py&lt;/code&gt; 执行 &lt;code&gt;import email.message&lt;/code&gt;，随后抛出“email 不是包”。标准库的 &lt;code&gt;email&lt;/code&gt; 明明是一个包，&lt;code&gt;email.message&lt;/code&gt; 是它的子模块，正常情况下不可能导入失败；报错说明 Python 找到的 &lt;code&gt;email&lt;/code&gt; 不是标准库那个。&lt;/p&gt;&#xA;&lt;p&gt;第二步看 Python 实际导入了什么。在 &lt;code&gt;/opt/ops/&lt;/code&gt; 目录下执行 &lt;code&gt;python -c &amp;quot;import email; print(email.__file__)&amp;quot;&lt;/code&gt;，输出是 &lt;code&gt;/opt/ops/email.py&lt;/code&gt;，不是 Python 安装目录下的 &lt;code&gt;email/__init__.py&lt;/code&gt;。也就是说，本地这个同名文件把标准库的 &lt;code&gt;email&lt;/code&gt; 包整个顶掉了。&lt;/p&gt;&#xA;&lt;p&gt;第三步确认导入顺序。&lt;code&gt;python -c &amp;quot;import sys; print(sys.path[:3])&amp;quot;&lt;/code&gt; 显示 &lt;code&gt;sys.path&lt;/code&gt; 的第一项是脚本所在目录（或空字符串代表当前目录），标准库路径排在后面。Python 导入模块时按 &lt;code&gt;sys.path&lt;/code&gt; 顺序查找，本地目录优先，所以 &lt;code&gt;import email&lt;/code&gt; 先命中了 &lt;code&gt;/opt/ops/email.py&lt;/code&gt;。这里还有一个容易混淆的点：&lt;code&gt;python script.py&lt;/code&gt; 时 &lt;code&gt;sys.path[0]&lt;/code&gt; 是脚本所在目录，&lt;code&gt;python -m module&lt;/code&gt; 时是当前目录，交互式解释器则是空字符串；两种方式都可能把本地文件顶到标准库前面。另外，&lt;code&gt;PYTHONPATH&lt;/code&gt; 里的路径也会插在标准库之前，排查时要一并看。&lt;/p&gt;&#xA;&lt;p&gt;第四步解释报错为什么指向标准库。&lt;code&gt;smtplib&lt;/code&gt; 内部要导入 &lt;code&gt;email.message&lt;/code&gt;、&lt;code&gt;email.utils&lt;/code&gt; 这些子模块；本地的 &lt;code&gt;email.py&lt;/code&gt; 是一个普通模块文件，不是包，不能有子模块，于是 &lt;code&gt;import email.message&lt;/code&gt; 失败，报“email is not a package”。错误信息里出现的是 &lt;code&gt;smtplib&lt;/code&gt; 和 &lt;code&gt;email&lt;/code&gt;，真正的“凶手”是那个同名的本地文件，所以第一眼很难联想到它。&lt;/p&gt;&#xA;&lt;p&gt;第五步验证因果。把 &lt;code&gt;email.py&lt;/code&gt; 重命名为 &lt;code&gt;mail_helper.py&lt;/code&gt;，同时删掉目录下的 &lt;code&gt;__pycache__&lt;/code&gt;（避免残留的 &lt;code&gt;email.cpython-*.pyc&lt;/code&gt; 干扰），再执行 &lt;code&gt;python -c &amp;quot;import email; print(email.__file__)&amp;quot;&lt;/code&gt;，输出回到标准库路径；日报脚本立即恢复正常。把文件改回 &lt;code&gt;email.py&lt;/code&gt;，故障复现，变量唯一。&lt;/p&gt;&#xA;&lt;p&gt;第六步排查同类隐患。扫描 &lt;code&gt;/opt/ops/&lt;/code&gt; 下所有文件名，和 Python 标准库模块名比对，确认没有 &lt;code&gt;logging.py&lt;/code&gt;、&lt;code&gt;json.py&lt;/code&gt;、&lt;code&gt;types.py&lt;/code&gt;、&lt;code&gt;queue.py&lt;/code&gt; 这类同名文件；这几个名字一旦撞上，会分别把日志、JSON、类型、队列相关模块顶掉，症状各不相同，但根因一样。&lt;/p&gt;&#xA;&lt;h2 id=&#34;解决方案&#34;&gt;&lt;a href=&#34;#%e8%a7%a3%e5%86%b3%e6%96%b9%e6%a1%88&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;解决方案&#xD;&#xA;&lt;/h2&gt;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;重命名同名文件&lt;/strong&gt;：把 &lt;code&gt;email.py&lt;/code&gt; 改成 &lt;code&gt;mail_helper.py&lt;/code&gt; 之类的业务名，确保不与标准库顶层模块重名；改完删除 &lt;code&gt;__pycache__&lt;/code&gt;，再验证 &lt;code&gt;import email&lt;/code&gt; 指向标准库。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;改成包结构&lt;/strong&gt;：把运维脚本收进一个包，例如 &lt;code&gt;ops/&lt;/code&gt; 目录加 &lt;code&gt;__init__.py&lt;/code&gt;，工具模块用 &lt;code&gt;ops.mail_helper&lt;/code&gt; 导入；包内文件名不再参与顶层模块名解析，冲突面小很多。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;加自动检查&lt;/strong&gt;：在 CI 或 pre-commit 里扫描仓库中的 &lt;code&gt;.py&lt;/code&gt; 文件名，与 &lt;code&gt;sys.stdlib_module_names&lt;/code&gt; 求交集，命中就失败；也可以用 Ruff 的 &lt;code&gt;A005&lt;/code&gt;（stdlib-module-shadowing）规则兜底。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;统一命名规范&lt;/strong&gt;：工具模块加业务前缀（&lt;code&gt;ops_mail.py&lt;/code&gt;、&lt;code&gt;ops_log.py&lt;/code&gt;），禁止用 &lt;code&gt;email&lt;/code&gt;、&lt;code&gt;logging&lt;/code&gt;、&lt;code&gt;json&lt;/code&gt;、&lt;code&gt;types&lt;/code&gt;、&lt;code&gt;queue&lt;/code&gt;、&lt;code&gt;select&lt;/code&gt;、&lt;code&gt;socket&lt;/code&gt; 这类通用名做文件名。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;排障手册补一条&lt;/strong&gt;：遇到标准库导入异常，先执行 &lt;code&gt;python -c &amp;quot;import 模块名; print(模块名.__file__)&amp;quot;&lt;/code&gt; 看实际加载路径，再 &lt;code&gt;print(sys.path[:3])&lt;/code&gt; 看搜索顺序，不要一上来就重装 Python。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;验证&lt;/strong&gt;：改完后跑一遍日报、巡检、备份三个脚本，确认发信、日志、JSON 解析都正常；再把检查脚本接入发布流程。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;根因分析&#34;&gt;&lt;a href=&#34;#%e6%a0%b9%e5%9b%a0%e5%88%86%e6%9e%90&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;根因分析&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;直接根因是 &lt;strong&gt;&lt;code&gt;/opt/ops/email.py&lt;/code&gt; 与标准库 &lt;code&gt;email&lt;/code&gt; 包同名，且脚本目录在 &lt;code&gt;sys.path&lt;/code&gt; 中最先被搜索，导致 &lt;code&gt;import email&lt;/code&gt; 加载了本地文件；&lt;code&gt;smtplib&lt;/code&gt; 需要 &lt;code&gt;email.message&lt;/code&gt; 子模块，本地模块不是包，导入失败&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;p&gt;更深一层，是 Python 的导入机制把“文件名”直接当成了“模块名”。在大多数语言里，一个业务文件叫 &lt;code&gt;email.py&lt;/code&gt; 不会影响系统库；在 Python 里，只要它和脚本在同一目录，就会参与顶层模块解析。命名在这里不是风格问题，而是命名空间问题。&lt;/p&gt;&#xA;&lt;p&gt;还有一层是错误信息的误导性。报错栈指向 &lt;code&gt;smtplib&lt;/code&gt; 和标准库 &lt;code&gt;email&lt;/code&gt;，没有任何一行提到“本地有个 email.py”；如果不知道用 &lt;code&gt;__file__&lt;/code&gt; 和 &lt;code&gt;sys.path&lt;/code&gt; 去验证实际加载路径，很容易把时间花在重装依赖、升级 Python 上。这个故障的排查成本，主要来自“看起来不像自己写的问题”。&lt;/p&gt;&#xA;&lt;h2 id=&#34;预防措施&#34;&gt;&lt;a href=&#34;#%e9%a2%84%e9%98%b2%e6%8e%aa%e6%96%bd&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;预防措施&#xD;&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;建立文件名黑名单：标准库顶层模块名禁止用作脚本/工具文件名；新文件提交前用脚本比对 &lt;code&gt;sys.stdlib_module_names&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;CI 增加 lint 规则：Ruff &lt;code&gt;A005&lt;/code&gt; 或自定义检查，发现 stdlib 同名文件直接失败。&lt;/li&gt;&#xA;&lt;li&gt;运维脚本统一包结构，工具模块加业务前缀；不要把所有脚本平铺在一个目录里靠通用名互相 import。&lt;/li&gt;&#xA;&lt;li&gt;排障手册固化“导入路径三连”：&lt;code&gt;模块.__file__&lt;/code&gt;、&lt;code&gt;sys.path[:3]&lt;/code&gt;、&lt;code&gt;python -v&lt;/code&gt; 导入日志；先确认加载的是谁，再谈修复。&lt;/li&gt;&#xA;&lt;li&gt;发布流程里加一条冒烟测试：用干净环境执行一次核心脚本，确认标准库和第三方依赖没有被本地文件遮蔽。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;总结&#34;&gt;&lt;a href=&#34;#%e6%80%bb%e7%bb%93&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;总结&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;在 Python 里，文件名就是模块名，脚本目录就是搜索路径的第一站。给工具起名 &lt;code&gt;email.py&lt;/code&gt;，等于在自家门口放了一个和邮局同名的信箱，所有寄往“email”的信都会先落到这里。这个故障不需要改代码逻辑、不需要重装环境，只需要把文件改名；难的是从“标准库坏了”的直觉里跳出来，先问一句：Python 实际加载的，到底是哪个文件？&lt;/p&gt;&#xA;&lt;h2 id=&#34;延伸阅读&#34;&gt;&lt;a href=&#34;#%e5%bb%b6%e4%bc%b8%e9%98%85%e8%af%bb&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;延伸阅读&#xD;&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://blog.5772447.xyz/posts/15f31637/&#34; &gt;Popen 未读管道：备份脚本卡在 64KB&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://blog.5772447.xyz/posts/1112ecdc/&#34; &gt;fork 撞上多线程：子进程启动即死锁&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;</description>
        </item></channel>
</rss>
