这事儿我在数据采集脚本里见过不止一次。
代码能跑。
from google.cloud import storage
client = storage.Client()运行没报错,项目也确实连上了对象存储。结果切回 VS Code,输入:
from google.cloud import stPylance 不提示 storage。
有时候连已经存在的类都不自动补全。更尴尬的是,程序继续运行,像什么都没发生过。
这到底是 Python 包坏了,还是 Pylance 拉胯?
先别急着改配置。这个现象通常不是一个问题,而是四个机制叠在了一起。
先把最容易混在一起的两件事分开:
这不是同一件事。
Python 执行:
from google.cloud import storage时,会按照当前解释器的 sys.path 去找模块。只要它最终找到了 google.cloud.storage,导入就能成功。
Pylance 的自动导入则不一样。它通常不会为了获取提示而真的执行整个包。它需要扫描目录、解析 __init__.py、读取类型存根、判断包的导出关系,然后建立自己的索引。
一个是“现在能不能加载”。
一个是“我能不能提前判断你可能要加载什么”。
这能完全一样吗?
不能。
这个问题最普通,也最容易被复杂现象掩盖。
比如你在终端里执行:
pip install google-cloud-storage包被装进了 A 环境。
但 VS Code 当前选中的解释器是 B 环境。
于是出现一种很典型的错觉:
我现在遇到这类问题,第一步不会看 settings.json,而是先看解释器路径。
python -c "import sys; print(sys.executable)"
python -m pip show google-cloud-storage
python -c "from google.cloud import storage; print(storage.__file__)"这三条命令要在 VS Code 当前使用的环境里执行。
如果项目用了虚拟环境,还要确认 VS Code 左下角显示的解释器,和终端里的 sys.executable 对得上。
不对就先切解释器。
这一步没对上,后面研究 __init__.py 都是白费劲。
google.cloud 这类包,不一定是传统的普通包很多人看到一个目录没有 __init__.py,第一反应是:包结构坏了。
不一定。
Python 支持命名空间包。像 google、google.cloud 这种路径,可能由多个独立安装的发行包共同组成。google-cloud-storage 只是其中的一部分,它不一定需要在某个统一的 google/cloud/__init__.py 里把所有子包写死。
运行时只要搜索路径里存在对应目录,Python 就可能把它拼起来。
这对运行时很方便。
对静态分析器就没那么省心了。
因为 Pylance 需要判断:
storage 是一个模块、一个子包,还是某个文件里的变量;所以,__init__.py 缺失,不一定意味着导入错误。
但它确实会影响工具理解包结构的方式。
__init__.py 到底做了什么?我们自己写一个简单包:
my_pkg/
├── __init__.py
└── client.pyclient.py 里有:
class Client:
pass如果 __init__.py 是空的,那么更明确的导入方式是:
from my_pkg.client import Client这时 Client 属于 my_pkg.client 模块,并没有自动变成 my_pkg 的公开成员。
如果我们希望用户这样写:
from my_pkg import Client就需要在 __init__.py 里显式导出:
from .client import Client
__all__ = ["Client"]这两种写法对 Python 来说都合法。
区别在于,第一种公开的是子模块路径,第二种公开的是包的顶层 API。
Pylance 更容易理解第二种,因为导出关系写在了静态代码里。
注意,是“更容易理解”,不是“加了 __all__ 就一定自动提示”。
__all__ 主要解决的是公开名称和通配符导入语义。它不能凭空把一个运行时才生成的对象变成静态分析器已经知道的符号。
有些包为了减少启动时间、兼容不同版本,喜欢做动态导出。
例如:
# package/__init__.py
def __getattr__(name):
if name == "Client":
from .client import Client
return Client
raise AttributeError(name)这样写以后:
from package import Client在运行时可能完全正常。
问题是,Pylance不一定会执行 __getattr__。它也不会随便运行第三方包,因为执行包代码可能有副作用,甚至会修改环境、发网络请求、读取配置。
那它怎么知道 Client 存在?
如果包同时提供了类型存根,事情会好很多:
package/
├── __init__.py
├── __init__.pyi
└── py.typed__init__.pyi 可以把静态 API 写出来:
from .client import Client as Client对于类型检查器来说,.pyi 是“这个模块对外承诺有哪些名字”的说明书。
而 py.typed 通常表示这个包携带的类型信息可以被类型检查器使用。
所以这里有一个经常被忽略的关系:
.pyi 和 py.typed 影响类型工具能不能理解;__init__.py 和显式导出影响工具能不能找到顶层 API;__getattr__ 会让运行时能力超过静态可见范围。这几件事不是一回事。
site-packages就算解释器正确,包结构也没问题,自动导入仍然可能不完整。
原因是索引有成本。
第三方包很多,目录层级很深,文件数量也不少。如果 Pylance 每次启动都把所有依赖完整展开,机器风扇先起飞,编辑器再说话。
所以 Pylance 会对部分包做索引控制。
常见相关设置包括:
{
"python.analysis.autoImportCompletions": true,
"python.analysis.indexing": true
}对于层级较深、自动导入不完整的包,也可以配置包索引深度:
{
"python.analysis.packageIndexDepths": [
{
"name": "google.cloud",
"depth": 3,
"includeAllSymbols": true
}
]
}这里要注意三点。
第一,autoImportCompletions 只控制自动导入建议,不负责修复解释器路径。
第二,索引深度主要影响“能不能被搜到”,不等于运行时 import 规则发生了变化。
第三,includeAllSymbols 不是越多越好。索引范围开得很大,可能让分析变慢,也可能把内部实现名称一起暴露出来。
为了一个提示,把整个依赖树都扫一遍,划算吗?
通常不划算。
import sys
print(sys.executable)再确认包实际装在哪里:
python -m pip show google-cloud-storage不要只看系统里的 pip 输出。
分别测试:
import google
import google.cloud
from google.cloud import storage
from google.cloud.storage import Client哪一层失败,问题范围就缩小了。
如果:
from google.cloud import storage成功,但:
from google.cloud import StorageClient失败,那就不是“整个包找不到”,而是顶层没有导出这个名字,或者这个名字只在子模块里存在。
可以查看包路径:
import google.cloud.storage as storage
print(storage.__file__)然后检查:
__init__.py 有没有显式导入;.pyi 类型存根;py.typed;这里我通常不建议一上来就改包源码。
先确认它到底提供了哪一种 API。
如果前面都没问题,再看:
python.analysis.indexing 是否关闭;python.analysis.autoImportCompletions 是否关闭;有时候重新选择一次解释器,再执行“Developer: Reload Window”,提示就回来了。
有时候不会。
因为工具能力也有边界。
自动导入很方便,但它只是编辑器根据静态信息给出的建议。
它不是 Python 运行时的权威答案。
一个包可能存在以下情况:
__getattr__ 动态生成对象;所以“Pylance没有提示”至少可能对应三种完全不同的结论:
这三种情况,修法完全不同。
第一种修解释器。
第二种查包结构和公开 API。
第三种才考虑存根、索引和 IDE 能力。
如果是自己维护的包,建议把公开 API 写得明确一点:
my_pkg/
├── __init__.py
├── client.py
├── client.pyi
└── py.typed顶层导出不要只靠动态逻辑:
from .client import Client
__all__ = ["Client"]如果实现必须动态加载,就补充对应的类型存根。
如果包是给团队内部使用的,文档里也最好明确:
# 推荐
from my_pkg import Client
# 不建议依赖内部实现
from my_pkg._internal.client_impl import Client否则用户今天跟着自动提示导入,明天包一升级,内部目录一改,代码就崩。
真正稳定的是公开 API,不是某个编辑器刚好提示出来的路径。
Python 包能运行,Pylance却不能自动导入,通常不是一个简单的“插件坏了”。
它更像是四层信息没有对齐:
.pyi 和 py.typed;我做跨境采集这几年,工具链里最怕的就是“代码能跑,所以环境一定没问题”这种判断。
运行时成功,只能证明当前这一次执行找到了目标。
它不能证明:
所以遇到这类问题,先别急着搜一行神配置。
先问自己一句:
到底是 Python 找不到,还是 Pylance看不懂?
答案不一样,排查路线也不一样。
这事儿,不能混着修。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。