Python 一句话代码技巧(三)

文章目录

前面两篇笔记里介绍的内容都不过是开胃小菜,这一篇笔记里记录的才是硬菜,写一句话代码时真正实用的技巧。

lambda 匿名函数

lambda 关键字的用法如下 lambda [parameters]: expression,其中 expression 的结果直接作为返回值。这一结构决定了:

  • lambda 定义的匿名函数中只能有一个表达式
  • lambda 定义的匿名函数一定有返回值 下面是一个 lambda 的使用示例:
1print(list(map(lambda x: x + 1, [1, 1, 4, 5, 1, 4])))
2
3# output: [2, 2, 5, 6, 2, 5]

lambda函数经常与 map() 函数及其他需要以函数作为为参数的函数共同使用,用于提高代码的简洁性与灵活性。因为 lambda 会创建一个函数,这个匿名函数也可以使用常规的 function([parameters]) 的方式调用:

1print((lambda x: [i + 1 if i % 2 else i for i in x])([1, 1, 4, 5, 1, 4]))
2
3# output: [2, 2, 4, 6, 2, 4]

在上面这个例子中,x 为匿名函数的形式参数,后面的列表 [1, 1, 4, 5, 1, 4] 是传入函数的实际参数。

另外,尽管 PEP8 标准不建议将 lambda 创建的匿名函数作为一个“右值”,但是这样做并不算是语法错误。就像下面这样:

1f = lambda x, n: x ** n
2print(f(3, 3))
3
4# output: 27

这段代码就相当于

1def f(x, n):
2	return x ** n
3
4print(f(3, 3))
5
6# output: 27

前文提到,lambda 创建的匿名函数有两条限制,即只能有一条表达式和一定有返回值。但是实际上,这两条限制可以使用一些部分技巧规避掉。在 Python 中,如果有多条表达式被放到一个元组中,那么这些表达式会被依次执行。

1S = (lambda s: (print(s.lower()), s.upper())[-1])("JackGDN")
2print(S)
3
4# output:
5# jackgdn
6# JACKGDN

在上面这个例子中,print(s.lower())s.upper() 这两个表达式被依次放置于一个元组中,因此这两条语句会被依次执行,而这个匿名函数的返回值为元组的最后一个元素,即 s.upper() 的计算结果。通过这个方法,我们可以实现将多条表达式放进一个匿名函数中。在另一些情况中,我们不需要函数有返回值(例如 __init__() 方法),也可以使用类似的结构。

1lst = [1, 2, 3]
2(lambda lst: (print(lst), lst.append(4), None)[-1])(lst)
3print(lst)
4
5# output:
6# [1, 2, 3]
7# [1, 2, 3, 4]

只需将最后的返回值设为 None 即可。

通过 lambda 匿名函数,配合在前文中了解到的内容,我们可以解决一些有趣的问题。

从键盘输入一个n,代表方阵的阶,依次读入n行由空格和数字字符组成的字符串,将字符串中的每个数字字符转换成整形数据,存成方阵的一行,如果输入的数字字符数目少于n个,以0补足,如果超过n个,截取前n位。输出该方阵与其转置矩阵相乘后得到的新矩阵。 example: input: 5 3 2 1 7 6 8 11 2 4 12 5  4 3 1 7 6 5 1 output: [[99, 155, 19, 38, 3], [155, 310, 54, 109, 11], [19, 54, 26, 51, 4], [38, 109, 51, 110, 7], [3, 11, 4, 7, 1]]

这道题的一行代码写法如下:

 1(
 2    lambda order: print(
 3        *list(
 4            map(
 5                lambda matrix: [
 6                    [
 7                        sum([a * b for a, b in zip(matrix[i], matrix[j])])
 8                        for j in range(order)
 9                    ]
10                    for i in range(order)
11                ],
12                [
13                    [
14                        list(list(map(int, input().strip().split(" "))) + [0] * order)[
15                            :order
16                        ]
17                        for _ in range(order)
18                    ]
19                ],
20            )
21        )
22    )
23)(int(input()))

使用递归实现 while 循环

一切 while 循环都能够改写为递归形式。因为 while 循环不支持一行写法,因此只能通过函数递归调用,并结合 lambda 实现一行写法。例如说:

1index = 0
2summ = 0
3lst = [1, 1, 4, 5, 1, 4, 1, 9, 1, 9, 8, 10]
4while index < len(lst) and summ < 33:
5	summ += lst[index]
6	index += 1
7print(summ)
8
9# output: 36

