一般来说,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 完成的,他写的打包脚本非常精彩,有兴趣的可以看一下这项工作的记录,很有意思:
- Package a binary with PyOxidizer #279
- ISSUE#218 Package a binary with https://github.com/indygreg/PyOxidizer?
- 现在每一个 release 都会打包一个 binary,有兴趣的可以下载跑来试试。
Anyway,我们遇到的问题是,很多包都依赖了 __file__
,去 patch 这些 package 几乎是不可能的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
warning: cProfile contains __file__ warning: ctypes.test contains __file__ warning: distutils.command.bdist_wininst contains __file__ warning: distutils.command.sdist contains __file__ warning: distutils.core contains __file__ warning: distutils.dist contains __file__ warning: distutils.tests contains __file__ warning: doctest contains __file__ warning: encodings contains __file__ warning: idlelib.browser contains __file__ warning: idlelib.config contains __file__ warning: idlelib.help contains __file__ warning: idlelib.help_about contains __file__ warning: idlelib.idle contains __file__ warning: idlelib.idle_test contains __file__ warning: idlelib.pyshell contains __file__ warning: idlelib.runscript contains __file__ warning: idlelib.tree contains __file__ warning: importlib contains __file__ warning: importlib._bootstrap contains __file__ warning: importlib._bootstrap_external contains __file__ warning: importlib.abc contains __file__ warning: inspect contains __file__ warning: lib2to3.pygram contains __file__ warning: lib2to3.tests contains __file__ warning: logging contains __file__ warning: logging.handlers contains __file__ warning: modulefinder contains __file__ warning: multiprocessing.spawn contains __file__ warning: pdb contains __file__ warning: pip.__main__ contains __file__ warning: pip._internal.build_env contains __file__ warning: pip._internal.pyproject contains __file__ warning: pip._internal.req.req_install contains __file__ warning: pip._internal.utils.misc contains __file__ warning: pip._internal.utils.setuptools_build contains __file__ warning: pip._internal.utils.virtualenv contains __file__ warning: pip._vendor contains __file__ warning: pip._vendor.certifi.core contains __file__ warning: pip._vendor.distlib.resources contains __file__ warning: pip._vendor.pep517._in_process contains __file__ warning: pip._vendor.pep517.wrappers contains __file__ warning: pip._vendor.pkg_resources contains __file__ warning: pkgutil contains __file__ warning: profile contains __file__ warning: pyclbr contains __file__ warning: pydoc contains __file__ warning: pydoc_data.topics contains __file__ warning: runpy contains __file__ warning: site contains __file__ warning: trace contains __file__ warning: turtle contains __file__ warning: turtledemo.__main__ contains __file__ warning: unittest contains __file__ warning: unittest.loader contains __file__ warning: unittest.test contains __file__ warning: venv contains __file__ warning: warnings contains __file__ __file__ was encountered in some embedded modules PyOxidizer does not set __file__ and this may create problems at run-time See https://github.com/indygreg/PyOxidizer/issues/69 for more |
所以就采用了一个这种的方案,将这些依赖放到一个 lib/
下,依然让它们作为文件系统的文件存在。虽然这样让打包出来的东西并不是一个纯正的 binary,解压之后会有一个 iredis binary,还有一个 lib/
目录,但综合考虑可能是性价比的方案了。
1 2 3 4 5 |
$ tar xf iredis.tar.gz -C iredis $ ls -l iredis total 63020 -rwxr-x--- 1 laixintao staff 64531880 Mar 1 18:33 iredis drwxr-xr-x 19 laixintao staff 608 Mar 1 18:33 lib |
如果你正在开发的是一个 lib 的话,强烈建议不要在代码中依赖 __file__ 了。为 Python 更广阔的应用场景做一份贡献!
所以 Django 是「选择了不会支持其他的 module loader」喽,我看 Django 在 setting.py 里都是直接使用 __file__ 的
settings 里面属于用户的代码,用户可以使用其他的方法读资源的,不一定非得使用默认的 __file__。
不过我看 Django 代码库很多地方都用 __file__,现在很多库要求不依赖 __file__ 几乎是不可能的了。所以只能提倡新的库不要依赖,假如一个东西没有对老库的以来的话,打包还是可能的。