MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Python创建自定义模块与包

2023-07-283.9k 阅读

Python创建自定义模块

模块的基本概念

在Python中,模块是一种组织代码的方式,它将相关的代码封装在一个单元中,便于复用和管理。一个Python模块本质上就是一个包含Python定义和语句的.py文件。例如,你创建一个名为example.py的文件,这个文件就是一个模块。模块可以包含函数、类、变量等各种Python对象的定义。

创建简单模块

  1. 创建模块文件 首先,我们在一个项目目录中创建一个新的Python文件,例如命名为math_operations.py。在这个文件中,我们定义一些简单的数学运算函数。

    def add(a, b):
        return a + b
    
    
    def subtract(a, b):
        return a - b
    
    
    def multiply(a, b):
        return a * b
    
    
    def divide(a, b):
        if b == 0:
            raise ValueError('除数不能为零')
        return a / b
    

    这里我们定义了四个基本的数学运算函数:加法add、减法subtract、乘法multiply和除法divide

  2. 使用模块 当我们有了这个模块后,就可以在其他Python文件中使用它。假设我们有另一个文件main.py,它位于与math_operations.py相同的目录下,我们可以这样导入并使用math_operations模块:

    import math_operations
    
    result_add = math_operations.add(5, 3)
    result_subtract = math_operations.subtract(5, 3)
    result_multiply = math_operations.multiply(5, 3)
    try:
        result_divide = math_operations.divide(5, 3)
    except ValueError as ve:
        print(ve)
    
    print(f'加法结果: {result_add}')
    print(f'减法结果: {result_subtract}')
    print(f'乘法结果: {result_multiply}')
    if 'result_divide' in locals():
        print(f'除法结果: {result_divide}')
    

    在上述代码中,通过import math_operations语句导入了我们自定义的模块,然后使用模块名.函数名的方式调用模块中的函数。

模块的导入方式

  1. import语句 这是最基本的导入方式,如我们之前例子中的import math_operations。这种方式将整个模块导入,在使用模块中的函数或变量时,需要使用模块名.对象名的形式。优点是明确知道对象来自哪个模块,代码可读性强,尤其在处理大型项目中多个模块可能有同名对象的情况时很有用。

  2. from...import语句 这种方式可以从模块中导入特定的对象。例如,我们可以从math_operations.py模块中只导入addmultiply函数:

    from math_operations import add, multiply
    
    result_add = add(5, 3)
    result_multiply = multiply(5, 3)
    
    print(f'加法结果: {result_add}')
    print(f'乘法结果: {result_multiply}')
    

    这种方式在使用时不需要再写模块名,直接使用对象名即可。它的优点是代码更简洁,在导入的对象较少且模块名较长时很方便。但缺点是如果多个模块中有同名对象,可能会导致命名冲突,难以追踪对象的来源。

  3. from...import *语句 这种方式会导入模块中的所有公共对象(没有以下划线_开头的对象)。例如:

    from math_operations import *
    
    result_add = add(5, 3)
    result_subtract = subtract(5, 3)
    result_multiply = multiply(5, 3)
    try:
        result_divide = divide(5, 3)
    except ValueError as ve:
        print(ve)
    
    print(f'加法结果: {result_add}')
    print(f'减法结果: {result_subtract}')
    print(f'乘法结果: {result_multiply}')
    if 'result_divide' in locals():
        print(f'除法结果: {result_divide}')
    

    虽然这种方式看起来很方便,但是它可能会导致命名空间污染,尤其是在大型项目中,不同模块可能有同名的对象,很难确定对象的具体来源。所以一般不推荐在大型项目中使用,只在一些小型的、临时的脚本中可以酌情使用。

模块的__name__属性

