Python名字,Python的作用域

  Python名字,Python的作用域

  本文主要介绍Python学习的名称、范围和命名空间,后面是上一篇文章的内容展开全文。需要的朋友可以参考一下,希望对你的学习有所帮助。

  

目录
LEGB规则全局表达式属性引用和名称引用属性空间汇总前言:

  我们再来回顾一下函数的局部空间。首先,向全局空间添加一个键值对相当于定义一个全局变量。那么,如果在函数的局部空间添加一个键值对,是不是相当于创建了一个局部变量?

  定义f1():

  local()[ name ]= Summer Festival

  尝试:

  打印(姓名)

  e:除外

  打印(e)

  f1() # name name 未定义

  对于全局变量,变量的创建是通过向字典中添加键值对来实现的。因为全局变量总是会改变,所以需要一个字典来动态地维护它们。

  但是就函数而言,内部变量是以静态的方式存储和访问的,因为哪些变量存在于局部范围内,在编译的时候就已经确定了,我们可以通过PyCodeObject的co_varnames得到哪些变量在里面。

  所以,虽然我们说搜索是按照LGB做的,实际上访问函数内部的变量是静态访问的,但是按照LGB完全可以理解。

  所以命名空间是Python的灵魂,它定义了Python变量的范围,使得Python对变量的搜索非常清晰。

  

LEGB规则

  从Python2.2开始,因为嵌套函数的引入,当内部函数找不到变量时,最好的办法是先去外部函数,而不是直接跑到全局空间去找。

  那么此时的规则就是LEGB:

  a=1

  def foo():

  a=2

  定义栏():

  打印(一份)

  返回栏

  f=foo()

  f()

  注射毒品

  调用f,实际是调用函数栏,最后输出结果是2。如果按照LGB的规则寻找,因为功能栏的范围没有A,所以要在全球寻找,打印出来的结果是1。

  但是我们之前说过,范围只由文本决定,而功能栏位于函数foo中,所以功能栏定义的范围是嵌入在函数foo的范围中的。换句话说,函数foo的作用域是函数bar作用域的直接外围作用域。

  所以我们应该先在foo的范围内找,如果没有,那就在全局范围内找。并且作用域和命名空间是对应的,所以最后打印出2。

  另外,执行f=foo()时,会执行函数foo中的def bar():语句。这时解释器会把a=2和函数栏绑定,然后返回。这种绑定的整体称为闭包。

  所以:闭包=内部函数引用的外部作用域。

  这里显示的规则是LEGB,其中e代表封闭,e代表直接外围作用域。

  

global表达式

  有一个很奇怪的问题。刚开始学Python的时候,有一段时间对它很迷茫。让我们来看看。

  a=1

  def foo():

  打印(一份)

  foo()

  任何人

  首先,这段代码打印1,这显然没有问题,但问题就在这里。

  a=1

  def foo():

  打印(一份)

  a=2

  foo()

  UnboundLocalError:在赋值前引用了局部变量“a”

  只是在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名字空间内容请搜索盛行IT软件开发工作室以前的文章或继续浏览下面的相关文章希望大家以后多多支持盛行IT软件开发工作室!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: