Python作用域与名字空间源码学习笔记

目录
  • 作用域与名字空间
  • 1. 名字绑定
    • 1.1 赋值
    • 1.2 模块导入
    • 1.3 函数、类定义
    • 1.4 as关键字
  • 2. 作用域
    • 2.1 静态作用域
    • 2.2 划分作用域
    • 2.3 闭包作用域
    • 2.4 类作用域
    • 2.5 复杂嵌套
      • 2.5.1 函数嵌套类
      • 2.5.2 类嵌套类
  • 3. 名字空间
    • 3.1 Globals
    • 3.2 Locals
    • 3.3 Enclosings
    • 3.4 Builtin
  • 4. 问题与总结

作用域与名字空间

问题:

PI = 3.14
def circle_area(r):
    return PI * r ** 2
class Person(object):
    def __init__(self, name):
        self.name = name
    def say(self):
        print('i am', self.name)

以这个程序为例,代码中出现的每个变量的作用域分别是什么?程序中总共涉及多少个名字空间?Python又以怎样的顺序去查找一个变量呢?

1. 名字绑定

1.1 赋值

在Python中,变量只是一个与实际对象绑定起来的名字,变量定义本质上就是建立名字与对象的约束关系。因此,赋值语句本质上就是建立这样的约束关系,将右边的对象与左边的名字绑定起来:

a = 1

赋值语句是最基本的将名字与对象绑定的方式,除此之外还有很多其他方式都起到了这样的作用。

1.2 模块导入

当我们导入一个模块时,也会在当前上下文创建一个名字,并与被导入对象绑定。

# 在当前上下文创建一个名字test,与被导入的module对象绑定
import test

1.3 函数、类定义

# 函数名circle_area与function对象绑定
def circle_area(r):
    return PI * r ** 2
# 类名Person与类型对象绑定
class Person(object):
    def __init__(self):
        pass

1.4 as关键字

# 将名字t与module对象绑定
import test as t

2. 作用域

问题:当我们引入一个名字之后,它的可见范围有多大呢?

a = 1
def func1():
    print(a)  # 1
def func2():
    a = 2
    print(a)  # 2
print(a)  # 1

在不同的代码区域引入的名字,其影响范围是不一样的。第1行定义的a可以影响到func1,而func2中定义的a则不能。此外,一个名字可能会在多个代码区域中定义,但最终在某个代码区域中只能使用其中一个。

2.1 静态作用域

一个名字能够施加影响的程序正文区域,便是该名字的作用域。在Python中,一个名字在程序中某个区域能否起作用,是由名字引入的位置决定的,而不是运行时动态决定的。因此,Python具有静态作用域,也称为词法作用域。那么,作用域具体是如何划分的呢?

2.2 划分作用域

  • Python在编译时,根据语法规则将代码划分为不同的代码块,每个代码块形成一个作用域。首先,整个.py文件构成最顶层的作用域,这就是全局作用域,也成为模块作用域;其次,当代码遇到函数定义,函数体成为当前作用域的子作用域;再者,当代码遇到类定义,类定义体成为当前作用域的子作用域。
  • 一个名字在某个作用域引入后,它的影响范围就被限制在该作用域内。其中,全局作用域对所有直接或间接内嵌于其中的子作用域可见;函数作用域对其直接子作用域可见,并且可以传递。
  • 例子中的作用域的嵌套关系如下:

访问关系如下:

2.3 闭包作用域

闭包的概念:在计算机科学中,闭包,又称词法闭包或函数闭包,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例

代码示例:

>>> pi = 3.14
>>> def closure_print(name: str):
    	def circle_area(r: int):
        	print(name, pi * r * r)
	    return circle_area
>>> circle_area1 = closure_print("circle1: ")
>>> circle_area2 = closure_print("circle2: ")
>>> circle_area1(1)
circle1:  3.14
>>> circle_area2(2)
circle2:  12.56

划分作用域:

思考:circle_area1和circle_area2函数对象是怎么拿到name的?

2.4 类作用域

代码示例:

>>> language = 'chinese'
>>> class Male:
        gender: str = 'male'
        def __init__(self, name: str):
            self.name = name
        def Speak(self):
            print('i speak', language)
        def Gender(self):
            print('i am', gender)
>>> male = Male('zhangsan')
>>> male.Gender()
Traceback (most recent call last):
  File "<pyshell#11>", line 1, in <module>
    male.Gender()
  File "<pyshell#9>", line 8, in Gender
    print('i am', gender)
NameError: name 'gender' is not defined
>>> male.Speak()
i speak chinese

作用域分析:

