请不要再使用 __file__ 啦!

一般来说,Python 的 module 会有一个 __file__ 属性,定义了 module 的 path。在 Python 中,使用这个属性非常常见,比如获取 module 所在的目录地址,以便于读取这个 module 同级的,非 Python 脚本的其他文件,比如库需要依赖的数据等:module_dir = os.path.abspath(os.path.dirname(__file__)) ;或者用来获取脚本的位置,来进行魔法 import 操作。

但严格来说 __file__ 不总是有的。Python 文档中说:

__file__ 当 module 是从文件中加载的,才会有。如果是静态编译的 C Module,就不会有这个 __file__,extension module 中如果动态链接了 shared lib,那么 __file__ 的值就是动态链接库的位置。

另外,如果你的包是在一个 zip 里面的,__file__ 也没有。

所以,所有使用了 __file__ 的 Python 代码:

  • 要么是 broken 的;
  • 要么选择了不会支持其他的 module loader(不从文件系统 load module)

但是我们经常有读文件的需求,比如图片、json、csv 等,不用 __file__ 的话,怎么知道它们在哪里呢?

如果 Python 版本 < 3.7,解决方案会有点复杂。因为 3.7 之前一直没有一个完美的方案来读取文件资源。有一段时间 pkg_resources 是推荐的方式,Python3 后来引入了 ResourceLoader 接口,然后在 3.7 又 deprecated 了这个接口,并引入了 ResourceReader ,提供了对应的 API importlib.resources module 。即使最近的 ResourceReader,也有一些行为的不一致。但是到目前为止,应该是这些 API 中最好的选择了。

如果你使用 Python3.7+ 的话,可以直接使用内置的 importlib。如果是使用 3.5+ 的 Python 的,建议使用 importlib_resources,这个库是 importlib.resources 的向后兼容版本(实际上,这个库推荐 3.9 之前都使用这个库,3.9 使用标准库)。如果是用 3.5 以下的 Python 的话,应该尽快升级 Python 的版本。

当然了,一些小的脚本,可以不考虑兼容性直接使用 __file__ 的。

可能有人认为 Python 总是用脚本来跑的,可以依赖文件系统的路径,没有什么大不了的。但是我觉得现在 Python 的应用场景越来越多,Python 代码应该考虑运行在各种环境,最好不要对这些环境做一些“假设”。

这些想法起源与最近把 IRedis 打包成一个二进制来跑,希望用户在使用 IRedis 的时候,只要用 cURL 下载下来,然后解压,就可以运行了,不用装 pip,不用使用 pip 安装,甚至不装 Python 或者发行版的 Python 不支持 IRedis 都没问题,因为 IRedis 运行所需要的所有东西都已经包装在这个二进制里面了。在某些情况下是非常有用的,比如说 Redis 的 Docker 镜像,里面是没有 Python 的。

另外打包成 binary 体积也更小,现在用 docker 下载 Python 的 image 将近 1G 了,而打包好的 iredis.tar.gz 才 22M(包含Python解释器)。

这项打包工作是 Mac Chaffee 完成的,他写的打包脚本非常精彩,有兴趣的可以看一下这项工作的记录,很有意思:

Anyway,我们遇到的问题是,很多包都依赖了 __file__ ,去 patch 这些 package 几乎是不可能的:

所以就采用了一个这种的方案,将这些依赖放到一个 lib/ 下,依然让它们作为文件系统的文件存在。虽然这样让打包出来的东西并不是一个纯正的 binary,解压之后会有一个 iredis binary,还有一个 lib/ 目录,但综合考虑可能是性价比的方案了。

如果你正在开发的是一个 lib 的话,强烈建议不要在代码中依赖 __file__ 了。为 Python 更广阔的应用场景做一份贡献!



请不要再使用 __file__ 啦!”已经有2条评论

  1. 所以 Django 是「选择了不会支持其他的 module loader」喽,我看 Django 在 setting.py 里都是直接使用 __file__ 的

    • settings 里面属于用户的代码,用户可以使用其他的方法读资源的,不一定非得使用默认的 __file__。

      不过我看 Django 代码库很多地方都用 __file__,现在很多库要求不依赖 __file__ 几乎是不可能的了。所以只能提倡新的库不要依赖,假如一个东西没有对老库的以来的话,打包还是可能的。

Leave a comment

您的电子邮箱地址不会被公开。 必填项已用 * 标注