轨道
/
Python
Python
/
练习
/
火车司机
火车司机

火车司机

学习练习

简介

解包是指通过迭代,从一个集合(例如 list、tuple 或 dict)中提取元素的操作。 解包出来的值随后可以在同一条语句中赋给变量,这通常称为多重赋值。

特殊运算符 * 和 ** 常用于解包场景,也常用于多重赋值。

Caution

不要把 *<variable_name> 和 **<variable_name> 与 * 和 ** 混淆。* 和 ** 分别用于乘法和幂运算,而 *<variable_name> 和 **<variable_name> 则用作打包和解包运算符。

多重赋值

在多重赋值中,赋值运算符(=)左侧的变量数量必须与右侧的值的数量一致。 要用逗号 , 分隔这些值:

>>> a, b = 1, 2
>>> a
1

如果多重赋值中变量的数量与给定的值不符,就会抛出 ValueError:

>>> x, y, z = 1, 2

ValueError: too many values to unpack (expected 3, got 2)

多重赋值不限于一种数据类型:

>>> x, y, z = 1, "Hello", True
>>> x
1

>>> y
'Hello'

>>> z
True

多重赋值可以用来交换 lists 中的元素。 这种做法在排序算法中相当常见。 例如:

>>> numbers = [1, 2]
>>> numbers[0], numbers[1] = numbers[1], numbers[0]
>>> numbers
[2, 1]

由于 tuples 是不可变的,所以无法交换 tuple 中的元素。

解包

Note

下面的示例使用 lists,但同样的概念也适用于 tuples。

在 Python 中,可以把 list/tuple/dictionary 的元素解包到不同的变量中。 由于值在 lists/tuples 中按特定顺序排列,它们会按相同的顺序解包到变量中:

>>> fruits = ["apple", "banana", "cherry"]
>>> x, y, z = fruits
>>> x
"apple"

如果有不需要的值,可以用 _ 标记它们:

>>> fruits = ["apple", "banana", "cherry"]
>>> _, _, z = fruits
>>> z
"cherry"

深层解包

从一个嵌套在 list 或 tuple 内部的 list/tuple(也就是嵌套的 list/tuple)中解包并赋值,与浅层解包的方式相同,但通常需要限定符来明确上下文或位置:

>>> fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]]
>>> [[a, b], [c, d]] = fruits_vegetables
>>> a
"apple"

>>> d
"potato"

你也可以只对嵌套 list/tuple 的一部分进行深层解包:

>>> fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]]
>>> [a, [c, d]] = fruits_vegetables
>>> a
["apple", "banana"]

>>> c
"carrot"

如果解包时变量的位置不正确,或者值的数量不对,就会得到 ValueError:

>>> fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]]
>>> [[a, b], [d]] = fruits_vegetables

ValueError: too many values to unpack (expected 1)

用 * 解包 list/tuple

解包 list/tuple时,可以用 * 运算符捕获“剩余”的值。 这比切片 list/tuple(在某些情况下可读性较差)更清晰。 例如,可以取出第一个元素,然后把剩下的值放进一个不含第一个元素的新 list 中:

>>> fruits = ["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"]
>>> x, *last = fruits
>>> x
"apple"

>>> last
["banana", "cherry", "orange", "kiwi", "melon", "mango"]

我们也可以取出 list 开头和结尾的值,同时把中间的所有值归为一组:

>>> fruits = ["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"]
>>> x, *middle, y, z = fruits
>>> y
"melon"

>>> middle
["banana", "cherry", "orange", "kiwi"]

我们还可以在深层解包中使用 *:

>>> fruits_vegetables = [["apple", "banana", "melon"], ["carrot", "potato", "tomato"]]
>>> [[a, *rest], b] = fruits_vegetables
>>> a
"apple"

>>> rest
["banana", "melon"]

解包字典

解包字典与解包 list/tuple 稍有不同。 对字典进行迭代时,默认取的是键。 因此解包 dict 时,只能解包键,而不能解包值:

>>> fruits_inventory = {"apple": 6, "banana": 2, "cherry": 3}
>>> x, y, z = fruits_inventory
>>> x
"apple"

如果想解包值,可以使用 <dict>.values() 方法:

>>> fruits_inventory = {"apple": 6, "banana": 2, "cherry": 3}
>>> x, y, z = fruits_inventory.values()
>>> x
6

如果键和值都需要,就使用 <dict>.items() 方法。 <dict>.items() 会生成一个包含键值对的可迭代视图。 这些键值对可以解包到 tuple 中:

>>> fruits_inventory = {"apple": 6, "banana": 2, "cherry": 3}
>>> x, y, z = fruits_inventory.items()
>>> x
("apple", 6)

打包

打包是指把多个值归组到一个赋给变量的 list 中的能力。 当你想要_解包_值、做些修改,再把结果_打包_回一个变量时,这个特性很有用。 它还使得对 2 个或更多 lists/tuples/dicts 执行合并成为可能。

用 * 打包 list/tuple

可以用 * 运算符打包 list/tuple。 这会把所有值打包进一个 list/tuple。