全局作用域对其他所有内嵌其中的作用域均可见,所以在函数Speak()中可以访问到language

类作用域和函数作用域不一样,它对其子作用域是不可见的,所以在函数Gende()中gender是不可见的

思考:

>>> male.gender
'male'
>>> Male.gender
'male'
>>> male.gender = 'male2'
>>> male.gender
'male2'
>>> Male.gender
'male'

2.5 复杂嵌套

2.5.1 函数嵌套类

在Python中,类可以动态创建,甚至在函数中返回。通过在函数中创建并返回类,可以按函数参数对类进行动态定制

代码示例:

>>> language = 'chinese'
>>> def MakeMale(sSortName: str):
        class Male:
            sortName = sSortName
            def __init__(self, name: str):
                self.name = name
            def Speak(self):
                print('i speak', language)
			def Sort(self):
				print(sSortName)
        return Male
>>> ChineseMale: type = MakeMale('Chinese Men')
>>> chineseMale = ChineseMale('zhangsan')
>>> chineseMale.Speak()
i speak chinese
>>> chineseMale.sortName
Chinese Men
>>> chineseMale.Sort()
Chinese Men

2.5.2 类嵌套类

代码示例:

>>> class OutClass:
        inName = 'in'
        class InClass:
            name = inName
Traceback (most recent call last):
  File "<pyshell#26>", line 1, in <module>
    class OutClass:
  File "<pyshell#26>", line 3, in OutClass
    class InClass:
  File "<pyshell#26>", line 4, in InClass
    name = inName
NameError: name 'inName' is not defined

3. 名字空间

作用域是语法层面的概念,是静态的。当程序开始执行后,作用域中的名字绑定关系需要存储起来,存储的地方就是名字空间。由于名字绑定关系是由名字和对象组成的键值对,因此用dict是理想的存储容器(之前在介绍dict的相关内容时也有提到)

以计算圆面积的例子来认识作用域背后的运行时实体——名字空间。代码示例如下:

>>> PI = 3.14
>>> def closure_print(name: str):
    	def circle_area(r: int):
        	print(name, PI * r * r)
	    return circle_area

3.1 Globals

在Python中,每个模块都有一个dict对象,用于存储全局作用域中的名字,这就是全局名字空间Globals。在上述的例子中,根据我们之前对作用域的划分,可以肯定全局名字空间中一定包含两个名字:PI和closure_print。

如果其他模块也需要使用PI或closure_print函数,就需要通过import语句将模块导入,导入后我们就可以获得一个模块对象:

# 假设我们在test.py中导入上述模块testglobal.py
>>> import testglobal
>>> testglobal
<module 'testglobal' from 'D:\\myspace\\code\\pythonCode\\mix\\namespace\\testglobal.py'>
>>> type(testglobal)
<class 'module'>

通过内置函数dir()我们可以知道模块对象下有哪些属性可以访问:

>>> dir(testglobal)
['PI', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'closure_print']
>>> testglobal.closure_print
<function closure_print at 0x000002F33B14A050>

在Python中,一个对象可以访问哪些属性,成为对象的属性空间。因此,模块的属性空间和全局名字空间本质上就是同一个东西,都通过一个dict对象进行存储。那么如何找到这个dict对象呢——通过__dict__属性:

>>> testglobal.__dict__

此外,我们也可以通过内置函数globals()来获取当前模块的全局名字空间:

>>> globals()

我们分别打印它们的id,本质上就是同一个对象:

>>> id(testglobal.__dict__)
2219833831040
>>> id(globals())
2219833831040

3.2 Locals

Python执行一个作用域内的代码时,需要一个容器来访问当前作用域的名字,这就是局部名字空间Locals

当Python执行closure_print()函数时,将分配一个栈帧对象PyFrameObject来保存上下文信息以及执行状态。作为代码执行时必不可少的上下文信息,全局名字空间和局部名字空间也会在PyFrameObject上记录:

struct _frame {
    PyObject_VAR_HEAD
    struct _frame *f_back;      /* previous frame, or NULL */
    PyCodeObject *f_code;       /* code segment */
    PyObject *f_builtins;       /* builtin symbol table (PyDictObject) */
    PyObject *f_globals;        /* global symbol table (PyDictObject) */
    PyObject *f_locals;         /* local symbol table (any mapping) */
    PyObject **f_valuestack;    /* points after the last local */
    PyObject *f_trace;          /* Trace function */
    int f_stackdepth;           /* Depth of value stack */
    char f_trace_lines;         /* Emit per-line trace events? */
    char f_trace_opcodes;       /* Emit per-opcode trace events? */
    /* Borrowed reference to a generator, or NULL */
    PyObject *f_gen;
    int f_lasti;                /* Last instruction if called */
    int f_lineno;               /* Current line number. Only valid if non-zero */
    int f_iblock;               /* index in f_blockstack */
    PyFrameState f_state;       /* What state the frame is in */
    PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
    PyObject *f_localsplus[1];  /* locals+stack, dynamically sized */
};