将这个 while 循环改写为递归函数,就可以得到下面的代码:

1def recursive_sum(lst=[1, 1, 4, 5, 1, 4, 1, 9, 1, 9, 8, 10], index=0, summ=0):
2	if summ >= 33 or index >= len(lst):
3		return summ
4	else:
5		return recursive_sum(lst, index + 1, summ + lst[index])
6print(recursive_sum())
7
8# output: 36

随后将 if 语句改写为一个三元运算符:

1def recursive_sum(lst=[1, 1, 4, 5, 1, 4, 1, 9, 1, 9, 8, 10], index=0, summ=0):
2	return summ if summ >= 33 or index >= len(lst) else recursive_sum(lst, index + 1, summ + lst[index])
3print(recursive_sum())
4# output: 36

最后使用 lambda 就可以将这些内容写进一行了。

1print((recursive := lambda lst=[1, 1, 4, 5, 1, 4, 1, 9, 1, 9, 8, 10], index=0, summ=0: summ if summ >= 33 or index >= len(lst) else recursive_sum(lst, index + 1, summ + lst[index]))())
2
3# output: 36

不过需要注意,Python3 默认的递归深度为 1000。虽然这个限制可以通过 sys.setrecursionlimit()函数修改,但是太多层递归函数很有可能把你的 CPU 干破防(物理)。

这里出现了一个奇怪的 := ,这就是接下来要提到的海象运算符。

海象运算符

Python 中的代码有语句与表达式之分。一般来说,表达式可以被计算为一个值,而语句则用于执行每一个操作。例如说,3 > 1{1, 2} | {2, 3} 是表达式,因为这他们分别可以被计算为 True{1, 2, 3}。而 if 3 > 1 则是条件语句,a = {1, 2} | {2, 3} 是赋值语句。

前文提到,将多个表达式翻到一个元组中,这些表达式会依次执行。而海象运算符,就是将赋值语句变成赋值表达式执行。因此,海象运算符有以下使用场景:

1for i in range(num := 3):
2	print(i, num)
3
4# outut:
5# 0 3
6# 1 3
7# 2 3

或者

1import random
2print(num if (num := random.randint(-5, 5)) > 0 else 0)  # 注意海象运算符使用的位置,并且一定加括号
3
4# output:2

对于“一行代码”来说,最有用的还是放到 lambda 中执行:

1print((lambda x: (print(x), x := x + 4)[-1])(3)) 
2
3# output:
4# 3
5# 7

目前来说,海象运算符有诸多限制:

  • 只能在 Python 3.8 及更高版本使用
  • 不能直接当做赋值语句
  • 不能重载(和等号一样)
  • 不能写到推导式里,a = [i for i in range(x := 3)] 错!
  • 不能用于给可迭代对象的切片赋值,a[3] := 3 错!
  • 不能用于给类的属性赋值,myClass.myAttribute := 3 错!
  • 不能直接写到 lambda 匿名函数里,lambda x: x := 3 错!应该写成 lambda x: (x := 3) 这样写到元组里。这不是因为没加括号导致的语法解析错误,单纯是因为不能直接把赋值表达式写进匿名函数。

假如没有等号……(这是一个沙箱逃逸技巧)

前文提到,海象运算符在 Python 3.8 中加入。如果没有海象运算符,那么应该怎么办呢?我不禁思考,如果赋值不需要等号,那么岂不是写 Python 代码就不需要等号了?在使用 Python 写代码时,有以下情况会使用到 = 这个字符:

  • 赋值运算符,=+=-=*=/=**=//=%=
  • 比较运算符,==>=<=!=
  • 海象运算符,:=
  • 字符串
  • 定义带有默认值参数的函数
  • 调用带有默认值参数或关键字参数的函数

我们将这些等号按照从易到难的顺序逐条去除。

  • 海象运算符

不用海象运算符就好了,毕竟你也不会用到的。海象运算符的最大贡献就是将赋值语句变成了赋值表达式,而下面我会介绍另一种通过调用函数和方法的表达式取代赋值运算符的方法。

  • 字符串

等号的 ASCII 码为 61,只需要将字符串中所有需要等号的部分使用 chr(61) 替代即可。

  • 比较运算符

既然比较运算符都是可以重载的,那么我不妨直接使用魔法方法 __eq__()__ge__()__le__()__ne__() 取代四种带有等号的比较运算符。

  • 调用带有默认值参数和关键字参数的函数

