Self-study Python Fish-C Note11 P45to47

函数 (part 2)

**本节主要讲函数的 作用域(scope),闭包,装饰器 ** \

作用域 (scope) P45

作用域就是指一个变量可以被访问的范围,通常一个变量的作用域是由其在代码中被赋值的位置决定的

局部作用域

如果一个变量定义的位置是在一个函数里面,那么其作用域就仅限于该函数中

def fun1():
    x = 100
    print(x)
fun1()
# print(x) 这样在函数外无法调用 变量 x,会报错
100

全局作用域

在函数外部定义变量,那么其作用域就是全局的,也叫全局变量

x = 100
def fun1():
    print(x) # 具有全局作用域的变量在函数的内部,是可以访问到的
fun1() 
100
# 如果在函数内部有一个和全局变量一样名字的 局部变量。
# 那么在函数内部局部变量会覆盖同名的全局变量,
# 但是只要出了函数,局部变量就失效了此时打印的是全局变量的值
x = 100
def fun1():
    x = 200
    print(x)

fun1() # 此时打印的是函数内部局部变量的值 200
print(x) # 此时打印的是全局变量的值 100

## 事实上两个 x 并非同一个变量,只是由于作用域不同两者同名,但不同样
## 通过 id 函数 
x = 100
print(id(x))
def fun1():
    print(id(x))
fun1() # 在函数内部,如果没有同名变量覆盖,此时id就是上面的全局变量的id
def fun1():
    x = 200
    print(id(x)) # 如果函数内部有一个同名的局部变量,此时id就不一样了
fun1()
200
100
1225973454288
1225973454288
1225973457552

所以说全局变量可以在函数内部被访问到,但是却无法修改他的值。(我们在函数内部对其赋值操作,python会创建一个同名的局部变量进行覆盖)
这其实是 python 的一个保护机制

global 语句

在函数内部修改全局变量

x = 100
def fun1():
    global x # tell python, in this function all x are global variable
    x = 200
    print(x)
fun1()
print(x)
200
200

注意:在函数中去肆意的通过 global 去声明全局变量,并且修改全局变量的做法,是非常不提倡的
容易出现意想不到且难以排查的 bug

嵌套函数

示例:

def fun1():
    x = 100
    def fun2():
        x = 200
        print('In fun2, x=',x)
    print('In fun1 x=',x)
fun1()
# fun2() 这里在外部调用嵌套函数内部的函数会报错(找不到这个 fun2 的函数)
# 要想调用 fun2() 只能在 fun1() 中调用
print('-'*30)
def fun1():
    x = 100
    def fun2():
        x = 200
        print('In fun2, x=',x)
    fun2() # 嵌套内部的函数只能在 外部的函数内调用
    print('In fun1 x=',x) # 内部函数无法修改外部函数的变量,在外部函数 x 还是 100
fun1()
In fun1 x= 100
------------------------------
In fun2, x= 200
In fun1 x= 100
def fun1():
    x = 200
    def fun2():
        print(x) # 可以在内部函数访问外部函数的变量
        # x = 100 # 这个位置不能修改外部函数的变量
    fun2()
fun1()
200

nonlocal 语句

在内部函数去修改外部函数的变量

# 在内部函数修改外部函数作用域的值
def fun1():
    x = 100
    def fun2():
        nonlocal x
        x = 200
        print('In fun2, x=',x)
    fun2() # 嵌套内部的函数只能在 外部的函数内调用
    print('In fun1 x=',x)
fun1()
In fun2, x= 200
In fun1 x= 200

LEGB 规则

联想记忆:leg+B 单词leg(腿)+B
多个作用域的影响范围存在相互覆盖的情况,此时当冲突出现的时候,python 的解析规则
L: Local 局部作用域
E: Enclosed 嵌套函数的外层函数作用域
G: Global 全局作用域
B: Build-in 内置作用域
比如上面我们演示的,当局部作用域和全局作用域发生冲突的时候,python会使用局部作用域的变量,除非使用 Global 语句进行特别声明。当函数嵌套发生的时候,局部作用域又会覆盖外层函数的作用域,除非使用 nonlocal 语句进行声明
最后一个 B (最没地位), build-in,比如 BIF (build-in-function),如果我们起一个变量名和bif一样,就会覆盖这个内置函数, 如:

