Post

global和nonlocal

直接上例子,Leetcode768,我在SCL下用golang和python和cpp写的时候发现go和python的值捕获好像不太一样。下面的写法无法修改具体的值,但是go就可以直接修改,不需要特殊的声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution:
    def kthSmallestPrimeFraction(self, arr: List[int], k: int) -> List[int]:
        l,r = 0,1
        eps = 1e-8
        a,b = 0,0
        def get(mid):
            global a,b
            j,res,n = 0,0,len(arr)
            for i in range(n):
                while j + 1 < n and arr[j + 1] / arr[i] <= mid: j += 1
                if (arr[j] / arr[i]) <= mid: res += (j + 1)
                if abs(arr[j] / arr[i] - mid) <= eps:
                    a,b = arr[j],arr[i]
            return res
        while r - l > eps:
            mid = (l + r) / 2
            # print(mid)
            if get(mid) >= k: r = mid
            else: l = mid
        return [a,b]

此时其实变量所在的域不是全局作用域,而是一个外部函数的嵌套变量,nonlocal可以比较好的解决这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution:
    def kthSmallestPrimeFraction(self, arr: List[int], k: int) -> List[int]:
        l,r = 0,1
        eps = 1e-8
        a,b = 0,0
        def get(mid):
            nonlocal a,b
            j,res,n = 0,0,len(arr)
            for i in range(n):
                while j + 1 < n and arr[j + 1] / arr[i] <= mid: j += 1
                if (arr[j] / arr[i]) <= mid: res += (j + 1)
                if abs(arr[j] / arr[i] - mid) <= eps:
                    a,b = arr[j],arr[i]
            return res
        while r - l > eps:
            mid = (l + r) / 2
            # print(mid)
            if get(mid) >= k: r = mid
            else: l = mid
        return [a,b]

进一步想想里面的值捕获问题,py下传递都是值语义,直接复制id的方式,实际的作用域是分层的

  • L(Local):最内层,包含局部变量,比如一个函数/方法内部。
  • E(Enclosing):包含了非局部(non-local)也非全局(non-global)的变量。比如两个嵌套函数,一个函数(或类) A 里面又包含了一个函数 B ,那么对于 B 中的名称来说 A 中的作用域就为 nonlocal。
  • G(Global):当前脚本的最外层,比如当前模块的全局变量。
  • B(Built-in): 包含了内建的变量/关键字等,最后被搜索。

规则顺序: L –> E –> G –> B

所以这也能解释为什么不加global直接print一个全局变量的时候也能直接找到这个值。

Python 中只有模块(module),类(class)以及函数(def、lambda)才会引入新的作用域,其它的代码块(如 if/elif/else/、try/except、for/while等)是不会引入新的作用域的,也就是说这些语句内定义的变量,外部也可以访问,如下代码:

1
2
3
4
>>> if True:
...  msg = 'I am from Runoob'
>>> msg
'I am from Runoob'

实例中 msg 变量定义在 if 语句块中,但外部还是可以访问的。

如果将 msg 定义在函数中,则它就是局部变量,外部不能访问:

1
2
3
4
5
6
7
>>> def test():
...     msg_inner = 'I am from Runoob'
... 
>>> msg_inner
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'msg_inner' is not defined

定义在函数内部的变量拥有一个局部作用域,定义在函数外的拥有全局作用域。

局部变量只能在其被声明的函数内部访问,而全局变量可以在整个程序范围内访问。调用函数时,所有在函数内声明的变量名称都将被加入到作用域中

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/python3
 
total = 0 # 这是一个全局变量
# 可写函数说明
def sum( arg1, arg2 ):
    #返回2个参数的和."
    total = arg1 + arg2 # total在这里是局部变量.
    print ("函数内是局部变量 : ", total)
    return total
 
#调用sum函数
sum( 10, 20 )
print ("函数外是全局变量 : ", total)

结果是

1
2
函数内是局部变量 :  30
函数外是全局变量 :  0

内部作用域想修改外部域的变量的时候就要用到global 和 nonlocal 关键字了,开头的就是实际的例子。

实际想在内部作用域修改全局或者外部变量的时候需要针对具体变量加具体的关键字。

1
2
3
4
5
6
7
8
num = 1
def fun1():
    global num  # 需要使用 global 关键字声明
    print(num) 
    num = 123
    print(num)
fun1()
print(num)

命名空间

官文文档的一段话:

1
A namespace is a mapping from names to objects.Most namespaces are currently implemented as Python dictionaries。

命名空间提供了在项目中避免名字冲突的一种方法。各个命名空间是独立的,没有任何关系的,所以一个命名空间中不能有重名,但不同的命名空间是可以重名而没有任何影响。这个C++里的namespace的思路是一样的。

一般有三种命名空间:

  • 内置名称(built-in names), Python 语言内置的名称,比如函数名 abs、char 和异常名称 BaseException、Exception 等等。
  • 全局名称(global names),模块中定义的名称,记录了模块的变量,包括函数、类、其它导入的模块、模块级的变量和常量。
  • 局部名称(local names),函数中定义的名称,记录了函数的变量,包括函数的参数和局部定义的变量。(类中定义的也是)

一般的查找顺序就是:

局部的命名空间去 -> 全局命名空间 -> 内置命名空间

如果找不到就会触发一个NameError异常。

生命周期的问题,命名空间的生命周期取决于对象的作用域,如果对象执行完成,则该命名空间的生命周期就结束。因此,我们无法从外部命名空间访问内部命名空间的对象。这也是很自然的一个道理

Reference

This post is licensed under CC BY 4.0 by the author.

Trending Tags