执行python脚本比较常见的一种做法是python your_script.py,在应用中,还有一种写法是指定-m参数,例如:python -m module_name,本文来讨论一下它们的异同。

1、介绍

首先,通过在控制台执行python -h来看一下-m参数的介绍:

python 调用MQClientAPIImpl python调用.m文件_git

run library module as a script (terminates option list)

将包中的模块当做脚本来执行,并且会终止其他选项(即指定了-m参数后,其他选项失效,也就是说它具有非常高的优先级)。

2、示例

2.1 完整示例代码下载

本文所有的示例代码都上传到代码仓库中,如果读者想复现一下的话,可以先将其克隆到本地,然后按照提示切换到不同的commit版本从而实现对应的功能。

获取代码:

git clone git@codechina.csdn.net:SunJW_2017/use-python-dash-m.git

进入工作目录:

cd use-python-dash-m

至此,读者已经获取到了本文用到的所有代码,只需要在执行示例代码时按照说明检出至指定的提交即可。

2.2 基本用法

在工作目录执行git checkout 2cbd4ec1检出本示例需要的代码

检出代码后,其结构应该如下:

# 结构
use-python-dash-m
├── my_pkg
│   ├── __init__.py
│   └── inner.py
└── outer.py

然后,在工作目录执行python -m my_pkg.inner,控制台应该打印以下内容:

This is my_pkg
This is an outer function
This is an inner function

但如果不指定-m参数:python my_pkg/inner.py,控制台应该会报以下错误:

Traceback (most recent call last):
  File "my_pkg/inner.py", line 1, in <module>
    from outer import outer_fn
ModuleNotFoundError: No module named 'outer'

错误信息很明显,解释器找不到模块outer。如果你熟悉python导入模块的原理,那么大概会有以下感觉:不指定-m参数时,解释器搜索包的目录从当前工作目录下移了一级,即test/my_pkg,因此找不到outer;而指定-m参数时,解释器搜索包的目录就是当前执行命令的目录。

我们验证一下上述想法。

2.3 验证

在工作目录执行git checkout 02dc3c43检出本示例需要的代码

我对inner.py做了修改,在执行python my_pkg/inner.py时,由于由于第三行才会触发错误,因此,可以在控制台打印以下内容(只取错误之前的内容):

current work directoy: /Users/<myname>/Desktop/use-python-dash-m
module search paths: ['/Users/<myname>/Desktop/use-python-dash-m/my_pkg', '/Users/<myname>/opt/anaconda3/lib/python37.zip', '/Users/<myname>/opt/anaconda3/lib/python3.7', '/Users/<myname>/opt/anaconda3/lib/python3.7/lib-dynload', '/Users/<myname>/opt/anaconda3/lib/python3.7/site-packages', '/Users/<myname>/opt/anaconda3/lib/python3.7/site-packages/aeosa']

由此可知,尽管当前的工作目录仍然是use-python-dash-m,但增加的解释器导入包的搜索目录(sys.path第一个值)却下移了一级,自然就找不到outer了。

接下来执行python -m my_pkg.inner,控制台会打印以下内容:

This is my_pkg
current work directoy: /Users/<myname>/Desktop/use-python-dash-m
module search paths: ['/Users/<myname>/Desktop/use-python-dash-m', '/Users/<myname>/opt/anaconda3/lib/python37.zip', '/Users/<myname>/opt/anaconda3/lib/python3.7', '/Users/<myname>/opt/anaconda3/lib/python3.7/lib-dynload', '/Users/<myname>/opt/anaconda3/lib/python3.7/site-packages', '/Users/<myname>/opt/anaconda3/lib/python3.7/site-packages/aeosa']
This is an outer function
This is an inner function

从第三行打印内容可以知道,新增的包搜索目录为当前工作目录,因此可以正常运行。

3、运行机制

简单来说,-m参数的运行机制就是:从当前的sys.path搜索指定的包或模块,然后执行它。如果是模块,则将该模块当做__main__.py来执行,且不需要写.py的后缀;如果为包,则该包目录下必须有一个__main__.py文件。

在这里,模块指的就是一个python脚本,而指的是包含普通python脚本以及特殊文件__init__.py(有可能还包含其他包)的目录。
这种说法的准确性还有待商榷,不过对于理解本文的内容有帮助。

回到2.2节中的例子,在执行python -m my_pkg.inner时,由于inner本身就是一个模块,因此就直接执行它,需要注意的是,在执行inner.py里面的内容之前,先会执行inner.py所在包的__init__.py的内容,这也是控制台先打印“This is my_pkg”的原因。

那如果模块所在的包存在嵌套的情况,这是的执行逻辑是什么呢?

3.1 包嵌套时的执行逻辑

在工作目录执行git checkout 9927e816检出本示例需要的代码

这时代码目录应该如下:

use-python-dash-m
├── my_pkg
│   ├── __init__.py
│   ├── inner.py
│   └── inner_pkg
│       ├── __init__.py
│       └── inner.py
└── outer.py

执行python -m my_pkg.inner_pkg.inner,控制台打印的内容应该如下:

This is my_pkg
This is inner_pkg
This is an outer function
This is an inner function in inner_pkg

由此可见,如果要执行的模块位于嵌套的包内,那么解释器会从外向内逐层执行每个包的__init__.py文件中的内容。

了解了通过-m运行模块时的原理后,再来看如果通过-m运行包时的机制。

前面说,如果需要执行的是一个包,那么该包的一级目录下必须要有一个__main__.py文件才行。接着3.1中例子的代码,执行python -m my_pkg.inner_pkg,控制台应该打印以下内容:

This is my_pkg
This is inner_pkg
/Users/<myname>/opt/anaconda3/bin/python: No module named my_pkg.inner_pkg.__main__; 'my_pkg.inner_pkg' is a package and cannot be directly executed

这些内容足够的明确了:由于执行的一个包而不是模块,且该包中没有一个名为__main__.py的模块,因此,解释器无法直接执行。这也就意味着,执行一个包时,其实是要执行该包的一级目录下的__main__.py模块。

3.2 执行一个包

在工作目录执行git checkout b82f505f检出本示例需要的代码

这时再执行python -m my_pkg.inner_pkg,控制台应该打印以下内容:

This is my_pkg
This is inner_pkg
I'm a main function in package inner_pkg

4、用处

关于-m参数的用处,参考内容2中列出了一些非常棒的例子,这里不再重复,只列举一个我工作中遇到的问题。

问题的背景是需要用多进程来将一项耗时任务分配到子进程中来执行,而主进程立即返回。

耗时任务的入口是一个可以接收命令行参数的python脚本,且位于某个包中——这样它就成了一个模块。其结构大概如下:

my_project
├── my_pkg
│   ├── __init__.py
│   └── long_time_task_script.py
└── config.py

而且这个耗时脚本中还需要读取config.py中的某些参数,如果不使用-m命令,在my_project目录下执行python ./my_pkg/long_time_task_script.py,那么它将无法找到config.py这个模块(原因如上所述)(虽然我通过向Subprocess.Popen方法中的env参数新增PYTHONPATH变量也能解决路径问题,但我总觉得这不够pythonic)。

这时,-m参数就排上用场了,可以在my_project目录下执行python -m my_pkg.long_time_task_script优雅地解决这个问题。

5、参考内容

  1. Python -m 参数解析
  2. 关于 Python 命令中的 -m 参数