14.源文件间结构组织

予早 2024-10-05 10:24:25
Categories: Tags:

模块与包

模块,module

一个文件,包含Python源码,以.py为后缀

包,package

结构化模块,一个文件夹,包含__init__.py文件、若干.py文件和若干文件夹

module对象

模块和包被导入后为module对象,module名称可以通过__name__属性获取

模块与包的导入

模块的导入实质上是命名空间导入。

基本示例

import sys # 导入模块sys
from math import asin # 从模块math中导入asin,先导入模块math再将asin加入当前命名空间
import importlib # 导入包importlib

print(sys.modules)

细节探究

模块初始化

# 源文件组织结构
a_package
  __init__.py
  a_module_in_package.py
a_module.py
demo.py
# __init__.py
print("__init__ in a_package")
# a_module_in_package.py
print("a_module_in_package")


def a_func_in_module():
    print('a_func_in_(a_module_in_package)')
# a_module.py
print("a_module")


def a_func_in_module():
    print('a_func_in_(a_module)')


if __name__ == '__main__':
    print("__main__")
# demo.py
import a_module  # a_module
import a_package  # __init__ in a_package

a_package.a_module_in_package  # AttributeError: module 'a_package' has no attribute 'a_module_in_package'

导入控制

from xxx import *不会导入任何以单下划线作为前缀的对象,包括形如_a__a__a__

# a_module.py
_a = 0
__b = 0


def _c():
    print("_c")


def __d():
    print("__d")


def e():
    print("e")
# demo1.py
import a_module

print(a_module._a)  # 访问模块的protected成员
print(a_module.__b)
a_module._c()  # 访问模块的protected成员
a_module.__d()
a_module.e()
# demo2.py
from a_module import *

# print(_a)  # NameError: name '_a' is not defined
# print(__b)  # NameError: name '__b' is not defined
# _c()  # NameError: name '_c' is not defined
# __d()  # NameError: name '__d' is not defined
e()

from xxx import *会将非单下划线前缀的对象全部导入进而造成命名空间污染,通常会避免使用from xxx import *形式,若必须使用则使用__all__限定导出,字符串列表__all__指定模块中要导出的对象名称

# a_module.py
__all__ = ['__f__', '__g__']


def e():
    print("e")


def __f__():
    print("__f__")


__g__ = 0
# demo1.py
from a_module import *

print(__g__)
__f__()
e()  # NameError: name 'e' is not defined
# demo2.py
from a_module import __g__, __f__, __all__

print(__g__)
__f__()
print(__all__)  # ['__f__', '__g__']
e()  # NameError: name 'e' is not defined
# demo3.py
import a_module

print(a_module.__g__)
a_module.__f__()
print(a_module.__all__)  # ['__f__', '__g__']
a_module.e() # e

以Python标准库asyncio为例,下述asyncio\__init__.py片段中,子模块__all__动态构建父模块__all__

导入优先级

对于操作系统来说,一个文件夹下同时存在一个名为test的文件夹和一个名为test.txt的文件不会有命名冲突,对于Python而言,内置模块math.py、项目根目录下的包math、项目根目录下的math.py、三方包math的导入语句一样,会根据优先级导入,优先级为:内置模块 > 自定义包 > 自定义模块 > 三方安装包。

https://docs.python.org/3/tutorial/modules.html#the-module-search-path

实验一,自定义test包和test.py模块

test/_init_.py

TEST_STR = "test-package"

test.py

TEST_STR = "test-module"

main.py

from test import TEST_STR
print(f"test: {TEST_STR}")

结果为:

test: test-package

显然,导的是包

实验二,自定义math包和内置math.py模块

内置模块中有同名math的,其中有函数sqrt用以进行开方运算
math/_init_.py

def sqrt(i):
    return f"math-package {i}"

math.py

def sqrt(i):
    return f"math-module {i}"

main.py

from math import sqrt
print(f"math: {sqrt(64)}")

结果为:

math: 8.0

显然,导的是内置模块

实验三,三方模块与自定义模块或包

就用Flask做为测试,先安装flask

pip install Flask

新建一个demo项目,新建一个空flask.py或者空flask包,然后将如下代码写入main.py文件,main.py位于根目录下,此时可以引用到flask,不妨运行main.py看看打印的模块路径

import flask
print(flask)

自定义包

第一步,新建文件夹

好的,我们先new一个项目,空文件夹就行,例如新建一个叫my-wheel的项目

第二步,写核心代码

新建一个mywheel的包,包下新建文件example.py

def msg():
    return "a python lib named myWheel"

第三步,写setup.py文件

回到项目根目录,新建一个setup.py文件,写包主要是使用setuptools指明项目相关信息,以及打包相关配置,可以使用如下简单配置

from setuptools import setup, find_packages

