Python 开发指南:工程化、模块 ( module )、引入依赖

2023年 7月 12日 98.8k 0

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 模块都有两种用途:

  • 作为组件为其它模块提供功能,包括变量,函数,类定义。
  • 作为脚本送入 Python 解释器执行。
  • 当一个模块被引用时,解释器会从头到尾执行内部所有的代码行以获取变量,函数,类的定义。但是,该模块仅作为组件被使用时,其作为脚本部分的逻辑一般是不需要被执行的。为了将这两部分职责区分开,可以在模块文件的顶级声明中编写一个特殊的条件分支:

    # some definations
    def func(): pass
    var = 10
    
    print("as module")
    
    if __name__ == '__main__':
    	print("as script")
        # ...
    

    该分支内的代码块 只有在该模块作为程序入口时 才会执行,即同时打印 as moduleas 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__.pysetup.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 会首先尝试从环境中引入 numpypytzdateutil 包,并在导入失败时抛出 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"])

    作者:花花子 来源:稀土掘金

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论