“python进阶——模块化(一)命名空间和作用域”
本文最后更新于:1 年前
本文的核心内容是命名空间、作用域、模块、包。
¶(一)前言
¶1. 概述
为了便于开发和维护代码,有逻辑地阻止和处理代码段,经常需要把大量的工程代码通过模块化的方式将其分解成小的子任务和子模块。
之前提及的函数,之后要说明的类,都是模块化编程的一种,但是如果这些都堆在一个文件中,往往会导致文件过大,难以阅读,理解和维护,所以,需要提供更好的方式进行模块化编程。
Python中提供了模块(Module),包(Package)来完成这些工作。
为了更好地在划分这些层次时命名不出现错误,保证访问的变量不出现问题,Python引入了命名空间阻止命名冲突,引入了作用域规定变量的访问范围。
¶2. 优点:
-
简化编程,对于一个大工程的众多内容,可以有效聚焦工作重点,处理项目的核心问题。
-
便于维护,在产生问题时便于排查问题的来源,解决错误。
-
容易理解,相关代码均在一个模块或包中,前后密切相关,使整个工程的组织清晰有条理,而且这种模块式的存储方式很多时候可以隐藏代码细节,简化程序逻辑。
-
复用性高,可以将一个模块或包多次导入和调用,对于已经完成的功能不必重新编写,而且便于个性化拓展功能而不影响原来的代码——在Python特别是可以使用大量的第三方依赖库。
-
避免冲突,模块和包均具备独立的命名空间,也就是即使两个模块或者包之间有相同名字的函数/类/变量等,也不会在彼此之间产生冲突,命名是独立的。
¶(二)命名空间和作用域:
¶1. 命名:
在Python中一切皆对象,所以,对引用的对象赋予一个称谓就是命名。
命名包括对变量,对函数,对类等的命名,必须遵守一定的规范。
此处不介绍命名的基本原则和一般的规范,如有需要可以自行查阅。
¶2. 命名空间:
¶(1)概念:
命名空间是从名称到对象的映射,需要引用的对象名称需要在命名空间中寻找。
命名空间是真实存在的,不是抽象的概念,而且需要通过的一定的技术实现和管理——在Python中,命名空间是通过Python中的字典实现的,命名空间实际上是一个存储名称-对象的键值对字典dirc——本质上,名称(标识符)和对象(本质上是内存区域)是一对一的映射,名称是访问的依据,对象是存储的内容。
命名空间避免了项目处理时命名可能的同名冲突,同一个命名空间内不允许重名,而不同的命名空间是独立的,允许不同命名空间的变量重名,不会产生任何影响,也因此,可以同时存在多个命名空间。
¶(2)分类:
命名空间与作用域是一一对应的,这也成为了命名空间分类的依据。
命名空间在Python中被分为三类:
-
内置命名空间:存储的是Python语言的内置名称,包括各种异常名称以及内置函数,如len()、type()或者类型转换函数等等,在任何位置均可访问。
可以通过导入bulitins库,利用dir函数获得全部内置命名空间的名称,全部的内置命名如下(不分析每个名字的具体内容了......):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26import builtins
builtin_names = dir(builtins)
i=0
for name in builtin_names:
print(name,end=" ")
i+=1
if i==10:
print()
i=0
#运行结果:
# ArithmeticError AssertionError AttributeError BaseException BaseExceptionGroup BlockingIOError BrokenPipeError BufferError BytesWarning ChildProcessError
# ConnectionAbortedError ConnectionError ConnectionRefusedError ConnectionResetError DeprecationWarning EOFError Ellipsis EncodingWarning EnvironmentError Exception
# ExceptionGroup False FileExistsError FileNotFoundError FloatingPointError FutureWarning GeneratorExit IOError ImportError ImportWarning
# IndentationError IndexError InterruptedError IsADirectoryError KeyError KeyboardInterrupt LookupError MemoryError ModuleNotFoundError NameError
# None NotADirectoryError NotImplemented NotImplementedError OSError OverflowError PendingDeprecationWarning PermissionError ProcessLookupError RecursionError
# ReferenceError ResourceWarning RuntimeError RuntimeWarning StopAsyncIteration StopIteration SyntaxError SyntaxWarning SystemError SystemExit
# TabError TimeoutError True TypeError UnboundLocalError UnicodeDecodeError UnicodeEncodeError UnicodeError UnicodeTranslateError UnicodeWarning
# UserWarning ValueError Warning WindowsError ZeroDivisionError __build_class__ __debug__ __doc__ __import__ __loader__
# __name__ __package__ __spec__ abs aiter all anext any ascii bin
# bool breakpoint bytearray bytes callable chr classmethod compile complex copyright
# credits delattr dict dir divmod enumerate eval exec exit filter
# float format frozenset getattr globals hasattr hash help hex id
# input int isinstance issubclass iter len license list locals map
# max memoryview min next object oct open ord pow print
# property quit range repr reversed round set setattr slice sorted
# staticmethod str sum super tuple type vars zip -
全局命名空间:是在模块中定义的名称,在加载模块时被创建,记录模块的变量,包括了函数、类、其他导入模块、模块级的变量和常量,在当前模块内均可访问。
当处于全局作用域中时,由于和命名空间的一对一关系,可以使用locals()函数获得全局命名空间的名称。
也可以直接用globals()在任意位置获得全局命名空间。
1
2
3
4
5
6
7
8
9
10
11
12
13i=0
global_names = list(globals().items()) #或者 locals().items()
for name in global_names:
print(name,end=" ")
i+=1
if i==3:
print()
i=0
#运行结果:
# ('__name__', '__main__') ('__doc__', None) ('__package__', None)
# ('__loader__', <_frozen_importlib_external.SourceFileLoader object at 0x000001ED1BD45610>) ('__spec__', None) ('__annotations__', {})
# ('__builtins__', <module 'builtins' (built-in)>) ('__file__', 'D:\\pycharmwork\\blog_use.py') ('__cached__', None)
# ('sys', <module 'sys' (built-in)>) ('i', 0) -
局部命名空间:是函数中定义的名称,记录了函数的变量,包括函数的参数和局部定义的变量,(即使函数在类中也是如此)只能在函数代码块内访问。
只有在函数作用域中使用locals()函数可以获得局部命名空间。
1
2
3
4
5
6
7
8def fun():
i = 0
j=1
k="abc"
print(locals())
fun()
# 运行结果:
# {'i': 0, 'j': 1, 'k': 'abc'}
¶(3)生存周期:
指命名空间从被创建到被销毁的时间。
不同类型命名空间的生存周期不同,其创建和销毁的时间也不同,本质上,命名空间的生存周期取决于对象的作用域,命名空间的是为了对象而存在的,一旦对象执行完成,命名空间的声明周期就结束了。
-
内置命名空间:和Python解释器的作用域绑定,因此在Python解释器启动时创建,在解释器退出时销毁。
-
全局命名空间:和模块的作用域绑定,因此在解释器加载模块时创建,在模块被移除/解释器退出时销毁。
-
局部命名空间:和函数的作用域绑定,因此要考虑函数是否在类中。
- 在类中:解释器执行类定义语句时创建,类的命名空间被销毁或者类被移除时销毁。
- 不在类中:解释器调用函数时创建,调用结束/抛出异常时销毁。
¶(4)查找名称的顺序
因为需要在命名空间中查找,所以实际的查找顺序就是命名空间的查找顺序。
在Python中,查找顺序为局部命名空间—>全局命名空间—>内置命名空间,如果三者均未发现,放弃查找并引发NameError异常。
¶3. 作用域:
¶(1)概念:
和命名空间不同的是,作用域实际上是一个虚拟概念,没有实际对象,作用域的核心是变量的访问范围和规则。
作用域规定了Python解释器如何查找变量,确定执行代码对变量的访问权限——访问权限和访问顺序决定了程序访问的变量是否有效以及该赋何值。
因为python基础详细说明了全局和局部变量,这里不再介绍,可以结合之前的python基础——函数(一) - ZZHの个人博客 (07xiaohei.com)了解其原理。
¶(2)分类:
-
局部作用域Local,简写为L:
是最内层的作用域,对应了函数部分,函数的参数和函数内定义的变量、对象均属于局部作用域。
-
嵌套作用域Enclosing,简写为E:
也可以理解为外部作用域,外部是针对多层函数定义的。
对应了嵌套函数的作用范围,包括了非局部和非全局的变量,这一作用域主要是为了实现闭包。
对于函数A,假如有内部的嵌套函数B,如果在函数B中访问A的局部变量,则此时函数B不是直接使用,而是在函数B中创建一个__closure__属性,该属性用于保存被函数B访问的A中的局部变量,而此时__closure__属性内的变量的作用域均为嵌套作用域的变量。
可以结合python基础——函数(三) - ZZHの个人博客 (07xiaohei.com)中的装饰器部分进行更进一步的了解。
了解以上内容后,可以重点解释一下闭包的主要逻辑:
闭包的主要目的是,不要在返回内函数时将内函数要使用的外函数的临时对象销毁,为了这一目的使用闭包,定义新的嵌套作用域,当函数结束返回内函数时,外函数了解到内函数被返回,所以将自己要被内函数使用的临时对象中通过嵌套作用域绑定到了内函数上,防止其销毁后破坏内函数执行功能。通过这种方式,我们将内函数赋给一个新变量后,通过新变量调用内函数时,仍旧能够使用外函数的临时变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15def outer():
a = 1
b = 1
print(locals())
def inter():
c=1
print(locals())
return a + c
return inter
temp_fun =outer()
print(temp_fun())
# # 运行结果:
# {'b': 1, 'a': 1}
# {'c': 1, 'a': 1}
# 2可以看到,执行temp_fun()时,outer()函数的临时变量b因为不需要被内函数inter()使用,所以在外函数执行结束后被销毁了,而a被绑定到了内函数中,内函数的作用域中多出了a变量及其值,a的作用域即为嵌套作用域,c的作用域是局部作用域。
如果不返回内函数对象,实际上也会发生此过程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16def outer():
a = 1
b = 1
print(locals())
def inter():
c=1
print(locals())
return a + c
inter()
return 1
temp_fun =outer()
print(temp_fun)
# # 运行结果:
# {'b': 1, 'a': 1}
# {'c': 1, 'a': 1}
# 1另外,如果外函数的参数是传入的,且值不同,则每次调用不同参数的对应内函数id是不同的,也就是创建了两个函数对象,参数相同则是一个函数对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19def outer(a):
b = 1
print(id(a),id(b))
def inter():
c=1
print(id(c))
return a + c
return inter
temp_fun1 =outer(3)
print(id(temp_fun1()))
temp_fun2 =outer(4)
print(id(temp_fun2()))
# 运行结果:
# 140729683866472 140729683866408
# 140729683866408
# 140729683866504
# 140729683866504 140729683866408
# 140729683866408
# 140729683866536 -
全局作用域Global,简写为G:
范围是当前模块的任何位置,模块中的顶层位置所导入的其他模块、定义的全局变量、函数和类都属于全局作用域。
-
内置作用域Built-in,简写为B:
范围是所有模块的任何位置,所有内置命名空间的名称均属于内置做哟结构域,也是最外层的作用域。
¶(3)访问权限:
-
在内层作用域能访问该层及外层所有作用域的变量。
-
外层作用域不能访问其内层的作用域的变量。
-
除了内置作用域,其他作用域不能访问另一个同层级级别的作用域的变量。
¶(4)查找规则:
和命名空间的顺序是相同的,但是多了一级嵌套作用域。
具体的查找顺序为LEGB——即局部作用域—>嵌套作用域—>全局作用域—>内置作用域,均不存在抛出NameError的异常。
注意没有E作用域时可以直接跳过。
¶4. 作用域和命名空间的详细关系:
作用域是建立在命名空间上的虚拟概念,变量实际存储在命名空间内,由作用域决定变量的可访问范围。
命名空间的创建对应作用域的形成,二者是一对一的关系。
变量的定义需要在逻辑上加入到当前的最内层作用域范围,物理上保存到当前对应的命名空间内。
Python中,命名空间及作用域的创建是由模块、类、函数和列表推导完成的,其他的代码块均不会创建。
此处多为概念关系,另外此处逻辑其实也和其他语言的定义十分类似,可以根据自己的理解尝试编写代码并判断自己的理解是否正确,个人就不举例了。