调用这两种函数时,需要将格式类似于 parameter=value 的参数传入。在 Python 中,可以将字典解包得到这种参数和值的对应关系。例如执行 sorted([1, 3, 2, 4], **{'reverse': True}) 这行代码就可以得到 [4, 3, 2, 1] 的结果。

  • 定义带有默认值参数的函数

目前来说,这里出现的等号没有合适的办法去除,因为函数定义的时候不能使用解包字典的操作,换句话说,使用等号是定义默认值参数的唯一方法。对于这个问题比较好的解决方法是,不要使用默认值参数,而是使用可变参数 *args 和关键字参数 **kwargs 替代。

  • 赋值运算符

赋值运算符不可以被重载,但是有别的办法避免使用等号赋值。首先需要说明的是,和海象运算符一样,+=-=*=/=//=**=%= 都是“非必须”的,即他们都可以使用 = 替代,这样我们唯一需要思考的就是替换掉单独的 =。Python 中提供了两个内置函数 globals()locals(),这两个函数的返回值分别为存储全局变量和局部变量的字典。因此想要进行依次赋值运算,只需更新字典的内容。例如我想定义 a = 3,就可以使用 globals().update({'a': 3}) 来规避等号。此外,在一个类中,所有的属性都存储在 __dict__ 属性中,使用 self.__dict__.update({'a': 3}) 来取代 self.a = 3

想要去除等号,有一种终极方法,就是使用 exec() 函数 ;=)

__setitem__()__getitem__() 函数

刚才提到了不使用等号完成赋值操作的方法,但是上面那种方法也有问题。例如对于下面这段代码:

1lst = [1, 1, 4, 5, 1, 4]
2for i, num in enumerate(lst):
3	if num % 2:
4		lst[i] += 1
5print(lst)
6
7# output: [2, 2, 4, 6, 2, 4]

我们试着一行行将上面这段代码中的等号去除。

1globals().update({'lst': [1, 1, 4, 5, 1, 4]})
2for i, num in enumerate(lst):
3	if num % 2:
4		...

不对,我们是要修改 lst 的第 i 个值,必须要精确定位到这个位置,那么再使用 globals().update() 就显得力不从心了。于是有了 __getitem__() 方法和 __setitem__() 方法。这两个方法是用于实现切片功能的,例如 lst[i] = lst[i] + 1 实际上执行的是 lst.__setitem__(i, lst.__getitem__(i) + 1)。因此将上面那段代码补全之后就是

1globals().update({'lst': [1, 1, 4, 5, 1, 4]})
2for i, num in enumerate(lst):
3	if num % 2:
4		lst.__setitem__(i, num + 1)
5print(lst)
6
7# output: [2, 2, 4, 6, 2, 4]

前文还提到,海象运算符“不能用于给可迭代对象的切片赋值”,所以我们不妨用 a.__setitem__(3, 3) 来取代 a[3] := 3

setattr()getattr() 函数

前文提到,globals()locals() 函数分别以字典形式返回 Python 的全局变量和局部变量。那么问题来了,一个类中的属性应该存储到那里呢?

答案是,以上两个都不是。基类 object 中有一个 __dict__ 属性,类中所有的属性(除了它自己)都会以字典形式存储在这个 __dict__ 属性中。因此使用 myObject.__dict__.update({'value': myObject.__dict__['value'] + 1}) 替代 myObject.value += 1 是可行的……

不过我们有更简便的方法,及使用 setattr() 函数与 getattr() 函数。例如 setattr(myObject, 'value', getattr(myObject, 'value', -1) + 1)getattr() 函数的第三个参数为默认值,当类中不存在某个属性时则返回该默认值,可省略)就可以用来取代 myObject.value = myObject.value + 1 if hasattr(myObject, 'value') else 0

同理,使用 setattr() 函数和 getattr() 函数也可以解决海象运算符无法对属性赋值的问题。

此外,setattr() 函数也可以动态向类中添加方法,这一点我们未来会在描述符协议中再提。

type() 函数动态创建类

大多数人都知道使用 class 关键字可以创建类,也知道 type() 函数可以用于查看对象类型,但是不知道 type() 函数同样可以创建类。type() 函数创建类的语法如下:

1class type(name: str, bases: tuple, dict: dict, **kwds: dict)

其中 name 类的名称,bases 为继承的父类,dict 为类的属性和方法,*kwds 为额外参数,一般只有在定义元类时才会使用(使用 class 关键字定义一个类时也有这个参数,大多数情况下,直接继承自元类 type 时才会使用这个参数)。

