python 变量作用域、闭包、装饰器

一.python的作用域

1,什么是“LEGB”

Python与大多数编程语言一样,搜索变量值的时候,即命名空间的规则,会采用’就近原则’:
由近及远依次为: 本地作用域(Local) --> 外部嵌套函数作用域(Enclosing Local) --> 全局/模块作用域(Global) --> 内置作用域(Built-in).

a = str(1) # str:Built-in
b = 3       # b:Global
def f1():
    print(b) # b:Global
    a = 3    # a:Local
    print(a)
def f2():
    a = 3         # a:Enclosing
    def f3():
        b = 3     # b:Local
        print(b)
  • LocalEnclosing 是一个相对的概念. 在函数 f1 中, a 是一个 Local 变量,而在 f2 中 a 是一个 Enclosing 变量。
  • 只有模块、类、函数才会引入新的作用域. 而 if for while 语句不会引入新的作用域.
  • 全局作用域中的变量对于下层作用域比如函数来说, 是一个只读变量.
2,使用global、nonlocal 关键字声明外部作用域变量
1)示例:

示例1:

a = 4
def foo():
    a = 3
foo()
print(a) # 4

示例2:

a = 4
def foo():
    a = a + 3
foo()
# local variable 'a' referenced before assignment
  • 上面的示例1中,我们可以看到,因为全局变量对于下层的作用域来说只有只读权限,所以函数内部不是修改全局变量 a 的值,而是重新定义了一个本地变量 a。所以全局变量 a 的值并没有改变。
  • 上面的示例2中,还是由于局变量对于下层的作用域来说只有只读权限,在函数 foo 内部, a = a + 3 这个表达式的存在会让 Python 编译函数的定义体时,它判断 a 是局部变量,因为在函数中给它赋值了。所以才出现为变量未定义的报错。
2)global、nonlocal

如何在内部作用域中去修改外部作用域的变量值呢?这里推出两个关键字来声明外部作用域变量:globalnonlocal 。内部作用域中要修改外部作用域变量的值时。要用 global、nonlocal 关键字声明外部作用域变量。

a = 3
def foo():
    global a  # 使用 global 声明 a ,便可以在函数中修改 a 的值
    a = 4
foo()
print(a) 
# 4       
def f1():
    a = 3
    def f2():
        nonlocal a  # 使用 nonlocal 声明 a, 便可以在嵌套的函数内部修改 a 的值
        a = a + 1
        print(a)
    f2()
  • 如上示例所示,关键字global用于本地作用域声明全局/模块作用域变量使用;而关键字nonlocal用于本地作用域声明外部嵌套函数作用域变量使用。

二,闭包与自由变量

1)函数实质与属性
  1. 函数是一个对象
  2. 函数执行后完成内部变量回收
  3. 函数属性
  4. 函数返回值
def check(val):
    #以16进制打印val的id值
    print('%x'%id(val))
    if val > 60:
        print('pass')
    else:
        print('fail')
    def func():
        print(val)
    func()
    return func

if __name__ == "__main__":
    f = check(90)
    f()
    print(f.__closure__)	# closure:内部函数中对enclosing作用域的变量进行引用
    
---------output-------------------
557a7165b5e0  # 此时可看出val的id值为1009dd4a0
pass
90
---------
90
---------
(<cell at 0x7fc8c5230bb8: int object at 0x557a7165b5e0>,)

按照python的变量回收机制,val在check函数执行完成后,应该被回收掉,为什么会出现调用f()时,出现val的值呢?f中存在int类型的val值的引用,因为python采用引用计数的垃圾回收机制,当check()函数执行完成后,val对象被保存在func的函数属性中,仍存在引用,所以并没有对val进行垃圾回收。

2)什么是闭包、自由变量

什么是闭包:

闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量

总结:闭包实质上是一个函数,它能够访问函数体之外定义的非全局变量,而这个非全局变量指的就是自由变量。它使得函数在执行后,函数的返回对象不会被内存回收,并且闭包中包含着自由变量,只要闭包能被访问到,自由变量就可以被访问。

示例1:

下面举一个例子, 定义一个 avg 函数,参数为一个值, 不断累加的计算从开始到现在所接收的全部值的平均值

def make_average():
    series = []
    def average(value):
        series.append(value)
        total = sum(series)
        return total / len(series)
    return average
avg = make_average() # 1
print(avg(10))      # 2
print(avg(20))      # 3

#-----output----------
10.0
15.0
  • 调用 make_average 返回一个 average 函数对象.这就是一个闭包函数,因为 avg 可以访问 average 函数定义体之外的 series。
  • 这里嵌套函数 average 并没有’改变’ series, 只是修改它的值, 因为 series 是一个可变的列表.所以并不会报错.那么如果 series 是一个不可变对象呢? 会发生什么?

上面的例子效率比较低, 没一次都得 sum.我们难道不可以保存每一步计算的 total 吗?

示例2:

def make_average():
    count = 0
    total = 0
    def average(value):
        count += 1
        total += value
        return total / count
    return average
avg = make_average()
avg(10)
# UnboundLocalError: local variable 'count' referenced before assignment
  • 说明: 在嵌套函数 average 内部有 count += 1,此表达式等价为 count = count + 1。在编译阶段,会把内部的 count 解释为一个本地变量,所以如果没有 nonlocal 声明的话,会报错 local variable ‘count’ referenced before assignment

示例3:
我们需要使用 nonlocal 将 count 和 total 变成自由变量

def make_average():
    count = 0
    total = 0
    def average(value):
        nonlocal count, total
        count += 1
        total += value
        return total / count
    return average
# 开始调用.返回一个函数对象,并且这个对象是一个闭包
avg = make_average()
print(avg(10))
print(avg(20))

#-----output-----------------------
10.0
15.0

三,装饰器

装饰器用来’装饰’一个函数,为函数添加额外的功能。
装饰器接收一个函数作为参数,装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。
举例, 我们定义一个装饰器,用来计算并且显示每个函数运行的时间。

import time
def decorate(func):
    def wrapper(*args):
        to = time.perf_counter()
        result = func(*args)
        t1 = time.perf_counter() - to
        print(f'运行时间为:{t1}')
        return result
    return wrapper
@decorate
def foo(n):
    i = 0
    while i < n*n:
        i += 1
    return i
print(foo(600))
# 程序的运行时间为:0.022877089999383315
# 360000

@decorate是一个语法糖, 等同于 func = decorate(func), 所以此时 func 是 wrapper 函数的引用.

猜你喜欢

转载自blog.csdn.net/TFATS/article/details/107981048