原文: http://zengrong.net/post/2192.htm
- 本站文章除注明转载外,均为本站原创或者翻译。
- 本站文章欢迎各种形式的转载,但请18岁以上的转载者注明文章出处,尊重我的劳动,也尊重你的智商;
- 本站部分原创和翻译文章提供markdown格式源码,欢迎使用 文章源码 进行转载;
- 本文标题:main.py 和 init.py
- 本文链接: http://zengrong.net/post/2192.htm
本文基于 python 3.4 ,在 python 2.7 上也可以使用。
在昨天的文章 在 setuptools 中使用 dependency_links 里,我提到了发布hhlb工具集的工作。今天又遇到了一些具体的问题。
hhlb 的文件夹结构如下:
.
├── MANIFEST.in
├── README.rst
├── hhlb
│ ├── __init__.py
│ ├── __main__.py
│ ├── admin.py
│ ├── base.py
│ ├── bin
│ ├── build.conf
│ ├── config.py
│ ├── init.py
│ ├── res.py
│ ├── templ.py
│ └── update.py
├── requirements.txt
├── setup.py
└── test
└── testbuild.py
hhlb 是一个 python package。最初的做法,我在 hhlb 的同级目录中放了一个 build.py
文件,在这个文件中 import hhlb
这个 package ,使用其功能。
例如,下面的代码会强制初始化整个项目:
python build.py init -af
build.py
这个引导文件其实 没有存在的必要 。既然我要将 hhlb 发布成一个工具,我就应该让它能像这样被调用:
hhlb init -af
python -m hhlb init -af
第一种调用方法,就像 pip 一样。第二种方法,就像 http.server 一样。
另外,在开发过程中,还可以直接通过指定 hhlb 文件夹路径的方式来调用:
python /path/to/hhlb init -af
1. 调用顺序
为了实现上面的功能,我们先需要了解一下 python 对于 package 的调用顺序。
如果你希望 python 将一个文件夹作为 package 对待,那么这个文件夹中必须包含一个名为 __init__.py
的文件,即使它是空的。 参见: Packages
如果你需要 python 讲一个文件夹作为 package 执行,那么这个文件夹中必须包含一个名为 __main__.py
的文件,当执行 python -m hhlb
或者 python hhlb
的时候,这个文件中的代码都会被执行。参见: main — Top-level script environment
让我们做个试验。
在 hhlb/__init__.py
中写入如下内容:
print('__init__')
print('__init__.__name__', __name__)
print('__init__.__package__', __package__)
在 hhlb/__main__.py
中写入如下内容:
print('__main__')
print('__main__.__name__', __name__)
print('__main__.__package__', __package__)
执行 python hhlb
,打印如下:
-> % python hhlb
__main__
__main__.__name__ __main__
__main__.__package__
这说明,将 hhlb 当作文件夹执行的时候,对于 __main__.py
来说,变量 __package__
是一个空字符串。而 __init__.py
不会被执行。
下面看看执行 python -m hhlb
的效果:
-> % python -m hhlb
__init__
__init__.__name__ hhlb
__init__.__package__ hhlb
__main__
__main__.__name__ __main__
__main__.__package__ hhlb
当作为模块执行的时候,python 会先执行 __init__.py
,然后执行 __main__.py
。而且,前者和后者对于 __name__
变量的理解是不同的。
2. 定义一个 main()
对于一个 package 来说,既然 __init__.py
必须存在,而且当作为模块执行的时候,它会先执行,我们就应该把入口函数 main()
定义在 __init__.py
中。
当我们使用模块方式 -m
调用包的时候, __init__.py
定义了 main()
函数,然后在 __main__.py
中调用它,就能实现我们统一入口的目的。
那么继续试验。
将 hhlb/__init__.py
修改成这样:
print('__init__')
print('__init__.__name__', __name__)
print('__init__.__package__', __package__)
def main():
print('__init__.main()')
在 hhlb/__main__.py
中对 main()
进行调用:
print('__main__')
print('__main__.__name__', __name__)
print('__main__.__package__', __package__)
import hhlb
hhlb.main()
注意,在 __main__.py
中,没有必要判断 __name__
是否为 __main__
,因为无论如何调用, __name__
的值都一定是 __main__
。
执行 python -m hhlb
,调用正常:
-> % python -m hhlb
__init__
__init__.__name__ hhlb
__init__.__package__ hhlb
__main__
__main__.__name__ __main__
__main__.__package__ hhlb
__init__.main()
执行 python hhlb
,调用失败:
-> % python hhlb
__main__
__main__.__name__ __main__
__main__.__package__
Traceback (most recent call last):
File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/runpy.py", line 171, in _run_module_as_main
"__main__", mod_spec)
File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/runpy.py", line 86, in _run_code
exec(code, run_globals)
File "hhlb/__main__.py", line 7, in
import hhlb
ImportError: No module named 'hhlb'
发生了什么事?
3. sys.path
从上面的试验看出,不同的调用顺序,得到的结果完全不同。
究其原因,还是调用顺序的问题。把下面这段代码分别加入 __init__.py
和 __main__.py
即可了解原因:
import sys
print('sys.path', sys.path)
在使用 python hhlb
调用的时候, sys.path
的内容如下(省略了报错信息):
-> % python hhlb
__main__
__main__.__name__ __main__
__main__.__package__
__main__.sys.path ['hhlb', '/Library/Frameworks/Python.framework/Versions/3.4/lib/python34.zip', '/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4', '/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/plat-darwin', '/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/lib-dynload', '/Users/zrong/pythonenv/hhl/lib/python3.4/site-packages']
在使用 python -m hhlb
调用的时候, sys.path
的内容如下:
-> % python -m hhlb
__init__
__init__.__name__ hhlb
__init__.__package__ hhlb
__init__.sys.path ['', '/Library/Frameworks/Python.framework/Versions/3.4/lib/python34.zip', '/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4', '/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/plat-darwin', '/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/lib-dynload', '/Users/zrong/pythonenv/hhl/lib/python3.4/site-packages']
__main__
__main__.__name__ __main__
__main__.__package__ hhlb
__main__.sys.path ['', '/Library/Frameworks/Python.framework/Versions/3.4/lib/python34.zip', '/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4', '/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/plat-darwin', '/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/lib-dynload', '/Users/zrong/pythonenv/hhl/lib/python3.4/site-packages']
__init__.main()
可以看出, sys.path
的第一个搜索路径,一个是 hhlb
,一个是空字符串。
对于 python -m hhlb
的调用方式来说,由于 __init__.py
被事先载入,此时 python 解释器已经知道了自己就是一个 package ,因此当前路径被包含在 sys.path
中。然后再调用 __main__.py
,这时 import hhlb
这个包就毫无压力了。
而对于 python hhlb
的调用方式来说,由于 __init__.py
没有被载入,python 解释器并不知道自己正在一个 package 下面工作。默认的,python 解释器将 __main__.py
的当前路径 hhlb
加入 sys.path
中,然后在这个路径下面寻找 hhlb
这个模块。显然, hhlb 文件夹下面并没有 hhlb 这个模块,因此出错。
要理解这点,就要明白 __init__.py
是 python 解释器将当前文件夹作为 package 处理的必要条件。如果没有读取到 __init__.py
,python 就不会认为当前的文件夹是一个 package,而只是把它当作普通文件夹来处理。
4. 问题解决
既然知道了问题原因,解决的方法也就立刻浮现了。
我们只需要把当前路径加入到 sys.path
中,就能解决这个问题。
最终的 __main__.py
是这个样子的(删除了 print):
import sys
import os
if not __package__:
path = os.path.join(os.path.dirname(__file__), os.pardir)
sys.path.insert(0, path)
import hhlb
hhlb.main()
QA
- 为什么不像上面提到的那样,直接在
sys.path
前面加上一个空字符串来解决问题呢?
因为,如果不是在当前路径下调用,空字符串就没效果了:python /path/to/hhlb
。 - 为什么不写
if __package__ == ''
来判断__package__
的值呢?这样应该更准确。
这并不是偷懒。因为你可能会变态到使用这种方法调用:python hhlb/__main__.py
。而这种情况下,__package__
的值就是None
而不是''
了。