大型且扩展的PYTHONPATH是否会影响性能?

时间:2018-06-29 05:12:37

标签: python python-2.7 pythonpath

比方说,您有一个项目,该项目在各个地方都有多个级别的文件夹,为了使导入调用更整洁,人们已经为整个项目修改了PYTHONPATH。

这意味着不用说:

from folder1.folder2.folder3 import foo

他们现在可以说

from folder3 import foo

并将folder1 / folder2添加到PYTHONPATH。这里的问题是,如果您继续这样做,并在PYTHONPATH中添加了大量路径,是否会对性能产生明显影响?

要在性能方面增加一些规模感,我要问的是最小毫秒(即:100 ms?500 ms?)

3 个答案:

答案 0 :(得分:3)

因此,在系统调用中将看到在PYTHONPATH中具有许多不同目录与具有深层嵌套的包结构之间的性能折衷。因此,假设我们具有以下目录结构:

bash-3.2$ tree a
a
└── b
    └── c
        └── d
            └── __init__.py
bash-3.2$ tree e
e
├── __init__.py
├── __init__.pyc
└── f
    ├── __init__.py
    ├── __init__.pyc
    └── g
        ├── __init__.py
        ├── __init__.pyc
        └── h
            ├── __init__.py
            └── __init__.pyc

我们可以使用这些结构和strace程序来比较和对比为以下命令生成的系统调用:

strace python -c 'from e.f.g import h'
PYTHONPATH="./a/b/c:$PYTHONPATH" strace python -c 'import d'

许多PYTHONPATH条目

因此,这里的权衡实际上是启动时的系统调用,而不是导入时的系统调用。对于PYTHONPATH中的每个条目,python首先检查目录是否存在:

stat("./a/b/c", {st_mode=S_IFDIR|0776, st_size=4096, ...}) = 0
stat("./a/b/c", {st_mode=S_IFDIR|0775, st_size=4096, ...}) = 0

如果目录存在(它由右0表示...),则解释器启动时Python将搜索许多模块。对于每个模块,它都会检查:

stat("./a/b/c/site", 0x7ffd900baaf0)    = -1 ENOENT (No such file or directory)
open("./a/b/c/site.x86_64-linux-gnu.so", O_RDONLY) = -1 ENOENT (No such file or directory)
open("./a/b/c/site.so", O_RDONLY)       = -1 ENOENT (No such file or directory)
open("./a/b/c/sitemodule.so", O_RDONLY) = -1 ENOENT (No such file or directory)
open("./a/b/c/site.py", O_RDONLY)       = -1 ENOENT (No such file or directory)
open("./a/b/c/site.pyc", O_RDONLY)      = -1 ENOENT (No such file or directory)

每个失败,它继续前进到路径中的下一个条目,以搜索要订购的模块。我的3.5解释器以这种方式查找了25个模块,从而在启动时针对每个新的152条目产生了一个递增的PYTHONPATH系统调用。

深层包装结构

深度包结构在解释器启动时不会带来任何损失,但是当我们从深度嵌套的包结构中导入时,我们确实会看到差异。作为基准,这是从d/__init__.pya/b/c目录中的PYTHONPATH的简单导入:

stat("/home/matt/a/b/c/d", {st_mode=S_IFDIR|0775, st_size=4096, ...}) = 0
stat("/home/matt/a/b/c/d/__init__.py", {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
stat("/home/matt/a/b/c/d/__init__", 0x7ffd900ba990) = -1 ENOENT (No such file or directory)
open("/home/matt/a/b/c/d/__init__.x86_64-linux-gnu.so", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/home/matt/a/b/c/d/__init__.so", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/home/matt/a/b/c/d/__init__module.so", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/home/matt/a/b/c/d/__init__.py", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
open("/home/matt/a/b/c/d/__init__.pyc", O_RDONLY) = 4
fstat(4, {st_mode=S_IFREG|0664, st_size=117, ...}) = 0
read(4, "\3\363\r\n\17\3105[c\0\0\0\0\0\0\0\0\1\0\0\0@\0\0\0s\4\0\0\0d\0"..., 4096) = 117
fstat(4, {st_mode=S_IFREG|0664, st_size=117, ...}) = 0
read(4, "", 4096)                       = 0
close(4)                                = 0
close(3)                                = 0

基本上,这是在寻找d软件包或模块。找到d/__init__.py后,将其打开,然后打开d/__init__.pyc并将内容读入内存,然后关闭两个文件。

使用深度嵌套的包结构,我们必须重复此操作3次,这对于每个目录15个系统调用来说是一件好事,总共可以再进行45个系统调用。虽然这少于通过向我们的PYTHONPATH添加路径所添加的调用次数的一半,但read调用可能比其他系统调用更耗时(或需要更多系统调用)取决于__init__.py文件的大小。

TL; DR

考虑到所有这些因素,这些差异几乎肯定不足以抵消所需解决方案的设计优势。

如果您的进程是长期运行的(例如网络应用),而不是短暂的,则尤其如此。

我们可以通过以下方式减少系统调用:

  1. 删除所有无关的PYTHONPATH条目
  2. 预先编译您的.pyc文件,以免将其写入
  3. 保持包装结构平整

我们可以通过删除py文件来大大提高性能,这样就不会与PYC文件一起读取这些文件用于调试目的……但这对我来说似乎太过分了。

希望这很有用,可能比必要的潜水要深得多。

答案 1 :(得分:2)

除非您在慢速驱动器位置上附加路径,否则不会对性能产生影响。但这可能产生的影响可以忽略不计。

通过在PYTHONPATH上添加太多位置,您最有可能遇到的问题是模块冲突,其中不同的位置具有相同的模块但版本不同。

答案 2 :(得分:2)

这是有史以来最可怕的想法。

首先,因为它使代码更难阅读和推理。等待,“ folder3”,这是从哪里来的?另外,因为如果两个包用相同的名称定义了一个子模块,那么导入时将获得的子模块取决于PYTHONPATH中的顺序。并且一旦重新排列了PYTHONPATH,以便从“ packageX”而不是“ packageY”获得“ moduleX”,则有人在“ packageX”下添加了一个“ moduleY”,这使“ moduleY”与“ packageY”不符。然后你就被搞砸了...

但是那只是不那么令人讨厌的部分...

如果您有一个使用from folder1.folder2.folder3 import foo的模块,而另一个使用from folder3 import foo,则在sys.modules中最终得到两个不同的模块对象(模块的两个实例)-以及所有定义的对象在这些模块中,它们也是重复的(两个实例,ID不同),现在您有了一个程序,只要涉及到身份测试,它就会以最不稳定的方式开始运行。而且由于异常处理依赖于身份,因此如果foo是一个异常,则取决于模块的哪个实例引发了该实例,以及哪个实例试图捕获该异常,因此该测试将成功或失败而没有可识别的模式。

祝您调试顺利...