3.3 Enclosings

在作用域存在嵌套的情况下,Python将内层代码块依赖的所有外层名字存储在一个容器内,这就是闭包名字空间Enclosings

对于示例:

>>> pi = 3.14
>>> def closure_print(name: str):
    	def circle_area(r: int):
            name = 1
        	print(name, pi * r * r)
	    return circle_area

当Python执行到print(name, pi * r * r)语句时,按照Locals、Enclosings、Globals这样的顺序查找语句中涉及的名字:名字name在Enclosings中找到,名字pi在Globals中找到,名字r在Locals中找到。那么还有一个名字print是如何找到的呢?

3.4 Builtin

Python在builtin模块中提供了很多内建函数和类型,构成运行时的另一个名字空间:内建名字空间Builtin

全局名字空间中有一个名字指向内建名字空间:

>>> import builtins
>>> id(testglobal.__builtins__)
3065787874688
>>> id(builtins.__dict__)
3065787874688

4. 问题与总结

函数作用域对内部所有的作用域均可见,包括内部嵌套的类作用域和函数作用域(例如闭包);类作用域对内部所有的作用域均不可见,包括内部嵌套的类作用域和函数作用域。

“只要在当前Locals命名空间中无同名变量且没有global,nonlocal等关键字的声明的话,就一定创建一个该名字的新局部变量”,以nonlocal的使用为例:

示例1:

>>> def closure_print(name: str):
        def circle_area(r: int):
            print(locals())
            print(name, PI * r * r)
        return circle_area
>>> c = closure_print('circle1')
>>> c(1)
{'r': 1, 'name': 'circle1'}
circle1 3.14

示例2:

>>> PI = 3.14
>>> def closure_print(name: str):
        def circle_area(r: int):
            print(locals())
            name += '1'
            print(name, PI * r * r)
        return circle_area
>>> c = closure_print('circle1')
>>> c(1)
{'r': 1}
Traceback (most recent call last):
  File "<pyshell#4>", line 1, in <module>
    c(1)
  File "<pyshell#2>", line 4, in circle_area
    name += '1'
UnboundLocalError: local variable 'name' referenced before assignment

示例3:

>>> PI = 3.14
>>> def closure_print(name: str):
        def circle_area(r: int):
            print(locals())
            name = 'circle2'
            print(locals())
            print(name, PI * r * r)
        return circle_area
>>> c = closure_print('circle1')
>>> c(1)
{'r': 1}
{'r': 1, 'name': 'circle2'}
circle2 3.14

示例4:

>>> PI = 3.14
>>> def closure_print(name: str):
        def circle_area(r: int):
            print(locals())
            nonlocal name
            name += '1'
            print(locals())
            print(name, PI * r * r)
        return circle_area
>>> c = closure_print('circle1')
>>> c(1)
{'r': 1, 'name': 'circle1'}
{'r': 1, 'name': 'circle11'}
circle11 3.14

locals()输出的到底是什么?C源码如下:

int
PyFrame_FastToLocalsWithError(PyFrameObject *f)
{
    /* Merge fast locals into f->f_locals */
    PyObject *locals, *map;
    PyObject **fast;
    PyCodeObject *co;
    Py_ssize_t j;
    Py_ssize_t ncells, nfreevars;
    if (f == NULL) {
        PyErr_BadInternalCall();
        return -1;
    }
    // 初始赋值locals为f->f_locals
    locals = f->f_locals;
    if (locals == NULL) {
        locals = f->f_locals = PyDict_New();
        if (locals == NULL)
            return -1;
    }
    // 获取对应的PyCodeObject
    co = f->f_code;
    // 获取co_varnames字段
    map = co->co_varnames;
    if (!PyTuple_Check(map)) {
        PyErr_Format(PyExc_SystemError,
                     "co_varnames must be a tuple, not %s",
                     Py_TYPE(map)->tp_name);
        return -1;
    }
    fast = f->f_localsplus;
    j = PyTuple_GET_SIZE(map);
    if (j > co->co_nlocals)
        j = co->co_nlocals;
    if (co->co_nlocals) {
        // 将co_varnames加入到locals中
        if (map_to_dict(map, j, locals, fast, 0) < 0)
            return -1;
    }
    // 闭包相关
    ncells = PyTuple_GET_SIZE(co->co_cellvars);
    nfreevars = PyTuple_GET_SIZE(co->co_freevars);
    if (ncells || nfreevars) {
        // 将co_cellvars加入到locals
        if (map_to_dict(co->co_cellvars, ncells,
                        locals, fast + co->co_nlocals, 1))
            return -1;
        /* If the namespace is unoptimized, then one of the
           following cases applies:
           1. It does not contain free variables, because it
              uses import * or is a top-level namespace.
           2. It is a class namespace.
           We don't want to accidentally copy free variables
           into the locals dict used by the class.
        */
        if (co->co_flags & CO_OPTIMIZED) {
            // 将co_freevars加入到locals
            if (map_to_dict(co->co_freevars, nfreevars,
                            locals, fast + co->co_nlocals + ncells, 1) < 0)
                return -1;
        }
    }
    return 0;
}