setup(
    # 项目名称
    name="myWheel",

    # 版本
    version="1.0",

    # 作者
    author="yuzao",

    # 作者邮箱
    author_email="yuzao@xxx.com",

    # 项目描述(简单描述)
    description="A tutorial for writing a python lib",

    # 项目主页
    url="https://my-wheel.com/",

    # 分类器
    classifiers=[
        # 开发状态
        'Development Status :: 1 - Alpha',

        # 开发的目标用户
        'Intended Audience :: Developers',

        # 主题
        'Topic :: Software Development :: Build Tools',

        # 许可证信息
        'License :: OSI Approved :: MIT License',

        # 目标 Python 版本
        'Programming Language :: Python :: 3.7',
        'Programming Language :: Python :: 3.8',
        'Programming Language :: Python :: 3.9',
    ],

    # python版本要求,不满足安装失败
    python_requires='>=3.7',

    # 你要安装的包,通过 setuptools.find_packages 寻找当前目录的包
    packages=find_packages()
)

第四步,安装包

此时一个最简单的包就写完了,整体项目结构如下

在项目根目录下直接安装即可

python setup.py install

第五步,使用包

from mywheel.example import msg
print(msg())
# a python lib named myWheel

第六步,上传pypi

在pypi注册账号,并将自己的包上传,就可以使用pip进行安装了

https://www.jianshu.com/p/9fb0d69134d2

命名空间与作用域

TLDR

命名空间

命名空间是一个标识符集合,同一命名空间标识符名称必须不同,不同命名空间标识符名称可以相同,命名空间用于解决标识符命名冲突问题。函数builtins.open()与函数os.open()就是通过命名空间进行区分。

在Python中,命名空间以字典的形式实现,是标识符到对象的映射。

内置命名空间,built-in namespace

内置命名空间即builtins模块中所有定义的标识符组成的集合,在解释器启动时创建,在解释器退出时销毁。

全局命名空间,global namespace

全局命名空间即某个模块中所有定义的标识符组成的集合,在模块导入时创建,在解释器退出时销毁。

局部命名空间,local namespace

局部命名空间即函数中或类中所有定义的标识符组成的集合,在调用函数或使用类时创建,在调用完毕或使用完毕时销毁。

命名空间示例解析

以person.py文件为例:

作用域

A scope is a textual region of a Python program where a namespace is directly accessible.

作用域是可以直接访问命名空间的一段Python程序文本区域,亦即命名空间中标识符可以作用生效的一段Python代码区域。

注:python-scopes-and-namespaces中描述了四个层次的作用域,官方并未命名,本文擅自命名。

内置作用域

内置作用域即解释器一次执行期间所有加到的py文件。

全局作用域

全局作用域即某个模块所有代码区域,对应于一整个py文件。

嵌套作用域

嵌套作用域即类或函数多层嵌套结构形成的代码区域(不包含局部作用域),嵌套作用域介于全局作用域和局部作用域之间,嵌套作用域是相对于类或函数的。

局部作用域

局部作用域即函数或类本身结构形成的代码区域,局部作用域是相对于类或函数的。

作用域示例解析

以person.py文件为例:

示例

一般情况下变量由内而外依次从作用域中查找,global用于声明该变量为全局变量,需要到全局作用域查找,nonlocal用于声明该变量为非本地(也非全局)变量,需要到嵌套作用域查找。

  1. 先在局部作用域查找变量。

    a = 1
    def print_local_num():
        a = 9  # 定义局部变量a
        print(a)  # 9
        
    print_local_num()
    print(f"{a=}")  # a=1 print_local_num改变的不是全局变量a
    
  2. 本地找不到向上层查找直至全局作用域,对变量只读。

    a = 1
    def print_global_num():
        print(a)
    
    print_global_num()  # 1
    
  3. 本地找不到上层可以找到,但在本地希望修改变量的情况下只会在本地查找,无则UnboundLocalError。

    a = 1
    def get_global_num():
        print(a)  # UnboundLocalError: cannot access local variable 'a' where it is not associated with a value
        a += 9
        return a
    
    get_global_num()
    
  4. 本地找不到全局可以找到,本地希望修改可以在本地声明变量为global。

    a = 1
    def print_global_num():
        # print(a)  # SyntaxError: name 'a' is used prior to global declaration
        global a  # 声明a为全局变量,不能在声明之前使用变量a
        a = 9
        print(a)  # 9
    
    
    print_global_num()  # 9
    print(f"{a=}")  # a=9 print_global_num改变的是全局变量a
    
  5. 无论嵌套多少层,全局就是全局。

    a = 1
    
    
    def func1():
        a = 0
    
        def func2():
            global a  
            a = 9
    
        print(a)  # 0
        func2()
        print(a)  # 0
    
    
    func1()
    print(f"{a=}")  # a=9
    
  6. 如果希望使用并修改嵌套作用域中定义的变量,使用nonlocal进行声明,表示非局部(也非全局)。

    a = 1
    
    
    def func1():
        a = 0
    
        def func2():
            nonlocal a  
            a = 9
    
        print(a)  # 0
        func2()
        print(a)  # 9
    
    
    func1()
    print(f"{a=}")  # a=1
    
  7. nonlocal逐层向上寻找变量的定义,直到全局作用域之前。

    a = 1
    
    
    def func1():
        global a
        print(a)
    
        # a = 1  # 即使在此处打开该语句注释下面也会报错,因为此处并非变量定义的地方
    
        def func2():
            # nonlocal会在最近上层寻找变量定义,此处上层仅仅声明使用全局变量a,并未定义,global a语句注释掉同理
            nonlocal a  # SyntaxError: no binding for nonlocal 'a' found
            a = 9
    
        func2()
        print(a)
    
  8. 多层嵌套,所有nonlocal声明的a均会查找到a=1处定义的a。

    a = 0
    def func1():
        # nonlocal a  # SyntaxError: no binding for nonlocal 'a' found
        a = 1
        def func2():
            nonlocal a
            print(a)  # 1
            a = 2
            def func3():
                nonlocal a
                print(a)  # 2
                a = 3
                def func4():
                    nonlocal a
                    print(a)  # 3
                    a = 4
                    def func5():
                        nonlocal a
                        print(a)  # 4
                        a = 5
                        print(a)  # 5
                        ...
                    func5()
                    print(a)  # 5
                func4()
                print(a)  # 5
            func3()
            print(a)  # 5
        func2()
        print(a)  # 5
    func1()
    print(a)  # 0
    