str = '我们把 str 函数给毁了'
print(str)
# str(100) 此时我们还想把 100 通过 str 这个 BIC build-in-class 转化成字符串就不好用了。此时 str(100) 这里会报错
# 因为前面我们把 str 作为一个变量名赋值了一个字符串,此时 str 变成了一个全局变量而不是 build-in。
# 根据 LEGB 原则,G在B前,所有当两者出现冲突的时候,G 把 B 给覆盖了

# 我们再恢复回来 str 
del str
str(100)
我们把 str 函数给毁了





'100'

闭包 (closure) P46

核心技巧:
(1)利用嵌套函数的外层作用域具有记忆能力的特性,让数据保存在外层函数的参数或者变量中。
(2)将内层函数作为返回值返回,从外部间接的调用内层的函数。

# 上面讲到内部函数需要在外部函数中调用
def fun1():
    x = 200
    def fun2():
        print(x)
    fun2() # 内部函数 fun2() 需要在外部 fun1() 调用
fun1()
print('-'*30)
# 如何不用通过 fun1() 调用 fun2()
# 使用 return 语句 将 fun2() 返回
def fun1():
    x = 200
    def fun2():
        print(x)
    return fun2 # 这里注意将函数作为返回值或者参数的话,不用加 (),只要写下名字, 函数只有在定义和调用的时候才需要加 ()
fun1() # 此时得到 fun2 函数的一个引用
200
------------------------------





<function __main__.fun1.<locals>.fun2()>
fun1()() # 此时就调用了 fun2
200
# 不通过 fun1() 函数来调用 fun2()
# 可以调用 fun1() 函数,并将结果赋值给一个变量
q = fun1()
q()
200

函数内部定义的变量,在函数调用完后应该失去意义。但这里这里我们调用了 fun1() 函数,将其结果赋值给 q 变量。理论上 fun1() 函数拥有的变量x 也应该消失,但是我们却通过 q 给访问到了。(LEGB的E拥有的特性)
即对于嵌套函数来说,外层形式的作用域是会通过某种形式保存下来的。尽管这个函数已经调用完了,但是外层作用域的变量会保存下来,并不会像局部作用域一样调用完就消失了。

工厂函数 (闭包)

通过上文 E 的特性,利用嵌套函数实现工厂的功能

# 例子 1
def power(exp):
    def exp_of(base):
        return base**exp # ** is 幂运算
    return exp_of

square = power(2)
cube = power(3)

print(square(2))
print(square(5))
print(cube(2))
print(cube(5))
# 这里 power 就像是一个工厂,由于参数的不同得到两个不同的生产线 square (返回参数平方)和 cube(返回参数立方)。 
# 由于嵌套函数的外层作用域会被保存下来,
# 在执行 square = power(2) 语句的时候,square 变量指向的 exp_of() 函数他就记住了外层函数的 exp 参数是2
# 而在执行 cube = power(3) 语句的时候,cube 变量指向的 exp_of() 函数的 exp 参数则是 3
4
25
8
125

我们利用内层函数能够记住外层函数的作用域的特性,使用 nonlocal 语句,让其可以修改到外层函数作用域的变量,我们就可以实现一个带记忆功能的函数

# 例子 2
# 使用 nonlocal 语句可以让嵌套函数的内层函数去修改到外层函数的变量
def outer():
    x = 0
    y = 0
    def inner(x1,y1):
        nonlocal x,y
        x+=x1 # x=x+x1
        y+=y1
        print(f'now x = {x}, y={y}')
    return inner
move = outer()
move(1,2)
move(-2,2)
now x = 1, y=2
now x = -1, y=4

闭包来实现:比如在游戏开发中,我们希望将游戏角色的移动位置保护起来,不希望被其他函数轻易的修改到。