>>> fruits = ("apple", "banana", "cherry")
>>> more_fruits = ["orange", "kiwi", "melon", "mango"]

# fruits and more_fruits are unpacked and then their elements are packed into combined_fruits
>>> combined_fruits = *fruits, *more_fruits

# If there is no * on to the left of the "=" the result is a tuple
>>> combined_fruits
("apple", "banana", "cherry", "orange", "kiwi", "melon", "mango")

# If the * operator is used on the left side of "=" the result is a list.
# Note the trailing comma.
>>> *combined_fruits_too, = *fruits, *more_fruits
>>> combined_fruits_too
['apple', 'banana', 'cherry', 'orange', 'kiwi', 'melon', 'mango']

关于在左侧使用 * 的更多背景,参见 PEP 3132。

用 ** 打包字典

打包字典通过使用 ** 运算符来完成。 这会把一个字典中的所有键-值对打包进另一个字典,或者把两个字典合并在一起。

>>> fruits_inventory = {"apple": 6, "banana": 2, "cherry": 3}
>>> more_fruits_inventory = {"orange": 4, "kiwi": 1, "melon": 2, "mango": 3}

# fruits_inventory and more_fruits_inventory are unpacked into key-values pairs and combined.
>>> combined_fruits_inventory = {**fruits_inventory, **more_fruits_inventory}

# then the pairs are packed into combined_fruits_inventory
>>> combined_fruits_inventory
{"apple": 6, "banana": 2, "cherry": 3, "orange": 4, "kiwi": 1, "melon": 2, "mango": 3}

函数中 * 和 ** 的用法

用函数形参打包

当你创建一个接受任意数量实参的函数时,可以在函数定义中使用 *args 或 **kwargs。 *args 用于把任意数量的位置实参(非关键字实参)打包成一个 tuple,而 **kwargs 用于把任意数量的关键字实参打包成一个字典。

*args 的用法:

# This function is defined to take any number of positional arguments

>>> def my_function(*args):
...     print(args)

# Arguments given to the function are packed into a tuple

>>> my_function(1, 2, 3)
(1, 2, 3)

>>> my_function("Hello")
("Hello")

>>> my_function(1, 2, 3, "Hello", "Mars")
(1, 2, 3, "Hello", "Mars")

**kwargs 的用法:

# This function is defined to take any number of keyword arguments

>>> def my_function(**kwargs):
...   print(kwargs)

# Arguments given to the function are packed into a dictionary

>>> my_function(a=1, b=2, c=3)
{"a": 1, "b": 2, "c": 3}

*args 和 **kwargs 也可以互相结合使用:

>>> def my_function(*args, **kwargs):
...   print(sum(args))
...   for key, value in kwargs.items():
...       print(str(key) + " = " + str(value))

>>> my_function(1, 2, 3, a=1, b=2, c=3)
6
a = 1
b = 2
c = 3

你也可以在 *args 之前写形参,以支持特定的位置实参。 这样一来,单独的关键字实参就必须出现在 **kwargs 之前。

Caution

实参的结构必须像这样:

def my_function(<positional_args>, *args, <key-word_args>, **kwargs)

如果不遵循这个顺序,就会出错。

>>> def my_function(a, b, *args):
...   print(a)
...   print(b)
...   print(args)

>>> my_function(1, 2, 3, 4, 5)
1
2
(3, 4, 5)

按错误的顺序写实参会导致错误:

>>>def my_function(*args, a, b):
... print(args)

>>>my_function(1, 2, 3, 4, 5)
Traceback (most recent call last):
  File "c:\something.py", line 3, in <module>
    my_function(1, 2, 3, 4, 5)
TypeError: my_function() missing 2 required keyword-only arguments: 'a' and 'b'

解包到函数调用中

你可以用 * 把 list/tuple 中的实参解包到函数调用中。 这对不接受 iterable 的函数非常有用:

>>> def my_function(a, b, c):
...   print(c)
...   print(b)
...   print(a)

numbers = [1, 2, 3]
>>> my_function(*numbers)
3
2
1

把 * 解包与内置函数 zip() 一起使用是另一个常见用例。 zip() 函数接收多个可迭代对象,并返回一个由 tuples 组成的 list,其中把每个 iterable 中的值归为一组:

>>> values = (['x', 'y', 'z'], [1, 2, 3], [True, False, True])
>>> a, *rest = zip(*values)
>>> rest
[('y', 2, False), ('z', 3, True)]

说明

你的朋友 Linus 是一名火车司机,在城市之间驾驶货运列车。虽然他开火车很在行,但对物流和电脑却一窍不通。他想请你用编程帮忙整理列车信息,并修正路线数据里的错误。

Note

这道练习用切片、索引和各种 dict 方法就能轻松解决。不过,我们希望你在完成下面每个任务时练习打包、解包和多重赋值。

1. 创建包含所有车厢的数组

你的朋友一直在记录每节车厢的标识符(ID),但系统在任意时刻要处理多少节车厢,他从来都说不准。如果能把这些数据打包成一个统一的list,物流程序的其余部分就会轻松得多。

