一般来说,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 几乎是不可能的:

Python

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/ 目录,但综合考虑可能是性价比的方案了。

Python

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 更广阔的应用场景做一份贡献!