# 例3
# 游戏角色撞墙反弹
origin = (0,0) # 这是原点
legal_x = [-100,100] # 限定 x 轴移动范围
legal_y = [-100,100] # 限定 y 轴移动范围

def create(pos_x=0, pos_y=0): # 初始化位置 是 0,0
    def moving(direction,step): # 定义一个实现角色移动的函数,direction 表示方向,1表示向上或者右,-1表示向下或者左
        # direction 是一个 [x,y] 的列表,xy取值为 (-1,0,1)。向右[1,0],向左[-1,0],向上[0,1],向下[0,-1]
        nonlocal pos_x, pos_y
        new_x = pos_x+direction[0]*step
        new_y = pos_y+direction[1]*step
        
        # 检测是否超出边界,如果超出则撞墙反弹
        if new_x < legal_x[0]:
            pos_x = legal_x[0]-(new_x - legal_x[0])
        elif new_x > legal_x[1]:
            pos_x = legal_x[1]-(new_x - legal_x[1])
        else:
            pos_x = new_x
        
        if new_y < legal_y[0]:
            pos_y = legal_y[0] - (new_y - legal_y[0])
        elif new_y > legal_y[1]:
            pos_y = legal_y[1] - (new_y - legal_y[1])
        else:
            pos_y = new_y
        
        return pos_x, pos_y
    return moving
move = create()
print('向右移动20步后,位置是:', move([1,0],20))
print('向上移动120步后,位置是:', move([0,1],120)) # 到100,撞墙反弹了
print('向右下脚移动88步后,位置是:', move([1,-1],88))
向右移动20步后,位置是: (20, 0)
向上移动120步后,位置是: (20, 80)
向右下脚移动88步后,位置是: (92, -8)
move_id2 = create(10,10)
print('向左移动120步后,位置是:', move_id2([-1,0],120))
print('向左上移动120步后,位置是:', move_id2([-1,1],120)) 
print('向左下脚移动200步后,位置是:', move_id2([-1,-1],200))
向左移动120步后,位置是: (-90, 10)
向左上移动120步后,位置是: (10, 70)
向左下脚移动200步后,位置是: (-10, -70)

装饰器 P47

装饰器的本质 = 闭包 + 把函数作为参数传递给另外一个函数

把函数作为参数传给另一个函数

def test_func():
    print('正在调用 test_func 函数')
def report(func):
    print('现在开始调用函数')
    func() # 加 () 完成函数的调用
    print('函数调用完成')
report(test_func) # 通过 report函数调用 test_func。函数作为参数的时候不用加()
现在开始调用函数
正在调用 test_func 函数
函数调用完成
# 例1
## 统计函数的运行时间的函数
import time
def time_master(func):
    print('开始运行程序')
    start = time.time() # 通过time模块的time()函数,来获取当前的时间戳
    func()
    stop = time.time()
    print('程序运行结束')
    print(f'一共耗费: {(stop-start):.2f}秒')
    
def test_func():
    time.sleep(2) # 让程序啥也不干,就睡两秒
    print('hi')
    
time_master(test_func)
开始运行程序
hi
程序运行结束
一共耗费: 2.01秒

这样我们可以计算函数的运行时间,但是这并不是最优的方案,因为每次想要计算函数的运行时间,都要显示的调用 time_master()函数。更好的方案是在调用 test_func() 函数的时候,它可以自觉的执行 time_master() 函数。这里用到 python 的 装饰器

装饰器

# 例1
def time_master(func):
    def call_func():
        print('start')
        start = time.time()
        func()
        stop = time.time()
        print('end')
        print(f'in total, we used {(stop-start):.2f} second')
    return call_func

@time_master # 添加装饰器,在不修改原来代码的基础上实现 额外的功能
def test_func():
    time.sleep(2)
    print('hi')

test_func()
start
hi
end
in total, we used 2.01 second

