语言基础篇14——Python源文件组织结构,模块与包、命名空间与作用域的奥秘

源文件组织结构

模块与包

模块,module

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

包,package

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

module对象

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

模块与包导入的优先级

详见另一篇文章:Python同名包和模块如何处理

自定义包

详见另一篇文章:如何写一个Python三方包供别人使用

命名空间与作用域

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用于声明该变量为非本地(也非全局)变量,需要到嵌套作用域查找。。

  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
    
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值