Python的秘密生活:导入系统

发布: (2025年12月10日 GMT+8 11:46)
5 min read
原文: Dev.to

Source: Dev.to

Python 如何找到你的代码(以及为什么有时会找不到)

Timothy 懒散地靠在椅子上,盯着终端,带着那种调试同一个问题三小时后的疲惫与沮丧。

“我不明白,”他喃喃道,已经第百次运行脚本了。
“我把文件删掉了。我把 old_utils.py 从项目目录里物理删除了。我甚至重启了终端。但当我运行代码时…”

import utils

print(utils.get_version())
# Output: "Version 1.0 - DEPRECATED"

“它仍然在加载旧版本!这个文件根本不存在了!”

Margaret 走了过来,脸上带着会心的笑容。
“你的 Python 并没有被闹鬼,它只是记性很好。你在和导入系统作斗争——而且因为不懂规则而输掉了。”

缓存:sys.modules

当你输入 import utils 时,Python 首先做什么?

“它会在我的目录里找 utils.py 吗?”

Python 很懒——只要能省事就不做。它在搜索文件系统之前,会先检查一个叫 sys.modules 的字典。

import sys

print(type(sys.modules))   # <class 'dict'>
print(len(sys.modules))     # e.g., 347 (varies)

# Show a few entries
for name in list(sys.modules.keys())[:10]:
    print(name)
# sys, builtins, _frozen_importlib, _imp, _thread, ...

sys.modules 将模块名映射到模块对象。如果 'utils' 已经是一个键,Python 会立即返回那个缓存的对象——根本不触碰文件系统。

import sys
import json

print('json' in sys.modules)          # True
print(sys.modules['json'])            # <module 'json' from '.../json/__init__.py'>
print(json.dumps({'test': 'data'}))   # {"test": "data"}
print(dir(sys.modules['json'])[:5])   # ['JSONDecodeError', 'JSONDecoder', ...]

因为缓存会在进程生命周期内一直存在,模块源码的更改在解释器重新启动或缓存被刷新之前是不可见的。

问题演示

# Create a simple module file
with open('example.py', 'w') as f:
    f.write('''
def greet():
    return "Hello, version 1"
''')

import example
print(example.greet())   # Hello, version 1

# Modify the file
with open('example.py', 'w') as f:
    f.write('''
def greet():
    return "Hello, version 2"
''')

# Re‑import (still cached)
import example
print(example.greet())   # Hello, version 1
print('example' in sys.modules)  # True

重新加载模块

危险做法:从 sys.modules 删除 ❌

import sys

del sys.modules['example']   # Remove cached entry

import example               # Reloads from disk
print(example.greet())       # Hello, version 2

为什么危险:程序的其他部分可能仍然持有旧模块对象的引用,导致行为不一致。

import sys
if 'example' in sys.modules:
    del sys.modules['example']

# Write version 1
with open('example.py', 'w') as f:
    f.write('def greet(): return "Version 1"')

import example as ex1
from example import greet as greet1

# Replace file with version 2 and delete cache
del sys.modules['example']
with open('example.py', 'w') as f:
    f.write('def greet(): return "Version 2"')

import example as ex2
from example import greet as greet2

print(greet1())   # Version 1 (old reference)
print(greet2())   # Version 2
print(ex1 is ex2) # False

正确做法:importlib.reload()

import importlib
import example

# Edit the file to version 2
with open('example.py', 'w') as f:
    f.write('def greet(): return "Version 2"')

importlib.reload(example)   # Updates the existing module object
print(example.greet())      # Version 2
print(id(example))          # Same memory address as before

reload() 会就地更新模块,所以已有的引用会看到新代码。不过,在重新加载之前执行的 from example import greet 语句仍会指向旧的函数对象。

映射表:sys.path

如果模块尚未被缓存,Python 会使用 sys.path 在磁盘上搜索它。sys.path 是一个目录列表,定义了导入搜索顺序。

import sys
print(sys.path)

常见的条目包括:

  1. 用来启动 Python 解释器的脚本所在的目录。
  2. PYTHONPATH 环境变量中的条目(如果设置了)。
  3. 标准库目录。
  4. site‑packages(第三方库)。

Python 会在找到的第一个匹配项就停止搜索。通过操作 sys.path(例如在最前面插入项目的 src 文件夹),可以控制导入哪个版本的模块。

import sys
sys.path.insert(0, '/path/to/my/project')
import mymodule   # Now Python looks in '/path/to/my/project' first

要点总结

  • sys.modules 是 Python 的内存缓存,保存已导入的模块;它让导入变快,但也可能导致代码陈旧。
  • 使用 importlib.reload() 可以安全地重新加载模块。
  • 当 Python 在磁盘上搜索模块时,要留意 sys.path
  • 调试导入问题时,首先检查模块是否已经被缓存。
Back to Blog

相关文章

阅读更多 »