函数使用(三)
前言
- 这一篇是几块零碎但很常用的知识:条件表达式与短路求值、类型判断、
is与==、匿名函数lambda、闭包、装饰器、生成器。 - 装饰器和闭包这两节术语容易混:「带参数的函数被装饰」和「带参数的装饰器」不是一回事,文中专门标了出来。
零碎知识
-
灵活的 if-else
1
2
3
4
5
6
7
8if True:
print('真')
else:
print('假')
# 条件表达式(三元):把 if-else 写成一个值
a = 3 if False else 5
print(a) # 5多个条件串起来写,比一层层
if紧凑(注意链式比较90 <= score <= 100是 Python 的写法):1
2
3
4
5
6
7
8# 示例:成绩等级评价
for score in (95, 82, 61, 30, 120):
evaluation = ('优秀' if 90 <= score <= 100 else
'良好' if 70 <= score < 90 else
'及格' if 60 <= score < 70 else
'不及格' if 0 <= score < 60 else
'请输入有效的成绩(0-100)')
print(score, evaluation)输出:
1
2
3
4
5
6
7真
5
95 优秀
82 良好
61 及格
30 不及格
120 请输入有效的成绩(0-100) -
灵活的 or
1
2
3
4
5
6
7# or / and 是短路的:能定结果就不再往后算
# 而且返回的是「决定结果的那个操作数」,不一定是布尔值
a = False or 2
print(a) # 2
print(0 or '' or '兜底') # 兜底 —— 常见的默认值写法
print(1 and 5) # 5
print(not a) # False —— not 才一定返回布尔输出:
1
2
3
42
兜底
5
False -
类型判断
1
2
3
4
5
6
7a = 10
print(type(a)) # <class 'int'>
# 判类型优先用 isinstance:它认子类,也支持一次传多个类型
print(isinstance(a, int)) # True
print(isinstance(a, float)) # False
print(isinstance(a, (int, float))) # Truetype(a) == int这种写法能用但不推荐:bool是int的子类,
isinstance(True, int)是 True,而type(True) == int是 False,两者在继承关系上会给出不同答案;判类型统一用isinstance更稳。 -
判断是否是同一个对象
1
2
3
4
5
6
7
8a = [1, 2, 3]
b = a # 同一个对象:b 只是另一个名字
c = a.copy() # 内容一样,但是新对象
print(a == b, a is b) # True True
print(a == c, a is c) # True False —— 相等不一定是同一个
c.append(4)
print(a) # [1, 2, 3] 改 c 不影响 aa is b判的是「同一个对象」,等价于id(a) == id(b),但更推荐写is:
id()是会被复用的(对象销毁后新对象可能拿到同一个地址),拿两个临时对象的id比大小可能得出假相等。
is的正当用途是跟单例比:x is None、x is not None。
整体示例
1 | # 1) 条件表达式:把 if-else 写成一个值 |
实跑输出:
1 | 链式条件表达式: 良好 |
匿名函数
-
函数可以像普通变量一样赋值
1
2
3
4
5
6
7
8
9
10
11
12
13# 函数可以像普通变量一样赋值
def hello():
print('hello world')
print(hello.__name__)
a = hello
print(a.__name__)
hello()
a() -
函数可以作为参数传递,拓展函数的功能
1
2
3
4
5
6
7
8
9
10# 函数可以作为参数传递
def add(a, b):
return a + b
def calc(a, b, func):
return func(a, b)
print(calc(2, 3, add)) -
场景:当函数作为另一个函数的参数,只使用一次也得定义,否则无法使用,于是匿名函数就出现了
-
定义:就是没有名字的函数,使用 lambda 关键字定义
-
格式:
- 以 lambda 开头
- 后面跟上该匿名函数的参数,多个使用逗号隔开
- 最后一个参数后面跟上冒号 :
- 冒号的后面跟上一个表达式,这个表达式就是返回值,不需要使用return
-
示例1:
1
2
3
4
5
6
7def calc(a, b, func):
return func(a, b)
# mul = lambda x, y: x * y
# print(calc(3, 5, mul)) ← 只为这一次调用而定义的具名函数,可以省掉
print(calc(3, 5, lambda x, y: x * y)) # 15 -
示列2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16students = [
{'name': 'xiaowang', 'age': 15, 'height': 150},
{'name': 'xiaodu', 'age': 14, 'height': 145},
{'name': 'xiaopang', 'age': 12, 'height': 140},
{'name': 'banhua', 'age': 13, 'height': 155},
]
# def d_key(d):
# return d['height']
# students.sort(key=d_key) ← 只为排序临时定义一个函数,不如直接写 lambda
# key 是一个函数:收一个元素,返回「拿来比较的那个值」
students.sort(key=lambda x: x['height'])
for s in students:
print(s['height'], end=' ')
print() # 140 145 150 155 -
整体示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31# 1) 函数是对象:能赋值、能传参、能返回
def add(a, b):
return a + b
alias = add
print('赋值给别的名字后还能用:', alias(2, 3), ' __name__ 还是:', alias.__name__)
def calc(a, b, func):
return func(a, b)
print('当参数传进去:', calc(2, 3, add))
# 2) lambda:只有一个表达式的匿名函数,表达式的值就是返回值
print('lambda 当参数:', calc(3, 5, lambda x, y: x * y))
print('等价具名函数结果一致:', add(2, 3) == alias(2, 3))
# 3) 元素本身没法比较时,用 key 指定「拿什么比」
students = [
{'name': 'xiaowang', 'age': 15, 'height': 150},
{'name': 'xiaodu', 'age': 14, 'height': 145},
{'name': 'xiaopang', 'age': 12, 'height': 140},
{'name': 'banhua', 'age': 13, 'height': 155},
]
students.sort(key=lambda x: x['height'])
print('按身高升序:', [s['height'] for s in students])
print('按年龄降序:', [s['age'] for s in sorted(students, key=lambda x: -x['age'])])
print('sorted 返回新列表,原列表不动:', [s['height'] for s in students] == [140, 145, 150, 155])
# 4) map / filter 也是「函数当参数」,配 lambda 最省事
print('map:', list(map(lambda x: x * x, [1, 2, 3])))
print('filter:', list(filter(lambda x: x % 2, [1, 2, 3, 4])))实跑输出:
1
2
3
4
5
6
7
8
9赋值给别的名字后还能用: 5 __name__ 还是: add
当参数传进去: 5
lambda 当参数: 15
等价具名函数结果一致: True
按身高升序: [140, 145, 150, 155]
按年龄降序: [15, 14, 13, 12]
sorted 返回新列表,原列表不动: True
map: [1, 4, 9]
filter: [1, 3]
练习(不强求)
试着自己封装一个列表的sort函数,接受参数:排序对象、key、reverse
闭包
-
定义:
- 外部函数中定义一个内部函数,
- 内部函数中使用了外部函数的变量,
- 外部函数将内部函数作为返回值返回。
-
示例:四个例子逐个对比,只有后三个是闭包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33# ① 这不是闭包:内层没用到外层的变量
def wai1():
def nei():
print('内部函数')
return nei
a = wai1()
a()
# ② 是闭包:内层用了外层的 n
def wai2(n):
def nei():
return n * n
return nei
print(wai2(3), wai2(5)) # 记住不同的 n,就是两个不同的函数
# ③ 是闭包:记住的是一串参数
def wai3(*args):
def nei():
return sum(args)
return nei
print(wai3(1, 2, 3)())
# ④ 闭包最实用的样子:y = a*x + b,先把 a、b 定下来,函数留着以后用
def line_maker(a, b):
def line(x):
return a * x + b
return line
f1 = line_maker(3, 5) # f1 <=> y = 3*x + 5
print(f1(3), line_maker(5, 8)(6), line_maker(2, 3)(4))输出:
1
2
3
4内部函数
<function wai2.<locals>.nei at 0x...> <function wai2.<locals>.nei at 0x...>
6
14 38 11② 那一行打印的是函数对象本身(忘了加括号调用),地址每次运行都不同,这里只示意形式。想看闭包「记住」了什么,可以查
f1.__closure__。 -
作用:提高代码的复用度——把「一部分参数先定好、剩下的以后给」这件事变成一个个专用函数。
-
整体示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29# 闭包三条件:外层定义内层、内层用了外层的变量、外层把内层返回出去
def make_line(a, b): # y = a*x + b
def line(x):
return a * x + b
return line
f1 = make_line(3, 5)
f2 = make_line(5, 8)
print('f1(3) =', f1(3), ' f2(6) =', f2(6), ' 一次调用:', make_line(2, 3)(4))
print('被记住的变量:', [cell.cell_contents for cell in f1.__closure__])
# 内层改外层变量要 nonlocal:这里做出一个「计数器」
def counter(start=0):
n = start
def step():
nonlocal n
n += 1
return n
return step
c1, c2 = counter(), counter(100)
print('两个计数器互不干扰:', c1(), c1(), c2(), c2())
# 经典陷阱:循环里建的闭包共享同一个变量
fs = [lambda: i for i in range(3)]
print('以为会得到 0,1,2,其实全是:', [f() for f in fs])
fixed = [lambda i=i: i for i in range(3)] # 用默认参数在定义时把值定下来
print('用默认参数绑定后:', [f() for f in fixed])实跑输出:
1
2
3
4
5f1(3) = 14 f2(6) = 38 一次调用: 11
被记住的变量: [3, 5]
两个计数器互不干扰: 1 2 101 102
以为会得到 0,1,2,其实全是: [2, 2, 2]
用默认参数绑定后: [0, 1, 2]
装饰器
-
作用:当我们想要增强已有函数的功能,但不想(无法)修改原函数,可以使用装饰器解决
-
使用:
- 先写一个装饰器,就是一个函数,该函数接受一个函数作为参数,返回一个闭包,而且闭包中执行传递进来的函数,闭包中可以在函数执行的前后添加一些内容。
- 在需要装饰的函数前添加 @装饰器名 就可以使用了,如:
1
2
3# 等价于 show = zhuangshiqi(show)
def show():
print('原函数的内容') - 再使用原函数时,就已经是装饰过的了
-
示例1:无参无返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16# 装饰器 = 吃一个函数、再返回一个函数的函数
def zhuangshiqi(func):
def wrapper():
print('原函数前边添加的内容')
func()
print('原函数后边添加的内容')
return wrapper
# 等价于 show = zhuangshiqi(show)
def show():
print('原函数的内容')
show() -
示例2:被装饰的函数带参数(
*args, **kwargs原样透传)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20# 装饰器 = 吃一个函数、再返回一个函数的函数
def zhuangshiqi(func):
# 参数原样转发,被装饰函数的签名不用管
def wrapper(*args, **kwargs):
# 在原函数之前插内容
print('今天天气不错,出门就打到车了')
# 这里才真正调用原函数
func(*args, **kwargs)
return wrapper
# 等价于 test = zhuangshiqi(test)
def test(n):
print('我的幸运数字是:{}'.format(n))
# 调用的是包好之后的那层
test(7)注意名字:这一节是「带参数的函数被装饰」,不是「带参数的装饰器」。真正的带参数装饰器要再套一层(
@repeat(3)那种),见下面的整体示例。 -
示例3:要把返回值传出去
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17# 装饰器 = 吃一个函数、再返回一个函数的函数
def zhuangshiqi(func):
# 参数原样转发,被装饰函数的签名不用管
def wrapper(*args, **kwargs):
# 这里才真正调用原函数
return func(*args, **kwargs) + 10 # 忘了 return 的话,原函数的结果就丢了
return wrapper
# 等价于 pingfang = zhuangshiqi(pingfang)
def pingfang(n):
return n * n
print(pingfang(4)) # 26 = 16 + 10还有个必加的东⻄:
@functools.wraps(func)。不加它,被装饰函数的__name__、__doc__都会变成wrapper,报错信息和调试输出全乱:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import functools
def ok(func):
# 参数原样转发,被装饰函数的签名不用管
def wrapper(*args, **kwargs):
# 这里才真正调用原函数
return func(*args, **kwargs)
return wrapper
def show():
"""原本的文档"""
print(show.__name__, show.__doc__) # show 原本的文档 -
整体示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86import functools
import time
def log(func): # 最基础的装饰器
# 不加这行,被装饰函数的名字会变成 'wrapper'
# 参数原样转发,被装饰函数的签名不用管
def wrapper(*args, **kwargs): # 原函数的参数照样收
print('[调用] %s(%s, %s)' % (func.__name__, args, kwargs))
# 这里才真正调用原函数
return func(*args, **kwargs) # 别忘了把返回值传出去
return wrapper
def add(a, b):
"""两数相加"""
return a + b
print('add(2, 3) =', add(2, 3))
print('加了 wraps 才保得住名字和文档:', add.__name__, '/', add.__doc__)
def repeat(times): # 带参数的装饰器 = 装饰器工厂,多套一层
def deco(func):
# 参数原样转发,被装饰函数的签名不用管
def wrapper(*args, **kwargs):
for _ in range(times):
# 这里才真正调用原函数
result = func(*args, **kwargs)
return result
return wrapper
return deco
def greet(name):
print('hi', name)
return name
print('repeat 的返回值仍在:', greet('bob'))
def order(tag):
def deco(func):
# 参数原样转发,被装饰函数的签名不用管
def wrapper(*args, **kwargs):
print('进入', tag)
# 这里才真正调用原函数
return func(*args, **kwargs)
return wrapper
return deco
def target():
print('原函数')
print('多个装饰器:离函数近的先包进去,执行时外层先进入')
target()
def timed(func): # 实用例子:计时装饰器
# 参数原样转发,被装饰函数的签名不用管
def wrapper(*args, **kwargs):
start = time.perf_counter()
# 这里才真正调用原函数
result = func(*args, **kwargs)
print('%s 耗时非负: %s' % (func.__name__, time.perf_counter() - start >= 0))
return result
return wrapper
def work(n):
return sum(range(n))
print('work(1000) =', work(1000))实跑输出:
1
2
3
4
5
6
7
8
9
10
11
12
13[调用] add((2, 3), {})
add(2, 3) = 5
加了 wraps 才保得住名字和文档: add / 两数相加
hi bob
hi bob
hi bob
repeat 的返回值仍在: bob
多个装饰器:离函数近的先包进去,执行时外层先进入
进入 外层
进入 内层
原函数
work 耗时非负: True
work(1000) = 499500
生成器(generator)
-
列表推导式可以快速生成列表
1
2
3# 数据量小,一次算完没问题
l = [i for i in range(10)]
print(len(l)) # 10数据量大时,列表推导会一次性把所有元素都装进内存,占用会突然涨上去。
-
为了解决内存突然增大问题,Python 中引入了生成器
-
产生方式:
-
将列表生成式的 []改为 ()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21# 数据量非常小,内存占用不大
l = [i for i in range(10)]
# 数据量特别大,会造成内存占用突然增大
# l2 = [i for i in range(10000)]
# 生成器:把方括号换成圆括号
g = (i for i in range(3))
print(type(g).__name__) # generator
# 用 next 一个一个取,取完再取抛 StopIteration
print(next(g), next(g))
try:
next(g); next(g)
except StopIteration:
print('取完了 -> StopIteration')
# 也可以 for 遍历(生成器只能遍历一遍)
for i in (j for j in range(3)):
print(i, end=' ')
print()输出:
1
2
3
4generator
0 1
取完了 -> StopIteration
0 1 2 -
通过在函数中使用 yield关键字
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17def test(n):
l = []
for i in range(1, n + 1):
l.append(i)
return l
# print(test(5))
def test2(n):
for i in range(1, n + 1):
yield i
t = test2(5)
for j in t:
print(j)
-
-
特性:
- 惰性求值:定义时不计算,取的时候才算一个,所以内存占用与数据量无关。
- 用
next()一次取一个,取完再取抛StopIteration(for循环正是靠这个信号结束的,所以它不会报错)。 - 只能遍历一遍:耗尽之后再用
for或list()就是空的;要反复用就转成列表存下来。 - 可以
list(gen)转成列表,也可以直接for遍历。 - 生成器函数里
yield之前的代码在第一次next时才执行,yield会暂停、下次从暂停处继续。 - 补充两个写法:
yield from 可迭代对象(省一层循环)、gen.send(值)(从外面往生成器里塞值)。
-
整体示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49import sys
# 1) 列表推导一次算完全部,生成器表达式要一个算一个
lst = [i * i for i in range(10)]
gen = (i * i for i in range(10))
print('列表推导结果:', lst)
print('生成器对象:', type(gen).__name__)
print('同样 100 万个元素,列表占的字节远多于生成器:',
sys.getsizeof(list(range(10 ** 6))) > 1000 * sys.getsizeof((i for i in range(10 ** 6))))
# 2) 生成器只能走一遍:next 取,取完抛 StopIteration
g = (i for i in range(2))
print('next:', next(g), next(g))
try:
next(g)
except StopIteration:
print('取完再取 -> StopIteration')
print('再 for 一遍,什么都不产出:', [i for i in g])
# 3) yield:函数每次暂停在这里,下次从暂停处继续
def up_to(n):
print('开始')
for i in range(1, n + 1):
yield i
print('结束')
it = up_to(3)
print('调用函数不执行函数体,拿到的是:', type(it).__name__)
print('for 遍历才推进:', list(it))
# 4) yield from 把可迭代对象逐个产出,省一层循环
def flatten(seq):
for part in seq:
yield from part
print('yield from 拍平:', list(flatten([[1, 2], [3], [4, 5]])))
# 5) send 可以从外面往生成器里塞值
def echo():
while True:
got = yield
if got is None:
return
print('收到', got)
e = echo()
next(e)
e.send('a')
e.send('b')实跑输出:
1
2
3
4
5
6
7
8
9
10
11
12
13列表推导结果: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
生成器对象: generator
同样 100 万个元素,列表占的字节远多于生成器: True
next: 0 1
取完再取 -> StopIteration
再 for 一遍,什么都不产出: []
调用函数不执行函数体,拿到的是: generator
开始
结束
for 遍历才推进: [1, 2, 3]
yield from 拍平: [1, 2, 3, 4, 5]
收到 a
收到 b