我的编程空间,编程开发者的网络收藏夹
学习永远不晚

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

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

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

作用域与名字空间

问题:

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;      
    PyCodeObject *f_code;       
    PyObject *f_builtins;       
    PyObject *f_globals;        
    PyObject *f_locals;         
    PyObject **f_valuestack;    
    PyObject *f_trace;          
    int f_stackdepth;           
    char f_trace_lines;         
    char f_trace_opcodes;       
    
    PyObject *f_gen;
    int f_lasti;                
    int f_lineno;               
    int f_iblock;               
    PyFrameState f_state;       
    PyTryBlock f_blockstack[CO_MAXBLOCKS]; 
    PyObject *f_localsplus[1];  
};

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)
{
    
    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 (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作用域名字空间的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

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

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

解密Python中的作用域与名字空间

名字空间对于Python来说是一个非常重要的概念,并且与名字空间这个概念紧密联系在一起的还有名字、作用域这些概念,下面就来剖析这些概念是如何体现的
2023-02-28

Python中的作用域与名字空间实例分析

这篇文章主要介绍“Python中的作用域与名字空间实例分析”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Python中的作用域与名字空间实例分析”文章能帮助大家解决问题。变量只是一个符号从解释器的角
2023-07-05

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录