Python 中,一个单独的 *.py
文件被称之为一个模块 ( module );多个模块组织成了一个包 ( package );一个庞大的项目由各种层次的包组成。有一点值得注意的是,父包并不会自动地导入子包的模块。
引入依赖
import
既可以导入包,也可以导入包内具体的 模块。比如,先通过 conda 在项目环境中安装 numpy
,然后再通过 import
将它导入到当前的脚本。
# 可以同时引入多个包,多个包使用逗号相隔,下同。
import numpy
# 也可以通过 . 运算符单独导入包下的某个具体的模块。
# 这里仅作演示,我们事实上只导入 numpy 就足够了。
import numpy.core.multiarray
# 通过固定数据类型的方式,numpy 可以申请一块整齐且紧凑的连续内存空间保存数据。
arr = numpy.array([1, 2, 3], dtype=float)
print(*arr)
导入的包或模块可以通过 as
关键字起别名。比如:
import numpy as np
arr = np.array([1, 2, 3], dtype=float)
print(*arr)
Python 还提供了一种 from .. import
语句,它支持更细粒度的导入。有两种用法:
一:如果 from
后面是一个包,则可以 import
该包下的模块。
from pkg import module1 as m1
m1.var1 # ok
var1 # error
二:如果 from
后面是一个模块,则可以 import
该模块下定义的变量,函数,类定义。这些被导入的内容可以直接使用,不需要再指定模块名。
from pkg.module1 import var1 as v1, var2 as v2, func1 as f1,
v1 # ok
Script or Module
任何一个 Python 模块都有两种用途:
当一个模块被引用时,解释器会从头到尾执行内部所有的代码行以获取变量,函数,类的定义。但是,该模块仅作为组件被使用时,其作为脚本部分的逻辑一般是不需要被执行的。为了将这两部分职责区分开,可以在模块文件的顶级声明中编写一个特殊的条件分支:
# some definations
def func(): pass
var = 10
print("as module")
if __name__ == '__main__':
print("as script")
# ...
该分支内的代码块 只有在该模块作为程序入口时 才会执行,即同时打印 as module
和 as script
。当它仅作为组件被其它模块引用时,解释器只会打印 as module
。
目录结构
一个大型的 Python 工程通常由多个包构成。比如,一个简单的 Python 工程可以包含以下层次:
Project/
|
|-- project/
| |-- test/
| | |-- __init__.py
| | |-- test_main.py
| |
| |-- __init__.py
| |-- main.py
|
|-- setup.py
|-- requirements.txt
|-- README.md
作为项目程序入口的模块一般命名为 main.py
,但这并不是必须的。比如有些数据处理项目用多个可执行模块来提供不同的功能。这里简单介绍 __init__.py
和 setup.py
这两个模块。
_init_.py
每个工程的子目录下面都可以创建一个特殊的模块:__init__.py
,Python 会将任何包含这个模块的目录识别为包 Package。同时,当其它模块引入这个包时,Python 会首先加载 __init__.py
声明的逻辑。
所有在 __init__.py
声明的,或者是通过 from .. import
语句块引入的变量,函数,类定义,都将被纳入该包的定义域下。任何引用了此包的模块都将自动地获得它们。换句话说,如果项目开发者在声明包 __init__.py
文件时就声明了对子包模块的导入,那么对于项目用户而言,他只需要单独导入这一个包即可。
除了提供数据定义以外,项目开发者通常还会在这里编写一些初始化,或者是功能验证等前置工作。如:
# Let users know if they're missing any of our hard dependencies
hard_dependencies = ("numpy", "pytz", "dateutil")
missing_dependencies = []
for dependency in hard_dependencies:
try:
__import__(dependency)
except ImportError as e:
missing_dependencies.append(f"{dependency}: {e}")
if missing_dependencies:
raise ImportError(
"Unable to import required dependencies:\n" + "\n".join(missing_dependencies)
)
del hard_dependencies, dependency, missing_dependencies
这是来自 pandas/__init__.py
模块的一部分代码。在初始化过程中,pandas
会首先尝试从环境中引入 numpy
,pytz
,dateutil
包,并在导入失败时抛出 ImportError
异常。
setup.py*
如果你并不专门从事 Python 工具库的开发,而是聚焦于构建可运行的应用或数据处理任务,则此模块就不是必须的。
前文曾介绍过如何使用 pip 将项目的依赖导出到 requirements.txt
文件,这份文件相当于声明了能够保证项目正常运行的环境 env
。假设我们编写的是可直接运行的 Python 项目,并将它开源到了远程的 Git 仓库上,那么这份文件将有助于其它 clone 项目的用户快速复制出项目的启动环境。
当然,我们开发的也可以是更加基础的 Python 底层库,而其它开发者可以通过 pip install pkg
命令将我们的工具包作为依赖项引入并安装。
这时候就需要一个名为 setuptools
的工具将项目进行打包,它也被 conda 预装好了。打包时需携带版本号,开源协议,项目主页,开发者邮箱,第三方依赖 ( 重要 ) 等元数据信息,而我们只需要在 setup.py
( 另一个途径是 setup.cfg
) 中按照格式进行设置即可。
下面的 setup.py
是一份简单的演示。工具包依赖的其他库可以通过 install_requires
参数进行配置,pip 在安装此工具包时会通过读取该项配置解决依赖问题。
from setuptools import setup
setup(
name='pythonProject3',
version='1.0.0',
packages=['project', 'project.core'],
author='lijunhu',
author_email='junhuwiki@163.com',
description='a sample',
# 如果你确信依赖是向后兼容的,也可以设置成 'numpy>=1.22.0'
install_requires=[
'numpy==1.22.0'
]
)
在 PyCharm UI 上方的 Tools > Create setup.py / Run setup task
可以快速生成一份 setup 模板,并选择打包方式,比如 .egg
,.whl
( 可被 pip 直接安装 ),或者是 Windows 平台可运行的 .rmi
,乃至 Linux 平台的 *.rpm
。
打包好的文件会生成在项目根路径下的 /dist
目录。取它用户可以通过 pip install your_project.whi
将此工具包安装到环境下,同时 pip 会自行下载 1.22.0
版本的 numpy
并安装到环境中。
可以参考以下连接:
setup.py 与 requirements.txt 区别 - SegmentFault 思否
Python 中的 requirements.txt 与 setup.py_deephub的博客-CSDN博客_requirements.txt
花了两天,终于把 Python 的 setup.py 给整明白了 - 知乎 (zhihu.com)
Python的打包工具(setup.py)实战篇 - 尹正杰 - 博客园 (cnblogs.com)
命令行参数解析
工程项目的入口程序通常会附带配置项,用户可以在程序启动之前传入必要的参数,或者是做额外的配置。比如:
python main.py --address "hadoop1" "hadoop2" "hadoop3" --num 3
Python 在标准库中内置了 argparse 库,它可以帮助开发者进行命令行参数解析。在脚本的内部,我们仅仅需要实例化一个 ArgumentParser 解析器对象,然后将设置的命令行参数添加到该对象内部即可。约定上,以 -
为前缀的标识符被识别为配置项,而后面的参数为该配置项的值,见下方的实现:
import argparse
parser = argparse.ArgumentParser(description="config your cluster")
parser.add_argument("--address", "-a", help="your server ip", nargs="+", type=str,required=True)
parser.add_argument("--num", "-n", help="number of the slaves", type=int, default="3")
这里做一些简要说明:
help
参数给出该配置项相关的提示信息。type
指定参数值的类型。default
指定用户未指定配置项时的默认值。nargs="+"
表示该配置项至少接收一个或更多的值。该配置项的值会被收集到一个列表 list 内部。required=True
表示该项配置是 必须的。这里仅罗列出常用的命令行参数配置。完整内容可以参考官网:argparse — Parser for command-line options, arguments and sub-commands — Python 3.10.5 documentation 以及:argparse模块用法实例详解 - 知乎 (zhihu.com)
通过调用解析器的 parse_args()
方法来提取用户从外部传入的参数。出于测试的目的,这里手动以数组形式传入命令行参数:
# conf = parser.parse_args()
conf = parser.parse_args(["-a", "hadoop1", "hadoop2", "hadoop3", "--num", "3"])
解析成功的 conf
为 Namespace 类型的命名空间。直接通过 conf.{arg}
即可从中提取出解析出的配置项值。如:
print(conf.num) # 3
print(conf.address) # ['hadoop1', 'hadoop2', 'hadoop3']
这一手段是基于重写
__getattr__()
魔法函数的属性动态注入来实现的,见后文的元编程技术。
argparse 预留了 -h
和 --help
配置项,它可以根据开发者的设置打印出帮助信息。比如:
"""
usage: main.py [-h] --address ADDRESS [ADDRESS ...] [--num NUM]
config your cluster
optional arguments:
-h, --help show this help message and exit
--address ADDRESS [ADDRESS ...], -a ADDRESS [ADDRESS ...]
your server ip
--num NUM, -n NUM number of the slaves
"""
parser.parse_args(["-h"])
作者:花花子 来源:稀土掘金