Python进阶二: yield, nonlocal和global关键字以及Python函数5类参数的使用

1. 写在前面

今天整理python的三个关键字的简单使用和python函数的5类参数辨析, yield关键字有一个很重要的优点就是节省内存, 所以利用好yield, 有时候可以起到出其不意的效果, 而nonlocal关键字和global关键字有时候也会很有用。 Python函数的5类参数, 包括位置参数, 关键字参数, 默认参数, 可变位置参数, 可变关键字参数, 这些参数的使用往往让人望而生畏, 非常的灵活, 又极易报错, 所以也一块整理一下。

大纲如下

  • Python中三个重要的关键字(yield, nonlocal, global)
  • Python函数的5类参数

2. Python中三个重要的关键字

2.1 yield关键字

要想通俗理解 yield,可结合函数的返回值关键字 return,yield 是一种特殊的 return。说是特殊的 return,是因为执行遇到 yield 时,立即返回,这是与 return 的相似之处。

不同之处在于:下次进入函数时直接到 yield 的下一个语句,而 return 后再进入函数,还是从函数体的第一行代码开始执行

带 yield 的函数是生成器,通常与 next 函数结合用。下次进入函数,意思是使用 next 函数进入到函数体内。

# 举个栗子吧
def f():
    print('enter f...')
    return 'hello'

ret = f()    # 'enter f...'
ret      # hello

上面就是普通的函数的定义及使用。 下面看个带有yield的函数运行机制:

# 看下面新定义的f, 因为带有yield, 所以是生成器函数f:
def f():
    print('enter f...')
    yield 4
    print('i am next sentence of yield')

g = f()  # 这时候调用f什么也不会打印, 只会生成一个生成器对象g

# 如果想进入函数, 我们用next方法
next(g)    #  'enter f...'  4   这时候才进入函数体一次,由于碰到了yield, 就返回4, 出来了

# 此时再次调用next
next(g)   # i am next sentence of yield   这时候可以发现, 直接进入了yield的下面一句, 并会产生stopIteration异常, 这是告诉我们迭代器到头了

上面就是带有yield函数的运行机制, 当然这种函数有了一个新的名字叫做迭代器。 这种东西有什么好处呢? 很大的一个优点就是可以帮助节省内存空间, 比如我们要打印从1-10十个数字, 我们如果用迭代器的话, 可以这样写:

# 首先, 定义一个函数myrange:
def myrange(stop):
    start = 0
    while start < stop:
        yield start
        start += 1

# 使用生成器
for i in myrange(10):
    print(i)

这个是O(1)的空间复杂度,下面整理两个比较经典的yield的使用案例:

  1. 完全展开list
    这个就是, 如果列表里面有列表的时候, 可以把列表进行展开, 元素都存放到外面的列表中。思路就是遍历一遍列表, 判断一下, 如果当前元素是列表类型, 那么就重新调用这个函数, 否则, 返回元素即可。

    def deep_flatten(lst):
        for i in lst:
            if type(i) == list:
                yield from deep_flatten(i)    # 表示再进入到deep_flatten生成器
            else:
                yield i
    
    gen = deep_flatten([1, ['s', 3], 4, 5])
    for i in gen:
        print(i)       # 1 's' 3 4 5
    
  2. 列表分组
    这个就是输入一个列表, 然后输入分成几组, 返回分组后的列表。

    from math import ceil
    
    def divide_iter(lst, n):
        if n <= 0:
            yield lst
            return
        i, div = 0, ceil(len(lst) / n)  # div是计算每个分组的最大元素数
        while i < n:
            yield lst[i*div:(i+1)*div]
            i += 1
    list(divide_iter([1,2, 3, 4, 5], 0))   # [[1, 2, 3, 4, 5]]
    list(divide_iter([1, 2, 3, 4, 5], 2)) # [[1, 2, 3], [4, 5]]
    

    这是一个yield的经典使用案例, 可以达到O(1)的空间复杂度。

带yield的生成对象里面还封装了一个send方法, 下面拿一个例子来看这个东西干啥用:

# 带yield的生成器对象还封装了一个send方法
def f():
    print('enter f...')
    while True:
        result = yield 4
        if result:
            print('send me a value is: %d' %(result,))
            return
        else:
            print('no send')

g = f()  # 创建生成器对象, 什么也不打印
print(next(g))    # 进入 f,打印enter f...,并 yield 后返回值 4,并打印 4
print('ready to send')       
next(g)     #  no send
print(g.send(10))   # send 值 10 赋给 result,执行到上一次 yield 语句的后一句打印出 send me a value is:10

# 遇到 return 后返回,因为 f 是生成器,同时提示 StopIteration

通过以上分析,能体会到 send 函数的用法:它传递给 yield 左侧的 result 变量。return 后抛出迭代终止的异常,此处可看作是正常的提示。理解以上后,再去理解 Python 高效的“协程”机制就会容易多了。

2.2 nonlocal 关键字

关键词 nonlocal 常用于函数嵌套中,声明变量为非局部变量。 这个相对来说比较容易, 如下,函数 f 里嵌套一个函数auto_increase。实现功能:不大于 10 时自增,否则置零后,再从零自增。

# 函数 f 里嵌套一个函数auto_increase。实现功能:不大于 10 时自增,否则置零后,再从零自增。
def f():
    i = 0
    def auto_increase():
        nonlocal i    # 使用 nonlocal 告诉编译器,i 不是局部变量 , 如果没有这句话, 函数会报错, 说i没有在函数里面声明赋值
        if i >= 10:
            i = 0 
        i += 1
    ret = []
    for _ in range(28):
        auto_increase()
        ret.append(i)
    print(ret)

f() # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8]

2.3 global关键字