import datetime


class Person:
    class_name = 'Person'

    def __init__(self, name, age, gender):
        self.name = name
        self.age = min(age, 18)  # min定义在bulitins模块中
        self.gender = gender
        print(locals())


def get_a_person():
    second_name = '张'

    def get_a_name():
        first_name = "一二三四五六七"[datetime.datetime.now().weekday()]
        name = second_name + first_name
        return name

    p = Person(get_a_name(), 18, '男')
    return p


def main():
    p = get_a_person()
    print(p.name)  # 张四


if __name__ == '__main__':
    main()

通过循环导入深入理解导包机制

经典场景

main.py

import xxx_service

xxx_service.py

print("xxx_start")

from yyy_service import print_yyy


def print_xxx():
    print('xxx')


def print_xxx_and_yyy():
    print_xxx()
    print_yyy()


print("xxx_end")

yyy_service.py

print("yyy_start")
from xxx_service import print_xxx


def print_yyy():
    print('yyy')


def print_yyy_and_xxx():
    print_yyy()
    print_xxx()


print("yyy_end")

执行python test.py结果为:

ImportError: cannot import name 'print_yyy' from partially initialized module 'yyy_service' (most likely due to a circular import)
  1. 首先执行test.py文件,需要导入xxx_service
  2. 然后进入xxx_service.py
    1. 此时xxx_service模块已经初始化成功,但此时该模块为空
    2. 首先打印xxx_start
    3. 需要从xxx_service导入print_xxx
  3. 然后进入yyy_service.py
    1. 此时yyy_service模块已初始化成功,但此时该模块为空
    2. 首先打印yyy_start
    3. 需要从xxx_service导入print_xxx,由于xxx_service已被导入并初始化但此时模块为空,故导入错误,并提示可能由于循环导入造成异常

尝试修正

从程序角度,只需保证from xxx_service import print_xxx执行时def print_xxx()已经被执行即可。

main.py

import xxx_service

xxx_service.py

print("xxx_start")


def print_xxx():
    print('xxx')


from yyy_service import print_yyy


def print_xxx_and_yyy():
    print_xxx()
    print_yyy()


print("xxx_end")

yyy_service.py

print("yyy_start")
from xxx_service import print_xxx


def print_yyy():
    print('yyy')


def print_yyy_and_xxx():
    print_yyy()
    print_xxx()


print("yyy_end")

当然,实际业务场景中,重构代码,将耦合部分抽出是上策

加深理解

xxx_service.py

print("xxx_start")


def print_xxx_start():
    print('xxx_start')


from yyy_service import print_yyy_end


def print_xxx_end():
    print('xxx_end')


print("xxx_end")

yyy_service.py

print("yyy_start")


def print_yyy_start():
    print('yyy_start')


from xxx_service import print_xxx_start


def print_yyy_end():
    print('yyy_end')


print("yyy_end")

若此时分别执行如下两条命令,结果如何:

揭晓答案

python xxx_service.py

xxx_start
yyy_start
xxx_start
Traceback (most recent call last):
  ...
ImportError: cannot import name 'print_yyy_end' from partially initialized module 'yyy_service' (most likely due to a circular import)

python yyy_service.py

yyy_start
xxx_start
yyy_start
yyy_end
xxx_end
yyy_end

原因在于python xxx.py会已__main__作为模块名执行xxx.py中的代码,故随后从xxx.py导入函数还需要重新导入模块xxx