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的使用案例:
-
完全展开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
-
列表分组
这个就是输入一个列表, 然后输入分成几组, 返回分组后的列表。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:不带默认值的位置参数缺一不可
函数调用时,根据函数定义的参数位置来传递参数,是最常见的参数类型。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:关键字参数必须在位置参数右边
在调用 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:对同一个形参不能重复传值
def f(a,**b): pass In [113]: f(1,width=20.,width=30.) SyntaxError: keyword argument repeated
-
规则 4:默认参数的定义应该在位置形参右面
在定义函数时,可以为形参提供默认值。对于有默认值的形参,调用函数时,如果为该参数传值,则使用传入的值,否则使用默认值。如下 b 是默认参数:def f(a,b=1): print(f'a:{a}, b:{b}')
默认参数通常应该定义成不可变类型。
-
规则 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:可变关键字参数不能传入位置参数
如下,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类参数使用解释, 并整理了常见的几种传参报错情况, 这些东西当然不是一下子就能都使用的, 需要慢慢消化, 然后用到实际的生活中, 至少看一些文档的时候再遇到各种*
, **
的这种参数不慌了吧。