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

目录
  • LEGB规则
  • global表达式
  • 属性引用与名字引用
  • 属性空间
  • 小结

前言:

这里再回顾一下函数的local空间,首先我们往global空间添加一个键值对相当于定义一个全局变量,那么如果往函数的local空间里面添加一个键值对,是不是也等价于创建了一个局部变量呢?

def f1():
    locals()["name "] = "夏色祭"
    try:
        print(name)
    except Exception as e:
        print(e)
f1()  # name 'name' is not defined

对于全局变量来讲,变量的创建是通过向字典添加键值对的方式实现的。因为全局变量会一直在变,需要使用字典来动态维护。

但对于函数来讲,内部的变量是通过静态方式存储和访问的,因为局部作用域中存在哪些变量在编译的时候就已经确定了,我们通过PyCodeObject的co_varnames即可获取内部都有哪些变量。

所以,虽然我们说查找是按照LGB的方式查找,但是访问函数内部的变量其实是静态访问的,不过完全可以按照LGB的方式理解。

因此名字空间是Python的灵魂,它规定了Python变量的作用域,使得Python对变量的查找变得非常清晰。

LEGB规则

而从Python2.2开始,由于引入了嵌套函数,所以最好的方式应该是内层函数找不到某个变量时先去外层函数找,而不是直接就跑到global空间里面找。

那么此时的规则就是LEGB:

a = 1
def foo():
    a = 2

    def bar():
        print(a)
    return bar
f = foo()
f()
"""
2
"""

调用f,实际上调用的是函数bar,最终输出的结果是2。如果按照LGB的规则来查找的话,由于函数bar的作用域没有a、那么应该到全局里面找,打印的结果是1才对。

但是我们之前说了,作用域仅仅是由文本决定的,函数bar位于函数foo之内,所以函数bar定义的作用域内嵌于函数foo的作用域之内。换句话说,函数foo的作用域是函数bar的作用域的直接外围作用域。

所以应该先从foo的作用域里面找,如果没有那么再去全局里面找。而作用域和名字空间是对应的,所以最终打印了2。

另外在执行f = foo()的时候,会执行函数foo中的def bar():语句,这个时候解释器会将a=2与函数bar捆绑在一起,然后返回,这个捆绑起来的整体就叫做闭包。

所以:闭包 = 内层函数 + 引用的外层作用域

这里显示的规则就是LEGB,其中E表示enclosing,代表直接外围作用域。

global表达式

有一个很奇怪的问题,最开始学习Python的时候,笔者也为此困惑了一段时间,下面来看一下。

a = 1
def foo():
    print(a)
foo()
"""
1
"""

首先这段代码打印1,这显然是没有问题的,不过下面问题来了。

a = 1
def foo():
    print(a)
    a = 2
foo()
"""
UnboundLocalError: local variable 'a' referenced before assignment
"""

仅仅是在print语句后面新建了一个变量a,结果就报错了,提示局部变量a在赋值之前就被引用了,这是怎么一回事,相信肯定有人为此困惑。

而想弄明白这个错误的原因,需要深刻理解两点:

  • 一个赋值语句所定义的变量,在这个赋值语句所在的整个作用域内都是可见的;
  • 函数中的变量是静态存储、静态访问的, 内部有哪些变量在编译的时候就已经确定;

在编译的时候,因为a = 2这条语句,所以知道函数中存在一个局部变量a,那么查找的时候就会在当前作用域中查找。但是还没来得及赋值,就print(a)了,所以报错:局部变量a在赋值之前就被引用了。但如果没有a = 2这条语句则不会报错,因为知道局部作用域中不存在a这个变量,所以会找全局变量a,从而打印1

更有趣的东西隐藏在字节码当中,我们可以通过反汇编来查看一下:

import dis
a = 1
def g():
    print(a)
dis.dis(g)
"""
  7           0 LOAD_GLOBAL              0 (print)
              2 LOAD_GLOBAL              1 (a)
              4 CALL_FUNCTION            1
              6 POP_TOP
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE
"""

def f():
    print(a)
    a = 2
dis.dis(f)
"""
 12           0 LOAD_GLOBAL              0 (print)
              2 LOAD_FAST                0 (a)
              4 CALL_FUNCTION            1
              6 POP_TOP

 13           8 LOAD_CONST               1 (2)
             10 STORE_FAST               0 (a)
             12 LOAD_CONST               0 (None)
             14 RETURN_VALUE
"""

中间的序号代表字节码的偏移量,我们先看第二条,g的字节码是LOAD_GLOBAL,意思是在global名字空间中查找;而f的字节码是LOAD_FAST,表示在local名字空间中查找。因此结果说明Python采用了静态作用域策略,在编译的时候就已经知道了名字藏身于何处。

而且上面的例子也表明,一旦函数内有了对某个名字的赋值操作,这个名字就会在作用域内可见,就会出现在local名字空间中。换句话说,会遮蔽外层作用域中相同的名字。

