模块与包
模块,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'
导模块会执行
.py文件,导包会执行包下__init__.py文件顶层代码环境判断
if __name__ == '__main__'不会执行a_module_in_package包中存在模块a_module_in_package但尝试导入失败,是由于从a_module_in_package导入相当于从__init__.py导入,通常会将子模块在父模块__init__.py中进行导入,如内置包asyncio
导入控制
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__指定模块中要导出的对象名称
__all__只对from xxx import *或from xxx import yyy, zzz, ...生效,且可指定导出单下划线前缀的对象__all__不限制import xxx然后xxx.yyy的形式- 根据PEP8
__all__应该定义在import语句与其他成员(如常量、函数、类等)两者之间
# 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
- Python中根据代码块划分命名空间和作用域,代码块有且仅有三种,模块、类、函数或方法。
- 命名空间是标识符到对象的映射,作用域是命名空间中标识符可以作用生效的一段Python代码区域。
- 命名空间可以分为内置命名空间、全局命名空间、局部命名空间,作用域粗略可以分为全局作用域、局部作用域,详细可以分为内置作用域、全局作用域、嵌套作用域、局部作用域。
- 命名空间是相对的,一定先有某个代码块中的位置,然后才能针对该位置讨论局部命名空间和全局命名空间,内置命名空间是绝对的。局部命名空间是位置所属代码块的命名空间,全局命名空间是该位置所属模块的命名空间
- 命名空间之间不交叉且是嵌套结构,作用域之间交叉且是线性结构。
- 查找某个作用域使用的标识符时,以模块、类、函数或方法为界限自内而外(从小到大)一层层查找即可。
命名空间
命名空间是一个标识符集合,同一命名空间标识符名称必须不同,不同命名空间标识符名称可以相同,命名空间用于解决标识符命名冲突问题。函数builtins.open()与函数os.open()就是通过命名空间进行区分。
在Python中,命名空间以字典的形式实现,是标识符到对象的映射。
内置命名空间,built-in namespace
内置命名空间即builtins模块中所有定义的标识符组成的集合,在解释器启动时创建,在解释器退出时销毁。
全局命名空间,global namespace
全局命名空间即某个模块中所有定义的标识符组成的集合,在模块导入时创建,在解释器退出时销毁。
局部命名空间,local namespace
局部命名空间即函数中或类中所有定义的标识符组成的集合,在调用函数或使用类时创建,在调用完毕或使用完毕时销毁。
命名空间示例解析
以person.py文件为例:
- 各个命名空间之间是不交叉的。
- 内置命名空间由bulitins模块中定义的标识符构成。
- 全局命名空间person由下方紫色区域中定义的标识符构成,特别注意,
if __name__ == '__main__'块中定义的标识符属于全局命名空间person,习惯上,会将if __name__ == '__main__'块下逻辑抽出为main函数以将全局标识符转换为局部标识符来减少命名空间污染。 - person模块中使用了datetime中定义的的标识符。
Person类构成局部命名空间,由黄色部分定义的标识符组成。Person类中__init__方法构成局部命名空间,由红色部分定义的标识符组成。get_a_person函数构成局部命名空间,由蓝色部分定义的标识符组成。get_a_name函数构成局部命名空间,由灰色部分定义的标识符组成。mian函数构成局部命名空间,由绿色部分定义的标识符构成。- 特别注意,上述
定义的标识符指一般本地标识符的定义,例如蓝色部分second_name或绿色部分p,红色部分中self.name并非局部命名空间的标识符,但函数参数name属于该局部命名空间。 - 图中以代码文本展示命名空间中标识符源于何处,谨记命名空间是标识符集合。
作用域
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文件为例:
- 各个作用域之间是交叉的。
- 内置作用域,person.py文件中所有文本区域均可以访问内置命名空间的标识符,对应图中红色扫过的行,但内置作用域不仅仅是person.py文件,而是包含一次解释器启动期间所有加载到的py文件。
- 全局作用域,person模块对应的person.py中所有文件区域均可以访问内置命名空间的标识符和全局命名空间的标识符,对应图中的紫色扫过的行。
- 嵌套作用域,
get_a_name为嵌套在get_a_person内部的函数,则get_a_name函数向外一层为最近的一个嵌套作用域,对应蓝色扫过的行,同理,绿色扫过的行构成的作用域为函数__init__的嵌套作用域,嵌套作用域可以访问内置命名空间的标识符、全局命名空间的标识符和嵌套作用域更上层嵌套作用域可以访问的局部命名空间中的标识符(例如最近嵌套作用域的最近嵌套作用域,或者再嵌套若干层),当讨论的局部作用域确定,则其与全局作用域之间的作用域均为嵌套作用域。 - 局部作用域,绿色、黑色、蓝色、橙色和黄色扫过的行均为局部作用域,可以访问局部命名空间的标识符、全局命名空间的标识符、内置命名空间的标识符,局部作用域是相对的,讨论黑色扫过的行中的标识符,以黑色扫过的行为局部作用域,则嵌套作用域为绿色扫过的行,讨论绿色扫过的行中的标识符,以绿色扫过的行为局部作用域,无嵌套作用域,向外一层即为全局作用域。
- 讨论范围是默认以当前模块为全局,内置作用域和全局作用域是绝对的,嵌套作用域和局部作用域是相对的,当讨论的函数或类确定时,局部作用域和嵌套作用域才能确定。
- 标识符查找顺序为:由内而外,从小到大,局部作用域 -> 嵌套作用域 -> 全局作用域 -> 内置作用域,如
get_a_name函数中的second_name,首先get_a_name所在局部作用域决定可以访问局部、全局和内置命名空间,依次从命名空间查找标识符,然后有该标识符命名空间对应的代码中定义了该标识符,或者由内而外从作用域也可查找标识符定义。
示例
一般情况下变量由内而外依次从作用域中查找,global用于声明该变量为全局变量,需要到全局作用域查找,nonlocal用于声明该变量为非本地(也非全局)变量,需要到嵌套作用域查找。
先在局部作用域查找变量。
a = 1 def print_local_num(): a = 9 # 定义局部变量a print(a) # 9 print_local_num() print(f"{a=}") # a=1 print_local_num改变的不是全局变量a本地找不到向上层查找直至全局作用域,对变量只读。
a = 1 def print_global_num(): print(a) print_global_num() # 1本地找不到上层可以找到,但在本地希望修改变量的情况下只会在本地查找,无则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()本地找不到全局可以找到,本地希望修改可以在本地声明变量为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无论嵌套多少层,全局就是全局。
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如果希望使用并修改嵌套作用域中定义的变量,使用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=1nonlocal逐层向上寻找变量的定义,直到全局作用域之前。
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)多层嵌套,所有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)
- 首先执行
test.py文件,需要导入xxx_service - 然后进入
xxx_service.py- 此时
xxx_service模块已经初始化成功,但此时该模块为空 - 首先打印
xxx_start - 需要从
xxx_service导入print_xxx
- 此时
- 然后进入
yyy_service.py- 此时
yyy_service模块已初始化成功,但此时该模块为空 - 首先打印
yyy_start - 需要从
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.pypython yyy_service.py
揭晓答案
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。