1

约束 名字空间 作用域 之间的那些事

不管在什么编程语言, 都有作用域这个概念.作用域控制在它范围内代码的生存周期, 包括名字和实体的绑定.

名字和实体的绑定, 我们可以理解成赋值. num = int_obj, 当我们执行这句代码时, 实际上我们已经得到一个('num', int_obj)的关联关系, 我们也能将称之为约束, 这个约束也将存在名字空间(name space)里面, 名字空间也将是LEGB查找的依据.

而每个名字空间, 也将对应一个作用域, 作用域是代码正文中的一段代码区域, 作用域的有效范围更多是这段代码区域去衡量,一个作用域可以有多个名字空间, 一个名字空间也能有多个约束(多个赋值语句)

可以通过sys._getframe().f_code.co_name 查看代码所处的作用域, 先来看下sys._getframe是什么鬼吧?

# sys module
def _getframe(depth=None): # real signature unknown; restored from __doc__
    """
    _getframe([depth]) -> frameobject
    
    Return a frame object from the call stack.  If optional integer depth is
    given, return the frame object that many calls below the top of the stack.
    If that is deeper than the call stack, ValueError is raised.  The default
    for depth is zero, returning the frame at the top of the call stack.
    
    This function should be used for internal and specialized
    purposes only.
    """
    pass

从函数的定义可以看到, sys._getframe将返回一个frameobject对象, 那其实frameobject是什么对象? 为什么它能决定作用域?

frameobjec实际上就是python虚拟机上所维护的每个栈帧, 这和我们常规理解的栈帧多点差别, 因为python在原有栈帧的基础上, 在封装一层形成自己的栈帧. 虽然是有些不同, 但是我们还是能近似看成常规理解的栈帧, 包括入栈,出栈 局部变量等等

那么frameobejct里面究竟有什么?

# help(sys._getframe())
# Output:
class frame(object)
 .....            # 省略
 |  Data descriptors defined here:
 |  f_back        # 上一个栈帧对象(谁调用自己)
 |  f_builtins    # 内置名字空间
 |  f_locals      # 全局名字空间
 |  f_globals     # 全局名字空间
 |  f_code        # 帧指向的 codeObject对象
 .....            # 省略

我们现在已经知道frameobject的来历呢, 那么再回顾上面提到的: sys._getframe().f_code.co_name

毫无疑问, 我们还是得看下codeobject是什么东西, 才能知道name的意思:

同样也是print help大法

# print help(sys._getframe().f_code)
# Output:
class code(object)
 ......        # 省略
 |  Data descriptors defined here:
 |  
 |  co_name    # code block的名字, 通常是类名或者函数名 /* string (name, for reference) */ 
 |  
 |  co_names   # code block中所有的名字 /* list of strings (names used) */
 |
 ......        # 省略

虽然 sys._getframe().f_code.co_name 顶多也只能说明, 这段代码是在哪个code block里面, 并没有直接证明就是作用域, 但是从上面也已经谈到, 作用域是从代码正文的代码片段的决定, So, 也能近似看成算是作用域的名字了~

作用域话题似乎聊得有点深入了, 让我们暂告一段落, 继续讲讲 约束 和 作用域的关系吧

每个约束一旦创建, 将会持续的影响后面代码的执行, 但是约束也只能在名字空间内生效, 也就是说,一旦出了名字空间/作用域. 约束也将失效

a = 3
def f():
    a = 6
    print a    # 输出 6
f()
print a        # 输出 3

在上面例子可以看到, 变量a在模块层和函数f层都有赋值, 在执行函数f时,输出6, 但是在下面却输出了3, 也就是因为函数f 中的 a=3 约束只有在函数f的作用域中生效,函数结束,a的值, 应该是最开始的a=3来控制, 我们现在应该隐约有种感觉, 为什么赋值语句会被称为约束? 我们完全可以理解成, 一个变量名, 可能有多次改变其绑定的实体对象的机会, 但是最终显示是哪个实体, 完全就是从作用域->名字空间->约束 来决定

LEGB

从上面我们已经清楚 约束,名字空间, 作用域之间微妙的关系, 那么我们接下来就应该探讨下变量查找的方式了.

LEGB 分别是:

  • locals 是函数内的名字空间,包括局部变量和形参

  • enclosing 外部嵌套函数的名字空间(闭包中常见)

  • globals 全局变量,函数定义所在模块的名字空间

  • builtins 内置模块的名字空间
    而查找的优先顺序从左到右以此是: L -> E -> G -> B

从上面我们已经知道, 约束, 是受作用域和名字空间的影响, 所以查找肯定也是只能在名字空间去进行

来些简单代码吧:

a = 3
def f():
    print a     # 输出 3
    print open  # 输出 <built-in function open>
f()

print '----------------------分割线----------------'

a = 3
def f():
    def v():
        print a
    return v
test = f()
test()          # 输出 3

这段相信大家都知道为什么能够输出3, 当在函数内部的名字空间找不到关于变量a的约束时, 将会去全局变量的名字空间查到, OK, 已经找到了 (a,3)的约束, 返回 3., test()也是同理

同样的, 在函数内部和模块内部都不能找到open的约束, 那么只能去Bulitin(内置名字空间)去查找了, 找到了open了, 并且还是个函数, 所以返回 <built-in function open>

简单的演示完, 来些神奇的代码:

a = 3
def f():
    a = 4
    def v():
        print a
    return v
test = f()
test()     # 输出 4 Why?

有没有觉得很奇怪, a=4是在函数f里面定义的, 但是返回v的时候, 函数已经退出,理应释放了, 为什么test()还能输出4呢? 其实原因很简单, 首先这个已经是闭包函数了, 同样的还是遵循LEGB的原则, 函数v已经能够在外层嵌套作用域找到a的定义, 又因为闭包函数有个特点, 在构建的时候, 能够将需要的约束也一并绑定到自身里头, 所以即使函数f退出了, 变量a释放了, 但是不要紧, 函数v已经绑定好了相应的约束了, 自然而然也就能输出4
欢迎各位大神指点交流,转载请注明: https://segmentfault.com/a/11...


Lin_R
5.2k 声望334 粉丝

准则一:简单即美。