请实现一个函数 get_list_of_wagons(),它接受任意数量的车厢 ID。每个 ID 都是正整数。函数应当return一个list,其中包含给定的这些 ID。

>>> get_list_of_wagons(1, 7, 12, 3, 14, 8, 5)
[1, 7, 12, 3, 14, 8, 5]

2. 修正车厢数组

到这里,你对这些数据以及它在物流程序中的用法已经开始有感觉了。ID 系统总是把1这个 ID 分配给机车,列车中其余车厢则分配一个随机选取的、大于1的 ID。

你的朋友往列车上接了两节新车厢,却忘了更新系统!现在,必须把列车list中的前两节车厢移到末尾,否则整个顺序都会乱掉。

更麻烦的是,你的朋友刚又发现了一个list,里面似乎装着缺失的车厢 ID。他只记得,一旦新车厢移好位置,这个list里的 ID 就应该紧跟在指定机车的后面。

如果你能帮他改正错误、把数据合并起来,Linus 会非常感激。

请实现一个函数 fix_list_of_wagons(),它接收两个包含车厢 ID 的lists。函数应把第一个list的前两项移到末尾,并把第二个list中的值插入到机车 ID(1)之后(也就是它的右侧)。然后函数应当return一个修改后的list。

>>> fix_list_of_wagons([2, 5, 1, 7, 4, 12, 6, 3, 13], [3, 17, 6, 15])
[1, 3, 17, 6, 15, 7, 4, 12, 6, 3, 13, 2, 5]

3. 添加缺失的停靠站

现在车厢数据都正确了,Linus 希望你更新系统的路线信息。在一条运输路线上,列车可能会在几个不同的车站停靠,装卸货物。每趟行程的这些中间交货点数量可能各不相同。你的朋友希望你把这些缺失或额外的交货信息补充到系统的路线dict中。

请实现一个函数 add_missing_stops(),它先接收一个路线dict,后面再跟可变数量的关键字实参。这些实参可以是一个包含一个或多个停靠站的dict,也可以是任意数量的 stop_number=city 关键字对。函数随后应当返回更新后的路线dict,其中多了一个key,按顺序存放所有新增停靠站的list。

>>> add_missing_stops({"from": "New York", "to": "Miami"},
                      stop_1="Washington, DC", stop_2="Charlotte", stop_3="Atlanta",
                      stop_4="Jacksonville", stop_5="Orlando")

{"from": "New York", "to": "Miami", "stops": ["Washington, DC", "Charlotte", "Atlanta", "Jacksonville", "Orlando"]}

4. 扩展路线信息

Linus 一直在做路线程序,他发现有些路线缺少一些重要细节。初始路线信息已经构建成一个dict,你的朋友希望你把这个dict里可能缺的内容补上。系统中每条路线需要的细节都略有不同,所以 Linus 很想要一个通用的解决方案。

请实现一个名为 extend_route_information() 的函数,它接收两个dicts。第一个dict包含列车路线往来的起点和终点城市。

第二个dict包含其他路线细节,比如列车速度、长度或温度。函数应当返回一个合并了所有路线信息的dict。

Note

第二个dict可以包含与示例中不同的、或者更多的属性。

>>> extend_route_information({"from": "Berlin", "to": "Hamburg"}, {"length": "100", "speed": "50"})
{"from": "Berlin", "to": "Hamburg", "length": "100", "speed": "50"}

5. 整理车厢存放场

Linus 在查看车厢存放场时发现,车厢没有按正确的顺序存放。除了 ID 之外,每节车厢还有一种颜色,对应它所运载的货物类型。车厢在存放场里按网格存放,网格中每一列的车厢颜色都相同。

可是,物流系统显示,要存进存放场的车厢lists里,每一_行_都按颜色归在了一起。但要让存放网格正常工作,每一_行_都应该有三种不同的颜色,这样各_列_才能按颜色对齐。你的朋友希望你把这些车厢存放场lists理顺,好让车厢正确存放。

请实现一个名为 fix_wagon_depot() 的函数,它接收一个包含三项的list。这个list的每一项都是一个子数组(也就是“行”),里面包含三个tuples。每个tuple是一个 (<wagon ID>, <wagon color>) 对。

函数应当返回一个list,其中的三个“行”lists已重新排序,把车厢交换到了正确的位置。

>>> fix_wagon_depot([
                    [(2, "red"), (4, "red"), (8, "red")],
                    [(5, "blue"), (9, "blue"), (13,"blue")],
                    [(3, "orange"), (7, "orange"), (11, "orange")],
                    ])

[
[(2, "red"), (5, "blue"), (3, "orange")],
[(4, "red"), (9, "blue"), (7, "orange")],
[(8, "red"), (13,"blue"), (11, "orange")]
]
通过 GitHub 编辑 链接将在新窗口或新标签页中打开
Python Exercism

准备好开始 火车司机 了吗?

注册 Exercism,借助 17 个概念146 个练习 和真人导师指导,学习并掌握 Python,全部免费。