每个Python模块都有一个__name__属性,它的值取决于模块是如何被使用的。

  1. 作为主程序运行 当一个Python文件直接作为主程序运行时,它的__name__属性值为__main__。例如,我们在math_operations.py文件末尾添加以下代码:

    def add(a, b):
        return a + b
    
    
    def subtract(a, b):
        return a - b
    
    
    def multiply(a, b):
        return a * b
    
    
    def divide(a, b):
        if b == 0:
            raise ValueError('除数不能为零')
        return a / b
    
    
    if __name__ == '__main__':
        print('该模块作为主程序运行')
        result_add = add(5, 3)
        print(f'加法结果: {result_add}')
    

    当我们直接运行math_operations.py时,会输出该模块作为主程序运行以及加法结果。这是因为if __name__ == '__main__':这个条件成立,模块被当作主程序执行。

  2. 被其他模块导入math_operations.py被其他模块导入时,__name__属性的值就是模块名math_operations。例如,在main.py中导入math_operations模块时,math_operations模块中的if __name__ == '__main__':条件不成立,相关代码不会执行。这使得我们可以在模块中编写一些测试代码,当模块作为主程序运行时执行这些测试代码,而当模块被其他模块导入时,这些测试代码不会干扰正常的使用。

模块的搜索路径

当我们使用import语句导入一个模块时,Python会按照一定的顺序在一系列路径中搜索模块。

  1. 当前目录 Python首先会在当前工作目录中搜索模块。例如,如果main.pymath_operations.py在同一目录下,import math_operations就能找到该模块。

  2. Python标准库路径 如果在当前目录没有找到,Python会在标准库路径中搜索。这些路径是Python安装时配置好的,包含了Python自带的标准模块,例如ossys等模块就是在标准库路径中找到的。

  3. 环境变量PYTHONPATH指定的路径 PYTHONPATH是一个环境变量,它可以包含多个目录路径,Python会在这些路径中搜索模块。例如,在Linux或macOS系统中,可以通过以下方式设置PYTHONPATH环境变量:

    export PYTHONPATH=$PYTHONPATH:/path/to/your/modules
    

    在Windows系统中,可以通过系统环境变量设置界面添加PYTHONPATH变量,并设置其值为模块所在目录路径。

  4. 第三方库安装路径 当使用pip等工具安装第三方库时,这些库通常会被安装到Python的第三方库安装路径下。Python也会在这些路径中搜索模块。例如,安装numpy库后,import numpy就能找到该库模块,因为numpy被安装到了相应的第三方库路径中。

Python创建自定义包

包的基本概念

包是一种更高级的组织Python模块的方式,它本质上是一个包含多个模块的目录,并且这个目录下必须包含一个特殊的文件__init__.py(在Python 3.3及以上版本,这个文件可以为空,但存在它可以更好地标识这是一个包)。包可以有子包,形成层次结构,便于管理大型项目中的众多模块。

创建简单包

  1. 创建包目录结构 假设我们要创建一个名为my_package的包,用于处理图形相关的操作。我们创建如下目录结构:

    my_package/
        __init__.py
        shapes/
            __init__.py
            circle.py
            rectangle.py
    

    在这个结构中,my_package是顶级包目录,其中的__init__.py文件标识它是一个包。shapesmy_package的子包,同样包含__init__.py文件。circle.pyrectangle.pyshapes子包中的模块。

  2. 编写模块内容circle.py模块中,我们定义一个计算圆面积的函数:

    import math
    
    
    def circle_area(radius):
        return math.pi * radius ** 2
    

    rectangle.py模块中,我们定义一个计算矩形面积的函数:

    def rectangle_area(length, width):
        return length * width
    

导入包中的模块

  1. 从包外导入 假设我们有一个main.py文件位于my_package包的上级目录,我们可以这样导入my_package包中的模块:

    from my_package.shapes.circle import circle_area
    from my_package.shapes.rectangle import rectangle_area
    
    circle_result = circle_area(5)
    rectangle_result = rectangle_area(4, 6)
    
    print(f'圆的面积: {circle_result}')
    print(f'矩形的面积: {rectangle_result}')
    

    在上述代码中,使用from包名.子包名.模块名 import 对象名的方式导入包中的模块对象。

  2. 在包内导入 有时候,包内的模块可能需要相互导入。例如,我们在my_package/shapes/__init__.py文件中,可以通过相对导入的方式导入子包中的模块。假设我们想在__init__.py文件中提供一个统一的接口来获取图形面积,我们可以这样写:

    from .circle import circle_area
    from .rectangle import rectangle_area
    

    这里的from .表示相对当前包的导入,..表示相对上级包的导入。这样,在其他模块导入my_package.shapes包时,就可以直接使用my_package.shapes.circle_areamy_package.shapes.rectangle_area,而不需要再深入到具体的模块名。