先回答为什么要有 global。一个变量被多个函数引用,想让全局变量被所有函数共享。有的伙伴可能会想这还不简单,这样写:

i = 5
def f():
    print(i)
def g():
    print(i)
f() # 5
g()  # 5

但是如果想通过函数修改i的话, 就需要加上global关键字, 告诉函数这个i是全局的。

def h():
    global i
    i += 1
h()    # 6
g()  # 6

3. Python函数的5类参数

下面梳理一下python函数里面的5类参数, 主要包括位置参数,默认参数, 关键字参数, 可变位置参数, 可变关键字参数, 这个之前在python幕后的不为人知(一)也有总结, 这里在使用的角度再复盘一下, 理解这些参数的使用规则, 可以写出灵活多变的很多函数。

定义函数 f,只有一个参数 a,a 既可能为位置参数,也可能为关键字参数,这取决于调用函数 f 的传参。

def f(a):
  print(f'a:{a}')

f(1)    # 这样调用 f,a 就是位置参数,并且 a 被赋值为 1
f(a=1) # 调用 f,a 就是关键字参数(注意这里的关键字参数和上面博客里面的还不一样, 那个关键字参数指的是这里的可变关键字参数),a 同样被赋值为 1

如果函数 f 定义为如下,有一个默认值 0:

def f(a=0):
    print(f'a:{a}')

# 这时候这个a就是默认参数了, 调用的时候, 有下面两种方式
f()    #  这时候a会使用默认值0
f(1)   #  这时候a会用传递的值1

如果函数 f 定义为,如下:

def f(a,*b,**c):
    print(f'a:{a},b:{b},c:{c}')

函数 f 的参数稍显复杂,但也是最常用的函数参数定义结构。

  • 出现带一个星号的参数 b,这是可变位置参数;
  • 带两个星号的参数 c,这是可变关键字参数。

可变表示函数被赋值的变量个数是变化的。例如,可以这样调用函数:

f(1,2,3,w=4,h=5)

参数 b 被传递 2 个值,参数 c 也被传递 2 个值。可变位置参数 b 被解析为元组,可变关键字参数 c 被解析为字典。a:1,b:(2, 3),c:{'w': 4, 'h': 5}

所以,称带星号的变量为可变的。 这里的可变关键字参数和上面博客里面的关键字参数是一个道理, 具体的原理那边详细一些。

下面总结,Python 中五类参数的传递赋值规则。

常见的报错有以下六类:

  • SyntaxError: positional argument follows keyword argument,位置参数位于关键字参数后面
  • TypeError: f() missing 1 required keyword-only argument: 'b',必须传递的关键字参数缺失
  • SyntaxError: keyword argument repeated,关键字参数重复
  • TypeError: f() missing 1 required positional argument: 'b',必须传递的位置参数缺失
  • TypeError: f() got an unexpected keyword argument 'a',没有这个关键字参数
  • TypeError: f() takes 0 positional arguments but 1 was given,不需要位置参数但却传递 1 个

下面总结,6 个主要的参数使用规则。

  1. 规则 1:不带默认值的位置参数缺一不可
    函数调用时,根据函数定义的参数位置来传递参数,是最常见的参数类型。

    def f(a):
      return a
    
    f(1) # 这样传递值,参数 a 为位置参数
    

    如下带有两个参数,传值时必须两个都赋值。

    def f(a,b):
        pass
    
    In [107]: f(1)
    
    TypeError: f() missing 1 required positional argument: 'b'
    
  2. 规则 2:关键字参数必须在位置参数右边
    在调用 f 时,通过键–值方式为函数形参传值。

    def f(a):
      print(f'a:{a}')
    
    f(a=1)  # 参数a变为了关键字参数
    

    但是,下面调用,就会出现:位置参数位于关键字参数后面的异常。

    def f(a,b):
        pass
    
    In [111]: f(a=1,20.)
    
    SyntaxError: positional argument follows keyword argument
    
  3. 规则 3:对同一个形参不能重复传值

    def f(a,**b):
        pass
    
    In [113]: f(1,width=20.,width=30.)
    
    SyntaxError: keyword argument repeated
    
  4. 规则 4:默认参数的定义应该在位置形参右面
    在定义函数时,可以为形参提供默认值。对于有默认值的形参,调用函数时,如果为该参数传值,则使用传入的值,否则使用默认值。如下 b 是默认参数:

    def f(a,b=1):
      print(f'a:{a}, b:{b}')
    

    默认参数通常应该定义成不可变类型。

  5. 规则 5:可变位置参数不能传入关键字参数
    如下定义的参数 a 为可变位置参数:

    def f(*a):
      print(a)
    
    # 下面两种调用都OK
    In [115]: f(1)
    (1,)
    
    In [116]: f(1,2,3)
    (1, 2, 3)
    
    # 但是下面这个不行
    In [117]: f(a=1)
    
    TypeError: f() got an unexpected keyword argument 'a'
    
  6. 规则 6:可变关键字参数不能传入位置参数
    如下,a 是可变关键字参数:

    def f(**a):
      print(a)
    

    调用方法:

    In [119]: f(a=1)
    {'a': 1}
    
    In [120]: f(a=1,b=2,width=3)
    {'a': 1, 'b': 2, 'width': 3}
    

    但是不能这么用:

    In [121]: f(1)
    
    TypeError: f() takes 0 positional arguments but 1 was given
    

4. 总结

这里小总一下, 今天整理的是python中三个比较常用的关键字, yield, nonlocal和global, 还有python函数的5类参数使用解释, 并整理了常见的几种传参报错情况, 这些东西当然不是一下子就能都使用的, 需要慢慢消化, 然后用到实际的生活中, 至少看一些文档的时候再遇到各种***的这种参数不慌了吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值