当然Python也为我们精心准备了global关键字,让我们在函数内部修改全局变量。比如函数内部出现了global a,就表示我后面的a是全局的,直接到global名字空间里面去找,不要在local空间里面找了。

a = 1
def bar():
    def foo():
        global a
        a = 2
    return foo

bar()()
print(a)  # 2
# 当然,也可以通过globals函数拿到名字空间
# 然后直接修改里面的键值对

但如果外层函数里面也出现了变量a,而我们想修改的也是外层函数的a、不是全局的a,这时该怎么办呢?Python同样为我们准备了关键字: nonlocal,但是使用nonlocal的时候,必须是在内层函数里面。

a = 1
def bar():
    a = 2
    def foo():
        nonlocal a
        a = "xxx"
    return foo

bar()()
print(a)  # 1
# 外界依旧是1,但是bar里面的a已经被修改了

属性引用与名字引用

属性引用实质上也是一种名字引用,其本质都是到名字空间中去查找一个名字所引用的对象。这个就比较简单了,比如a.xxx,就是到a里面去找属性xxx,这个规则是不受LEGB作用域限制的,就是到a里面查找,有就是有、没有就是没有。

但是有一点需要注意,我们说查找会按照LEGB规则,但这必须限制在自身所在的模块内,如果是多个模块就不行了。举个栗子:

# a.py
print(name)
# b.py
name = "夏色祭"
import a

关于模块的导入我们后续会详细说,总之目前在b.py里面执行的import a,你可以简单认为就是把a.py里面的内容拿过来执行一遍即可,所以这里相当于print(name)。

但是执行b.py的时候会提示变量name没有被定义,可把a导进来的话,就相当于print(name),而我们上面也定义name这个变量了呀。

显然,即使我们把a导入了进来,但是a.py里面的内容依旧是处于一个模块里面。而我们也说了,名称引用虽然是LEGB规则,但是无论如何都无法越过自身所在的模块。print(name)在a.py里面,而变量name被定义在b.py里面,所以不可能跨过模块a的作用域去访问模块b里面的name,因此在执行 import a 的时候会抛出 NameError。

所以我们发现,虽然每个模块内部的作用域规则有点复杂,因为要遵循LEGB;但模块与模块之间的作用域还是划分的很清晰的,就是相互独立。

关于模块,我们后续会详细说。总之通过 . 的方式,本质上都是去指定的名字空间中查找对应的属性。

属性空间

我们知道,自定义的类里面如果没有__slots__,那么这个类的实例对象都会有一个属性字典。

class Girl:
    def __init__(self):
        self.name = "古明地觉"
        self.age = 16
g = Girl()
print(g.__dict__)  # {'name': '古明地觉', 'age': 16}

# 对于查找属性而言, 也是去属性字典中查找
print(g.name, g.__dict__["name"])  # 古明地觉 古明地觉

# 同理设置属性, 也是更改对应的属性字典
g.__dict__["gender"] = "female"
print(g.gender)  # female

当然模块也有属性字典,本质上和类的实例对象是一致的。

import builtins
print(builtins.str)  # <class 'str'>
print(builtins.__dict__["str"])  # <class 'str'>
# 另外,有一个内置的变量 __builtins__,和导入的 builtins 等价
print(__builtins__ is builtins)  # True

另外这个__builtins__位于 global名字空间里面,然后获取global名字空间的globals又是一个内置函数,于是一个神奇的事情就出现了。

print(globals()["__builtins__"].globals()["__builtins__"].
      globals()["__builtins__"].globals()["__builtins__"].
      globals()["__builtins__"].globals()["__builtins__"]
      )  # <module 'builtins' (built-in)>
print(globals()["__builtins__"].globals()["__builtins__"].
      globals()["__builtins__"].globals()["__builtins__"].
      globals()["__builtins__"].globals()["__builtins__"].list("abc")
      )  # ['a', 'b', 'c']

所以global名字空间和builtin名字空间,都保存了指向彼此的指针,不管套娃多少次,都是可以的。

小结

在 Python 中,一个名字(变量)的可见范围由作用域决定,而作用域由语法静态划分,划分规则提炼如下:

  • .py文件(模块)最外层为全局作用域;
  • 遇到函数定义,函数体形成子作用域;
  • 遇到类定义,类定义体形成子作用域;
  • 名字仅在其作用域以内可见;
  • 全局作用域对其他所有作用域可见;
  • 函数作用域对其直接子作用域可见,并且可以传递(闭包);

与作用域相对应, Python在运行时借助PyDictObject对象保存作用域中的名字,构成动态的名字空间 。

这样的名字空间总共有 4 个:

  • 局部名字空间(local):不同的函数,局部名字空间不同,可以通过调用 locals 获取;
  • 全局名字空间(global):全局唯一,可以通过调用 globals 获取;
  • 闭包名字空间(enclosing)
  • 内置名字空间(builtin):可以通过调用 builtins__.__dict 获取;