使用示例:

 1class A:
 2
 3	def __init__(self, value):
 4		self.value = value + 1
 5
 6	def echo(self):
 7		print(self.value)
 8
 9
10a = A(int(input()))
11a.echo()
12
13# input: 13
14# output: 14

就可以写成

 1(lambda A: A(int(input())).echo())(
 2    type(
 3        "A",
 4        (object,),
 5        {
 6            "__init__": lambda self, value: setattr(self, "value", value + 1),
 7            "echo": lambda self: print(self.value),
 8        },
 9    )
10)  # 这其实是一行代码,为了直观展现其结构,将其格式化。
11
12# input: 13
13# output: 14

名称重整机制

在我初学 Python 时,老师便告诉我们,在定义类时,在属性或者方法名前加两条下划线,就可以将其定义为私有属性/方法,无法在外部访问。但是事实真的如此吗?请看下面的代码:

 1class AClass:
 2
 3    def __init__(self, value):
 4        self.__value = value
 5
 6    def __echo(self):
 7        print(self.__value)
 8
 9
10a = AClass(10)
11a._AClass__value = 20
12a._AClass__echo()
13
14
15# output: 20

a 中的私有属性 __value 被从外部修改了,__echo() 方法也能够从外部调用,原因就是 Python 会将类中的私有方法和私用重命名成类似于上面 _AClass__value_AClass__echo() 的样子。

__import__() 函数动态导入模块

__import__() 函数可以动态导入库,能够用于取代独占一行的 import 关键字。该函数的定义如下:

1__import__(name: str, globals: dict=None, locals: dict=None, fromlist: list=(), level: int=0)

该函数的返回值为模块对象。一般情况下,我们只会用到 name 参数,使用方法为 __import__('time').time()

使用生成器对象的 throw() 方法取代 raise 关键字

如果你想 raise 一个异常,但又不想多占用一行,就可以写一个 (_ for _ in ()).throw(Exception),前面的推导式可随意填写。不过 try-except-finally 的异常处理语句目前还没有办法压缩至一行。


实战训练:

将这段代码压缩进一行:

 1def binary_search(numbers, value):
 2    max_index = len(numbers) - 1
 3    min_index = 0
 4    while min_index <= max_index:
 5        mid_index = (min_index + max_index) // 2
 6        if numbers[mid_index] == value:
 7            return mid_index
 8        elif numbers[mid_index] < value:
 9            min_index = mid_index + 1
10        else:
11            max_index = mid_index - 1
12    return -1
13
14
15numbers = [10, 15, 20, 27, 41, 69]
16print(binary_search(numbers, 69))
17
18numbers = [13, 18, 54, 61, 78, 93]
19print(binary_search(numbers, 10))
20
21# output:
22# 5
23# -1

首先将 while 循环改写为一个递归函数:

 1def binary_search(numbers, value):
 2    max_index = len(numbers) - 1
 3    min_index = 0
 4
 5    def my_while(min_index, max_index, numbers, value):
 6        if min_index <= max_index:
 7            mid_index = (min_index + max_index) // 2
 8            if numbers[mid_index] == value:
 9                return mid_index
10            elif numbers[mid_index] < value:
11                return my_while(mid_index + 1, max_index, numbers, value)
12            else:
13                return my_while(min_index, mid_index - 1, numbers, value)
14        else:
15            return -1
16
17    res = my_while(min_index, max_index, numbers, value)
18    return res
19
20
21numbers = [10, 15, 20, 27, 41, 69]
22print(binary_search(numbers, 69))
23
24numbers = [13, 18, 54, 61, 78, 93]
25print(binary_search(numbers, 10))