这里:@装饰器名 (如:@time_master )是一个语法糖。其原本的形式形式应该是:
语法糖:语法糖是一种特殊的语法,对语言的功能本身无影响,但是为让程序有更好的易用性,简洁性,可读性和便捷性。(如 f字符串 是 format函数的语法糖)

# 例2
def time_master(func):
    def call_func():
        print('start')
        start = time.time()
        func()
        stop = time.time()
        print('end')
        print(f'in total, we used {(stop-start):.2f} second')
    return call_func

def test_func():
    time.sleep(2)
    print('hi')

test_func = time_master(test_func) # 这里调用 time_master,然后把 test_func 作为参数传进去
test_func()
# 我们看回函数的定义过程,time_master函数是闭包,当我们调用time_master时候,他并不会去执行内部的 call_func,而是将其返回
# 我们这里调用了 time_master 并且将 test_func 给传递进去,此时并不会调用而是返回其内部的 call_func
# 把内部返回的 call_func 赋值给 新的 test_func,下面再调用 test_func 就相当于调用 call_func(即开始运行程序,获取时间戳......)
start
hi
end
in total, we used 2.02 second

如一开始的例子(例1),装饰器的语法糖就是在函数的定义上面加一个 @ 再加上装饰器的名字,在例1里是 time_master。如例1,这样我们后面再调用 test_func()的时候并不是直接去调用 test_func,而是把 test_func 这个函数作为一个参数,塞到上面的装饰器中,然后去调用这个装饰器。

多个装饰器 用在同一个函数上

注意调用的顺序

# 例2
def add(func):
    def inner():
        x = func()
        return x+1
    return inner

def cube(func):
    def inner():
        x = func()
        return x*x*x
    return inner

def square(func):
    def inner():
        x = func()
        return x*x
    return inner

@add
@cube
@square
def test():
    return 2

test()
# 相当于先 square 2*2=4,再 cube 4*4*4=64, 再 add 64+1=65
65
# 列2 不用语法糖
def test():
    return 2

s1 = square(test)
s2 = cube(s1)
s3 = add(s2)
s3()
65

如何给装饰器传递参数

装饰器的参数通过多加一层嵌套来实现

# 例3
def logger(msg):
    def time_master(func):
        def call_func():
            start = time.time()
            func()
            stop = time.time()
            print(f'{msg} 一共耗费了 {(stop-start):.2f}')
        return call_func
    return time_master

@logger(msg='A')
def funA():
    time.sleep(1)
    print('funA is running')
    
@logger(msg= 'B')
def funB():
    time.sleep(1)
    print('funB is running')
    
funA()
funB()
funA is running
A 一共耗费了 1.00
funB is running
B 一共耗费了 1.01
# 拆掉语法糖来看
def logger(msg):
    def time_master(func):
        def call_func():
            start = time.time()
            func()
            stop = time.time()
            print(f'{msg} 一共耗费了 {(stop-start):.2f}')
        return call_func
    return time_master

def funA():
    time.sleep(1)
    print('funA is running')
    
def funB():
    time.sleep(1)
    print('funB is running')
    
funA = logger(msg='A')(funA) # logger 是装饰器最外层,装饰器最外层需要传递参数 msg
# logger(msg='A') 得到的是 time_master 函数的引用(3层函数中间层)
# 我们要得到的是 call_func 的引用,所以要再调用一层 logger(msg='A')(funA)
funB = logger(msg='B')(funB)

funA()
funB()
funA is running
A 一共耗费了 1.01
funB is running
B 一共耗费了 1.00

没有参数的装饰器为 例1,带参数的装饰器 例3。两者一对比,即知 例3 只是添加一次调用,然后通过这次调用将参数给传递进去。

附言:
题目:Self-study Python Fish-C Note-12 P45-P47
本文为自学B站上鱼C的python课程随手做的笔记。一些概念和例子我个人为更好的理解做了些查询和补充
因本人水平有限,如有任何问题,欢迎大家批评指正!
原视频链接:https://www.bilibili.com/video/BV1c4411e77t?p=8

  • 20
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值