以上就是Python作用域与名字空间源码学习笔记的详细内容,更多关于Python作用域名字空间的资料请关注我们其它相关文章!

(0)

相关推荐

  • Python学习之名字,作用域,名字空间

    目录 变量只是一个名字 作用域和名字空间 LGB规则 eval和exec 前言: 我们在PyFrameObject里面看到了3个独立的名字空间:f_locals.f_globals.f_builtins.名字空间对于Python来说是一个非常重要的概念,Python虚拟机的运行机制和名字空间有着非常紧密的联系.并且在Python中,与名字空间这个概念紧密联系在一起的还有名字.作用域这些概念,下面就来剖析这些概念是如何体现的. 变量只是一个名字 很早的时候我们就说过,从解释器的角度来看,变量只是一

  • Python作用域与名字空间原理详解

    Python具有静态作用域,变量的作用域由它定义的位置决定,而与调用的位置无关. a = 2  def f():  a = 2 第一行的a的作用域是全局作用域,作用于定义位置后面的所有位置. 第四行的a的作用域是局部作用域,作用于f函数里. Python能够形成局部作用域的只有函数与类,其他语句不形成局部作用域. 函数与类的局部作用域 def f(): a = 1 class A: b = 2 if 1 == 1: c = 3 for _ in range(1): d = 4 while Tru

  • python 名称空间与作用域详情

    目录 一.名称空间 1.1 内置名称空间 1.2 全局名称空间 1.3 局部名称空间 1.4 加载顺序 1.5 查找顺序 二.作用域 2.1 全局作用域 2.2 局部作用域 2.4 函数对象+作用域应用 三.补充知识点 3.1 global关键字 3.2 nonlocal关键字 3.3 注意点 函数内部的函数只能在函数内部调用,不能在函数外部调用,通过接下来的学习你将会知道为什么会出现这种情况. 一.名称空间 名称空间(name spaces):在内存管理那一章节时,我们曾说到变量的创建其实就是

  • Python进阶_关于命名空间与作用域(详解)

    写在前面 如非特别说明,下文均基于Python3 命名空间与作用于跟名字的绑定相关性很大,可以结合另一篇介绍Python名字.对象及其绑定的文章. 1. 命名空间 1.1 什么是命名空间 Namespace命名空间,也称名字空间,是从名字到对象的映射.Python中,大部分的命名空间都是由字典来实现的,但是本文的不会涉及命名空间的实现.命名空间的一大作用是避免名字冲突: def fun1(): i = 1 def fun2(): i = 2 同一个模块中的两个函数中,两个同名名字i之间绝没有任何

  • Python学习之名字,作用域,名字空间(下)

    目录 LEGB规则 global表达式 属性引用与名字引用 属性空间 小结 前言: 这里再回顾一下函数的local空间,首先我们往global空间添加一个键值对相当于定义一个全局变量,那么如果往函数的local空间里面添加一个键值对,是不是也等价于创建了一个局部变量呢? def f1(): locals()["name "] = "夏色祭" try: print(name) except Exception as e: print(e) f1() # name 'n

  • Python作用域与名字空间源码学习笔记

    目录 作用域与名字空间 1. 名字绑定 1.1 赋值 1.2 模块导入 1.3 函数.类定义 1.4 as关键字 2. 作用域 2.1 静态作用域 2.2 划分作用域 2.3 闭包作用域 2.4 类作用域 2.5 复杂嵌套 2.5.1 函数嵌套类 2.5.2 类嵌套类 3. 名字空间 3.1 Globals 3.2 Locals 3.3 Enclosings 3.4 Builtin 4. 问题与总结 作用域与名字空间 问题: PI = 3.14 def circle_area(r): retur

  • Python内建类型int源码学习

    目录 1 int对象的设计 1.1 PyLongObject 1.2 整数的布局 1.3 小整数静态对象池 1.4 示例 2 大整数运算 2.1 整数运算概述 2.2 大整数运算处理过程 1.long_add()源码: 2.绝对值加法x_add() 3 其他 大整数转float溢出 “深入认识Python内建类型”这部分的内容会从源码角度为大家介绍Python中各种常用的内建类型. 问题:对于C语言,下面这个程序运行后的结果是什么?是1000000000000吗? #include <stdio

  • Python内建类型list源码学习

    目录 问题: 1 常用方法 小结: 题外话: 2 list的内部结构:PyListObject 3 尾部操作和头部操作 3.1 尾部操作 3.2 头部操作 4 浅拷贝和深拷贝 4.1 浅拷贝 4.2 深拷贝 4.3 直接赋值 4.4 小结 个人总结: TODO: 5 动态数组 5.1 容量调整 5.2 append() 5.3 insert() 5.4 pop() 5.5 remove() 6 一些问题 问题: “深入认识Python内建类型”这部分的内容会从源码角度为大家介绍Python中各种

  • Python内建类型float源码学习

    目录 1 回顾float的基础知识 1.1 PyFloatObject 1.2 PyFloat_Type 1.3 对象的创建 1.4 对象的销毁 1.5 小结 2 空闲对象缓存池 2.1 浮点对象的空闲链表 2.2 空闲链表的使用 3 其他 “深入认识Python内建类型”这部分的内容会从源码角度为大家介绍Python中各种常用的内建类型. 1 回顾float的基础知识 1.1 PyFloatObject 1.2 PyFloat_Type C源码(仅列出部分字段): PyTypeObject P

  • Python内建类型str源码学习

    目录 引言 1 Unicode 2 Python中的Unicode 2.1 Unicode对象的好处 2.2 Python对Unicode的优化 3 Unicode对象的底层结构体 3.1 PyASCIIObject 3.2 PyCompactUnicodeObject 3.3 PyUnicodeObject 3.4 示例 4 interned机制 5 总结 引言 “深入认识Python内建类型”这部分的内容会从源码角度为大家介绍Python中各种常用的内建类型. 在介绍常用类型str之前,在上

  • Python对象的底层实现源码学习

    目录 1. PyObject:对象的基石 2. PyVarObject:变长对象的基础 2.1 浮点对象 2.2 列表对象 3. PyTypeObject:类型的基石 4. PyType_Type:类型的类型 5. PyBaseObject_Type:类型之基 6. 补充 在“Python源码学习笔记:Python万物皆对象”中,我们对Python的对象类型体系有了一定的认识,这篇博客将从源码层面来介绍Python中万物皆对象的底层实现. 1. PyObject:对象的基石 在Python解释器

  • Bootstrap源码学习笔记之bootstrap进度条

    基本样式 要实现进度条效果要使用两个容器,外容器使用"progress"样式,子容器使用"progress-bar"样式.例如: <div class="progress"> <div class="progress-bar" style="width:40%"></div> </div> progress样式主要设置进度条容器的背景色,容器高度.间距等,pr

  • Python对象的生命周期源码学习

    目录 思考: 1 C API 2 对象的创建 2.1 两种创建对象的方式 2.2 由类型对象创建实例对象 3 对象的多态性 4 对象的行为 5 引用计数 思考: 当我们输入这个语句的时候,Python内部是如何去创建这个对象的? a = 1.0 对象使用完毕,销毁的时机又是怎么确定的呢? 下面,我们以一个基本类型float为例,来分析对象从创建到销毁这整个生命周期中的行为. 1 C API Python是用C写的,对外提供了API,让用户可以从C环境中与其交互,并且Python内部也大量使用了这

  • Mysql源码学习笔记 偷窥线程

    感觉代码有些凌乱,注释代码都写的比较随意,好像没有什么统一的规范,不同的文件中代码风格也有差异,可能Mysql经过了很多牛人的手之后,集众牛人之长吧.也可能是我见识比较浅薄,适应了自己的代码风格,井底之蛙了,总之还是怀着敬畏的心情开始咱的源码之旅吧.本人菜鸟,大神轻拍. Mysql可以启动起来了,应该怎么学习呢?总不能从main开始一步一步的看吧,Mysql作为比较底层的大型软件,涉及到数据库实现的方方面面,没有厚实的数据库理论基础和对Mysql各个模块相当的熟悉,从main开始势必会把自己引入

随机推荐