查找名字时会按照LEGB规则查找,但是注意:无法跨越文件本身,也就是按照自身文件的LEGB。如果属性查找都找到builtin空间了,那么证明这已经是最后的倔强。如果builtin空间再找不到,那么就只能报错了,不可能跑到其它文件中找。

到此这篇关于Python学习之名字,作用域,名字空间(下)的文章就介绍到这了,更多相关Python名字空间内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

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

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

  • 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 赋值 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 名称空间与作用域详情

    目录 一.名称空间 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学习之名字,作用域,名字空间

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

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

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

  • python读取文件名并改名字的实例

    第一版,能实现,但最后发现文件的顺序改变了: import os def reename(): nm=1 pathh="/home/huangyaya/file/image/pic/chips" filelist=os.listdir(pathh) for files in filelist: Olddir=os.path.join(pathh,files) filename=os.path.splitext(files)[0] filetype=os.path.splitext(fi

  • Python学习笔记整理3之输入输出、python eval函数

    1. python中的变量: python中的变量声明不需要像C++.Java那样指定变量数据类型(int.float等),因为python会自动地根据赋给变量的值确定其类型.如 radius = 20,area = radius * radius * 3.14159 ,python会自动的将radius看成"整型",area看成"浮点型".所以编程时不用再像之前那样小心翼翼的查看数据类型有没有出错,挺人性化的. 2. input和print: 先贴个小的程序 #

  • Python中的函数作用域

    在python中,一个函数就是一个作用域 name = 'xiaoyafei' def change_name(): name = '肖亚飞' print('在change_name里的name:',name) change_name() # 调用函数 print("在外面的name:",name) 运行结果如下: 在change_name里的name: 肖亚飞 在外面的name: xiaoyafei 我们再试一下在嵌套函数中是如何的寻找的? age = 15 def func():

  • 浅析Python的命名空间与作用域

    名称空间 名称空间(namespaces):用于存放名字与内存地址绑定关系的地方,是对栈区的划分 作用:名称空间可以使栈区中存放相同的名字,从而解决命名冲突 名称空间分为三种: 内置名称空间 全局名称空间 局部名称空间 内置名称空间 内置名称空间:用于存放Python解释器中内置的名字 生命周期:Python解释器启动则产生,Python解释器关闭则销毁 例如:print.input.int ... 全局名称空间 全局名称空间:运行顶级代码所产生的名字,或者说除函数内定义以及内置的外,剩下的都是

  • Python学习之函数的定义与使用详解

    目录 函数的定义 函数的分类 函数的创建方法-def 函数的返回值-return return与print的区别 函数的传参 必传参数 默认参数 不确定参数(可变参数) 参数规则 函数小练习 函数的参数类型定义 全局变量与局部变量 全局变量 局部变量 global关键字 递归函数 递归函数的定义方法 递归函数的说明 lambda-匿名函数 函数练习 函数的定义 什么是函数? — > 函数是具有某种特定功能的代码块,可以重复使用(在前面数据类型相关章节,其实已经出现了很多 Python 内置函数了

  • python学习之面向对象【入门初级篇】

    前言 最近在学习Python的面向对象编程,以前是没有接触过其它的面向对象编程的语言,因此学习这一部分是相当带劲的,这里也总结一下. 概述 python支持多种编程范式:面向过程.面向对象.面向切面(装饰器部分)等. 面向过程:根据业务逻辑从上到下写垒代码 函数式:将某功能代码封装到函数中,日后便无需重复编写,仅调用函数即可 面向对象:对函数进行分类和封装,让开发"更快更好更强..." OOP思想 面向对象的基本哲学:世界由具有各自运动规律和内部状态的对象组成,对象之间相互作用和通讯构

  • 利用Python学习RabbitMQ消息队列

    RabbitMQ可以当做一个消息代理,它的核心原理非常简单:即接收和发送消息,可以把它想象成一个邮局:我们把信件放入邮箱,邮递员就会把信件投递到你的收件人处,RabbitMQ就是一个邮箱.邮局.投递员功能综合体,整个过程就是:邮箱接收信件,邮局转发信件,投递员投递信件到达收件人处. RabbitMQ和邮局的主要区别就是RabbitMQ接收.存储和发送的是二进制数据----消息. rabbitmq基本管理命令: 一步启动Erlang node和Rabbit应用:sudo rabbitmq-serv

  • 快速入门python学习笔记

    本篇不是教给大家如何去学习python,有需要详细深入学习的朋友可以参阅:Python基础语言学习笔记总结(精华)本文通过一周快速学习python入门知识总计了学习笔记和心得,分享给大家. ##一:语法元素 ###1.注释,变量,空格的使用 注释 单行注释以#开头,多行注释以''开头和结尾 变量 变量前面不需要声明数据类型,但是必须赋值 变量命名可以使用大小写字母,数字和下划线的组合,但是首字母只能是大小写字母或者下划线,不能使用空格 中文等非字母符号也可以作为名字 空格的使用 表示缩进关系的空

随机推荐