__init__.py文件的作用

  1. 标识包 如前所述,__init__.py文件最基本的作用是标识它所在的目录是一个Python包。即使文件为空,只要存在这个文件,Python就会将该目录视为一个包。

  2. 初始化包 __init__.py文件可以包含包的初始化代码。例如,我们可以在my_package/__init__.py文件中定义一些全局变量或执行一些初始化操作:

    # my_package/__init__.py
    package_version = '1.0'
    print(f'初始化 my_package,版本号: {package_version}')
    

    当其他模块导入my_package包时,__init__.py中的代码会被执行,这里会输出初始化信息并定义了一个包级别的变量package_version

  3. 控制包的导入行为 我们可以在__init__.py文件中使用__all__变量来控制from包名 import *这种导入方式导入的内容。例如,在my_package/shapes/__init__.py文件中:

    from .circle import circle_area
    from .rectangle import rectangle_area
    
    __all__ = ['circle_area']
    

    当使用from my_package.shapes import *时,只会导入__all__列表中指定的circle_area,而不会导入rectangle_area。这有助于控制包的公共接口,避免不必要的对象被导入。

包的相对导入

  1. 相对导入语法 相对导入使用点号(.)来表示相对位置。单个点号(.)表示当前包,两个点号(..)表示上级包。例如,在my_package/shapes/circle.py模块中,如果我们想从rectangle.py模块导入rectangle_area函数,可以这样写:

    from .rectangle import rectangle_area
    

    这里的from .表示从当前包(my_package/shapes)内导入。

  2. 相对导入的适用场景 相对导入在包内模块之间的交互非常有用,尤其是在大型包结构中,模块之间有复杂的依赖关系时。它使得包内模块的导入路径更简洁,并且与包的实际目录结构紧密相关。例如,如果我们对包的目录结构进行了重构,只要包内的相对关系不变,相对导入的代码不需要修改。但是需要注意的是,相对导入只能在包内使用,不能在顶层脚本或非包结构的模块中使用。

发布和安装自定义包

  1. 创建setup.py文件 要发布和安装自定义包,我们需要创建一个setup.py文件。在my_package包的上级目录中创建setup.py文件,内容如下:

    from setuptools import setup, find_packages
    
    setup(
        name='my_package',
        version='1.0',
        packages=find_packages()
    )
    

    这里使用setuptools库来配置包的元数据。name指定包的名称,version指定版本号,packages=find_packages()会自动查找当前目录下的所有包结构。

  2. 构建包 在命令行中进入包含setup.py文件的目录,执行以下命令构建包:

    python setup.py sdist bdist_wheel
    

    这会在当前目录下生成dist目录,其中包含源分发包(.tar.gz格式)和wheel包(.whl格式)。

  3. 安装包 可以使用pip来安装构建好的包。例如,安装源分发包:

    pip install dist/my_package - 1.0.tar.gz
    

    或者安装wheel包:

    pip install dist/my_package - 1.0-py3-none-any.whl
    

    安装后,就可以在其他Python项目中像使用第三方包一样导入和使用my_package包了。

通过以上对Python自定义模块和包的创建、导入、管理等方面的详细介绍,你可以更好地组织和管理自己的Python代码,无论是小型脚本还是大型项目,都能通过合理的模块和包结构提高代码的可维护性和复用性。在实际项目中,根据功能和需求设计合适的模块和包结构是非常重要的,它有助于代码的清晰性和扩展性。同时,了解模块搜索路径、__name__属性等底层概念,能帮助你更好地理解Python的导入机制,避免在开发过程中遇到一些与导入相关的问题。在包的管理方面,掌握__init__.py文件的作用、相对导入以及包的发布和安装等知识,能让你将自己开发的包分享给他人使用,或者在不同项目中复用,提升开发效率和代码质量。