函数使用(三)

前言

  • 这一篇是几块零碎但很常用的知识:条件表达式与短路求值、类型判断、is 与 ==、匿名函数 lambda、闭包、装饰器、生成器。
  • 装饰器和闭包这两节术语容易混:「带参数的函数被装饰」和「带参数的装饰器」不是一回事,文中专门标了出来。

零碎知识

  • 灵活的 if-else

    1
    2
    3
    4
    5
    6
    7
    8
    if 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
    4
    2
    兜底
    5
    False
  • 类型判断

    1
    2
    3
    4
    5
    6
    7
    a = 10
    print(type(a)) # <class 'int'>

    # 判类型优先用 isinstance:它认子类,也支持一次传多个类型
    print(isinstance(a, int)) # True
    print(isinstance(a, float)) # False
    print(isinstance(a, (int, float))) # True

    type(a) == int 这种写法能用但不推荐:bool 是 int 的子类,
    isinstance(True, int) 是 True,而 type(True) == int 是 False,两者在继承关系上会给出不同答案;判类型统一用 isinstance 更稳。

  • 判断是否是同一个对象

    1
    2
    3
    4
    5
    6
    7
    8
    a = [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 不影响 a

    a is b 判的是「同一个对象」,等价于 id(a) == id(b),但更推荐写 is:
    id() 是会被复用的(对象销毁后新对象可能拿到同一个地址),拿两个临时对象的 id 比大小可能得出假相等。
    is 的正当用途是跟单例比:x is None、x is not None。

整体示例

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
# 1) 条件表达式:把 if-else 写成一个值
score = 82
level = '优秀' if score >= 90 else '良好' if score >= 70 else '及格' if score >= 60 else '不及格'
print('链式条件表达式:', level)
print('一行 if-else 等价于:')
if score >= 90:
lv = '优秀'
else:
lv = '良好' if score >= 70 else '及格'
print(' ', lv, '与上面一致:', lv == level)

# 2) or / and 短路:返回的是操作数本身,不一定是布尔
print('False or 2 ->', False or 2)
print('0 or "" or "兜底" ->', 0 or '' or '兜底')
print('1 and 5 ->', 1 and 5)
name = ''
print('取默认值:', name or '匿名用户')

# 3) 类型判断:优先 isinstance,别用 type() ==
print('type(10) ->', type(10).__name__)
print('isinstance(10, int):', isinstance(10, int))
print('isinstance(True, int):', isinstance(True, int), '(bool 是 int 的子类)')
print('type(True) == int:', type(True) == int, '(所以判类型用 isinstance)')
print('多类型一次判:', isinstance(1.5, (int, float)))

# 4) 内容相同 ≠ 同一个对象
a = [1, 2, 3]
b = a
c = a.copy()
print('a == b:', a == b, ' a is b:', a is b)
print('a == c:', a == c, ' a is c:', a is c, '(copy 出来内容一样但不是同一个对象)')
print('改 c 不影响 a:', (c.append(4), a == [1, 2, 3])[1])
print('判 None 用 is:', (lambda v: v is None)(None))

实跑输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
链式条件表达式: 良好
一行 if-else 等价于:
良好 与上面一致: True
False or 2 -> 2
0 or "" or "兜底" -> 兜底
1 and 5 -> 5
取默认值: 匿名用户
type(10) -> int
isinstance(10, int): True
isinstance(True, int): True (bool 是 int 的子类)
type(True) == int: False (所以判类型用 isinstance)
多类型一次判: True
a == b: True a is b: True
a == c: True a is c: False (copy 出来内容一样但不是同一个对象)
改 c 不影响 a: True
判 None 用 is: True

匿名函数

  • 函数可以像普通变量一样赋值

    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
    7
    def 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
    16
    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},
    ]

    # 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
    5
    f1(3) = 14  f2(6) = 38  一次调用: 11
    被记住的变量: [3, 5]
    两个计数器互不干扰: 1 2 101 102
    以为会得到 0,1,2,其实全是: [2, 2, 2]
    用默认参数绑定后: [0, 1, 2]

装饰器

  • 作用:当我们想要增强已有函数的功能,但不想(无法)修改原函数,可以使用装饰器解决

  • 使用:

    • 先写一个装饰器,就是一个函数,该函数接受一个函数作为参数,返回一个闭包,而且闭包中执行传递进来的函数,闭包中可以在函数执行的前后添加一些内容。
    • 在需要装饰的函数前添加 @装饰器名 就可以使用了,如:
      1
      2
      3
      @zhuangshiqi  # 等价于 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


    @zhuangshiqi # 等价于 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)
    @zhuangshiqi
    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)
    @zhuangshiqi
    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
    15
    import functools

    def ok(func):
    @functools.wraps(func)
    # 参数原样转发,被装饰函数的签名不用管
    def wrapper(*args, **kwargs):
    # 这里才真正调用原函数
    return func(*args, **kwargs)
    return wrapper

    @ok
    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
    86
    import functools
    import time


    def log(func): # 最基础的装饰器
    @functools.wraps(func) # 不加这行,被装饰函数的名字会变成 'wrapper'
    # 参数原样转发,被装饰函数的签名不用管
    def wrapper(*args, **kwargs): # 原函数的参数照样收
    print('[调用] %s(%s, %s)' % (func.__name__, args, kwargs))
    # 这里才真正调用原函数
    return func(*args, **kwargs) # 别忘了把返回值传出去
    return wrapper


    @log
    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):
    @functools.wraps(func)
    # 参数原样转发,被装饰函数的签名不用管
    def wrapper(*args, **kwargs):
    for _ in range(times):
    # 这里才真正调用原函数
    result = func(*args, **kwargs)
    return result
    return wrapper
    return deco


    @repeat(3)
    def greet(name):
    print('hi', name)
    return name


    print('repeat 的返回值仍在:', greet('bob'))


    def order(tag):
    def deco(func):
    @functools.wraps(func)
    # 参数原样转发,被装饰函数的签名不用管
    def wrapper(*args, **kwargs):
    print('进入', tag)
    # 这里才真正调用原函数
    return func(*args, **kwargs)
    return wrapper
    return deco


    @order('外层')
    @order('内层')
    def target():
    print('原函数')


    print('多个装饰器:离函数近的先包进去,执行时外层先进入')
    target()


    def timed(func): # 实用例子:计时装饰器
    @functools.wraps(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


    @timed
    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
      4
      generator
      0 1
      取完了 -> StopIteration
      0 1 2
    • 通过在函数中使用 yield关键字

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      def 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
    49
    import 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
本文结束 感谢您的阅读
正在加载今日诗词....