(译)用多重赋值和元组解包提高python代码的可读性

988 阅读6分钟

原文链接(侵删): http://treyhunner.com/2018/03/tuple-unpacking-improves-python-code-readability/

无论我是教python新手还是老手,经常发现他们没有很好的使用多重赋值。

多重赋值允许你在一行代码中同时分配多个值。也许你在了解它之后会觉得也不过如此嘛,但你要记住,多重赋值有可能会很棘手。

这篇文章就来详细的介绍下多重赋值。

多重赋值如何工作

在本文中,当我说到多重赋值,元组解包,迭代解包的时候,指的都是同一件事情。

python中的多重赋值看起来像这样:

x, y = 10, 20

这里我们把x设为10,y设为20。

上面的代码实际上是创建了一个包含10,20的元组,然后循环遍历该元组,并从循环中获取两个items中的每一个,然后分别分配给x和y。

像这样:

(x, y) = (10, 20)

在元组中使用括号是可选的,多重赋值也一样,下面这几行代码都是等价的:

x, y = 10, 20
x, y = (10, 20)
(x, y) = 10, 20
(x, y) = (10, 20)

多元赋值通常被称为“元组拆包”,是因为它经常与元组一起使用。但是我们也可以对任何迭代使用多个赋值,而不仅仅是元组。比如使用列表:

>>> x, y = [10, 20]
>>> x
10
>>> y
20

使用string:

>>> x, y = 'hi'
>>> x
'h'
>>> y
'i'

任何可以循环的东西都可以通过元组解包/多重赋值来“解压缩”。

接下来说说如何使用多重赋值。

在循环中解压缩

你经常会在for循环中看到解压缩。

这里有一个dict:

>>> person_dictionary = {'name': "Trey", 'company': "Truthful Technology LLC"}

通常我们不会对dict这样使用for循环:

for item in person_dictionary.items():
    print(f"Key {item[0]} has value {item[1]}")

而是像下面这样,使用多重赋值:

for key, value in person_dictionary.items():
    print(f"Key {key} has value {value}")

实际上,等同于把item赋值给key,value一样:

for item in person_dictionary.items():
    key, value = item
    print(f"Key {key} has value {value}")

所以多重赋值非常适合把一个字典拆分成键值对的形式,这一用法在其他地方也很常见。

比如使用enumerate的时候:

for i, line in enumerate(my_file):
    print(f"Line {i}: {line}")

同样的,在使用zip的时候:

for color, ratio in zip(colors, ratios):
    print(f"It's {ratio*100}% {color}.")

多任务虽然适用于任何分配,但不仅仅是循环分配。

代替硬编码索引

我们经常会在代码中看到硬编码索引(像point[0], items[1]这样):

print(f"The first item is {items[0]} and the last item is {items[-1]}")

我们可以用多重赋值来代替硬编码索引,这样能让代码的可读性更高。

比如,下面是一段有三个硬编码索引的代码:

def reformat_date(mdy_date_string):
    """Reformat MM/DD/YYYY string into YYYY-MM-DD string."""
    date = mdy_date_string.split('/')
    return f"{date[2]}-{date[0]}-{date[1]}"

通过使用多重赋值让这段代码可读性更高一些:

def reformat_date(mdy_date_string):
    """Reformat MM/DD/YYYY string into YYYY-MM-DD string."""
    month, day, year = mdy_date_string.split('/')
    return f"{year}-{month}-{day}"

所以当你在代码中见到硬编码索引时,停下来想想是否可以用多重赋值使得代码更可读。

多重赋值是很严格的

在解包我们给它的迭代时,多重赋值实际上相当严格。

如果试图将更大的迭代器拆分成更少的变量,会得到一个错误:

>>> x, y = (10, 20, 30)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: too many values to unpack (expected 2)

同样的,如果试图将一个较小的迭代器拆分成更多的变量,也会得到一个错误:

>>> x, y, z = (10, 20)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: not enough values to unpack (expected 3, got 2)

这种严格的规定相当好,如果当前正在处理的item与我们预期的不同,可以及时的发现并修改。

切片的代替品

多重赋值也可以用来代替切片中的硬编码。

我们都知道,切片可以方便的获取列表或者其它序列中的部分元素。

像下面的代码就是硬编码的方式;

all_after_first = items[1:]
all_but_last_two = items[:-2]
items_with_ends_removed = items[1:-1]

任何时候当你看到切片索引中使用硬编码的方式,你都可以使用多重赋值来代替。要做到这一点,我们需要用到* 操作符。

在python3中,* 操作符被加入到多重赋值的语法中,它允许我们在解包列表时使用* 代替剩余的部分,比如:

>>> numbers = [1, 2, 3, 4, 5, 6]
>>> first, *rest = numbers
>>> rest
[2, 3, 4, 5, 6]
>>> first
1

* 操作符允许我们替换列表中的硬编码,下面的两行代码是等价的:

>>> beginning, last = numbers[:-1], numbers[-1]
>>> *beginning, last = numbers

同样的:

>>> head, middle, tail = numbers[0], numbers[1:-1], numbers[-1]
>>> head, *middle, tail = numbers

深入拆包

深入拆包是python程序员经常忽视的一个特性,虽然使用的不是很频繁,但是了解了之后将很有用。

对于下面的代码来说,并不算是深入拆包:

>>> color, point = ("red", (1, 2, 3))
>>> color
'red'
>>> point
(1, 2, 3)

而下面的代码就可以这样说了,因为它将point元组进一步分为x,y,z三个变量:

>>> color, (x, y, z) = ("red", (1, 2, 3))
>>> color
'red'
>>> x
1
>>> y
2

如果对上面的代码你感到困惑,它其实跟下面的代码是一样的,在两边加上括号即可:

>>> (color, (x, y, z)) = ("red", (1, 2, 3))

首先通过第一层拆包得到两个对象,然后再将第二个对象拆包分配给x, y, z三个变量。

考虑下面两个列表:

start_points = [(1, 2), (3, 4), (5, 6)]
end_points = [(-1, -2), (-3, 4), (-6, -5)]

下面的代码使用浅层拆包对列表进行操作:

for start, end in zip(start_points, end_points):
    if start[0] == -end[0] and start[1] == -end[1]:
        print(f"Point {start[0]},{start[1]} was negated.")

我们用深层拆包可以做同样的事:

for (x1, y1), (x2, y2) in zip(start_points, end_points):
    if x1 == -x2 and y1 == -y2:
        print(f"Point {x1},{y1} was negated.")

使用深层拆包能够更清晰的看到正在处理的对象,另外,深层拆包的过程同样严格。

多重赋值可以提高代码的可读性和代码的正确性。它可以使您的代码更具描述性,同时还可以对您要解包的迭代对象的大小和形状进行隐式断言。

多重赋值一个最常见的用途是替换硬编码索引和硬编码的列表。