随后将这个递归函数压缩至一行:

 1def binary_search(numbers, value):
 2    max_index = len(numbers) - 1
 3    min_index = 0
 4
 5    my_while = lambda min_index, max_index, numbers, value: (
 6        -1
 7        if min_index > max_index
 8        else (
 9            (min_index + max_index) // 2
10            if numbers[(min_index + max_index) // 2] == value
11            else (
12                my_while((min_index + max_index) // 2 + 1, max_index, numbers, value)
13                if numbers[(min_index + max_index) // 2] < value
14                else my_while(
15                    min_index, (min_index + max_index) // 2 - 1, numbers, value
16                )
17            )
18        )
19    )
20
21    res = my_while(min_index, max_index, numbers, value)
22    return res
23
24
25numbers = [10, 15, 20, 27, 41, 69]
26print(binary_search(numbers, 69))
27
28numbers = [13, 18, 54, 61, 78, 93]
29print(binary_search(numbers, 10))

随后再将整个 binary_search() 函数体压缩为一行:

 1def binary_search(numbers, value):
 2
 3    return (
 4        my_while := lambda min_index, max_index, numbers, value: (
 5            -1
 6            if min_index > max_index
 7            else (
 8                (min_index + max_index) // 2
 9                if numbers[(min_index + max_index) // 2] == value
10                else (
11                    my_while(
12                        (min_index + max_index) // 2 + 1, max_index, numbers, value
13                    )
14                    if numbers[(min_index + max_index) // 2] < value
15                    else my_while(
16                        min_index, (min_index + max_index) // 2 - 1, numbers, value
17                    )
18                )
19            )
20        )
21    )(0, len(numbers) - 1, numbers, value)
22
23
24numbers = [10, 15, 20, 27, 41, 69]
25print(binary_search(numbers, 69))
26
27numbers = [13, 18, 54, 61, 78, 93]
28print(binary_search(numbers, 10))

最后一步,将函数内外全部压缩为一行:

 1print(
 2    *list(
 3        (
 4            lambda numbers, value: (
 5                (
 6                    my_while := lambda min_index, max_index, numbers, value: (
 7                        -1
 8                        if min_index > max_index
 9                        else (
10                            (min_index + max_index) // 2
11                            if numbers[(min_index + max_index) // 2] == value
12                            else (
13                                my_while(
14                                    (min_index + max_index) // 2 + 1,
15                                    max_index,
16                                    numbers,
17                                    value,
18                                )
19                                if numbers[(min_index + max_index) // 2] < value
20                                else my_while(
21                                    min_index,
22                                    (min_index + max_index) // 2 - 1,
23                                    numbers,
24                                    value,
25                                )
26                            )
27                        )
28                    )
29                )(0, len(numbers) - 1, numbers, value)
30            )
31        )(numbers, value)
32        for numbers, value in [
33            ([10, 15, 20, 27, 41, 69], 69),
34            ([13, 18, 54, 61, 78, 93], 10),
35        ]
36    ),
37    sep="\n",
38)
39
40# output:
41# 5
42# -1

我们不妨再尝试将等号去掉:

 1print(
 2    *list(
 3        (
 4            lambda numbers, value: (
 5                globals().update(
 6                    {
 7                        "binary_search": lambda numbers, value: (
 8                            (
 9                                binary_search.__dict__.update(
10                                    {
11                                        "my_while": lambda min_index, max_index, numbers, value: (
12                                            -1
13                                            if min_index > max_index
14                                            else (
15                                                (min_index + max_index) // 2
16                                                if numbers[
17                                                    (min_index + max_index) // 2
18                                                ].__eq__(value)
19                                                else (
20                                                    binary_search.my_while(
21                                                        (min_index + max_index) // 2
22                                                        + 1,
23                                                        max_index,
24                                                        numbers,
25                                                        value,
26                                                    )
27                                                    if numbers[
28                                                        (min_index + max_index) // 2
29                                                    ]
30                                                    < value
31                                                    else binary_search.my_while(
32                                                        min_index,
33                                                        (min_index + max_index) // 2
34                                                        - 1,
35                                                        numbers,
36                                                        value,
37                                                    )
38                                                )
39                                            )
40                                        )
41                                    }
42                                )
43                            ),
44                            binary_search.my_while(0, len(numbers) - 1, numbers, value),
45                        )[-1]
46                    }
47                ),
48                binary_search(numbers, value),
49            )[-1]
50        )(numbers, value)
51        for numbers, value in [
52            ([10, 15, 20, 27, 41, 69], 69),
53            ([13, 18, 54, 61, 78, 93], 10),
54        ]
55    ),
56    **{"sep": "\n"}
57)
58
59# output:
60# 5
61# -1
  • 之所以在 binary_search() 函数内不使用 locals().update(),而是使用 binary_search.__dict__.update() 创建 my_while 函数,是因为 locals() 具有不可修改性,即在任何情况下, locals().update() 都是无效的。
  • my_while 函数中,使用 binary_search.my_while() 来调用函数是因为在使用 binary_search.__dict__.update() 创建 my_while 函数时,my_while 被定义为了 binary_search 这个函数对象的一个属性/方法。

相关系列文章