Skip to content

高效Python90条之第6条 把数据结构直接拆分到多个变量里,不要专门通过下标访问

Python内置的元组(tuple)类型可以创建不可变的序列,把许多元素依次保存起来。 可以用整数作下标,通过下标来访问元组里面对应的元素。但不能通过下标给元素赋新值。

snack_calories = {"chips": 140, "popcorn": 80, "nuts": 190}

items = tuple(snack_calories.items())

print(type(snack_calories))  # <class 'dict'>
print(type(items))  # <class 'tuple'>

print(snack_calories)  # {'chips': 140, 'popcorn': 80, 'nuts': 190}
print(items)  # (('chips', 140), ('popcorn', 80), ('nuts', 190))
print(items[0])  # ('chips', 140)
print(items[1])  # ('popcorn', 80)
print(items[2])  # ('nuts', 190)

items[1] = ("apple", 200)
# TypeError: 'tuple' object does not support item assignment

Python还有一种写法,叫作拆分(unpacking)。这种写法让我们只用一条语句,就可以把元组里面的元素分别赋给多个变量,不用再通过下标去访问。 元组的元素本身不能修改,但是那些被赋值的变量是可以修改的。 通过unpacking来赋值要比通过下标去访问元组内的元素更清晰,而且这种写法所需的代码量通常比较少。当然,赋值操作的左边除了可以罗列单个变量,也可以写成列表、序列或任意深度的可迭代对象(iterable)。

favorite_snacks = {
    "salty": ("pretzels", 100),
    "sweet": ("cookies", 280),
    "veggie": ("carrots", 20),
}

(
    (type1, (name1, cals1)),
    (type2, (name2, cals2)),
    (type3, (name3, cals3)),
) = favorite_snacks.items()

print(f"Favorite {type1} is {name1} with {cals1} calories")
# Favorite salty is pretzels with 100 calories

print(f"Favorite {type2} is {name2} with {cals2} calories")
# Favorite sweet is cookies with 280 calories

print(f"Favorite {type3} is {name3} with {cals3} calories")
# Favorite veggie is carrots with 20 calories

可以通过unpacking原地交换两个变量,而不用专门创建临时变量。

# 传统方法
def bubble_sort(a):
    for _ in range(len(a)):
        for i in range(1, len(a)):
            if a[i] < a[i - 1]:
                temp = a[i]
                a[i] = a[i - 1]
                a[i - 1] = temp


names = ["pretzels", "carrots", "arugula", "bacon"]
bubble_sort(names)
print(names)
# ['arugula', 'bacon', 'carrots', 'pretzels']


# Unpacking方法
def bubble_sort(a):
    for _ in range(len(a)):
        for i in range(1, len(a)):
            if a[i] < a[i - 1]:
                a[i], a[i - 1] = a[i - 1], a[i]


names = ["pretzels", "carrots", "arugula", "bacon"]
bubble_sort(names)
print(names)
# ['arugula', 'bacon', 'carrots', 'pretzels']

原理分析:

Python处理赋值操作的时候,要先对=号右侧求值,于是,它会新建一个临时的元组,把a[i]a[i-1]这两个元素放到这个元组里面。例如,第一次进入内部的for循环时,这两个元素分别是'carrots''pretzels',于是,系统就会创建出('carrots','pretzels')这样一个临时的元组。

然后,Python会对这个临时的元组做unpacking,把它里面的两个元素分别放到=号左侧的那两个地方,于是,'carrots'就会把a[i-1]里面原有的'pretzels'换掉,'pretzels'也会把a[i]里面原有的'carrots'换掉。

现在,出现在a[0]这个位置上面的字符串就是'carrots'了,出现在a[1]这个位置上面的字符串则是'pretzels'

做完unpacking后,系统会扔掉这个临时的元组。

unpacking机制还有一个特别重要的用法,就是可以在for循环或者类似的结构里面,把复杂的数据拆分到相关的变量之中。

snacks = [("bacon", 350), ("donut", 240), ("muffin", 190)]

for i in range(len(snacks)):
    item = snacks[i]
    name = item[0]
    calories = item[1]
    print(f"#{i+1}: {name} has {calories} calories")

#1: bacon has 350 calories
#2: donut has 240 calories
#3: muffin has 190 calories

上面这段代码虽然没错,但看起来很乱,因为snacks结构本身并不是一份简单的列表,它的每个元素都是一个元组, 所以必须逐层访问才能查到最为具体的数据,也就是每种零食的名称(name)及卡路里(calories)。

下面换一种写法,首先调用内置的enumerate函数(参见第7条)获得当前要迭代的元组, 然后针对这个元组做unpacking,这样就可以直接得到具体的namecalories值了。

这才是符合Python风格的写法(Pythonic式的写法)。

snacks = [("bacon", 350), ("donut", 240), ("muffin", 190)]

for rank, (name, calories) in enumerate(snacks, 1):
    print(f'#{rank}: {name} has {calories} calories')

#1: bacon has 350 calories
#2: donut has 240 calories
#3: muffin has 190 calories

Python的unpacking机制可以用在许多方面,例如构建列表(Rule13)、给函数设计参数列表(Rule22)、传递关键字参数(Rule23)、接收多个返回值(Rule19条)等。

要点:

  • unpacking是一种特殊的Python语法,只需要一行代码,就能把数据结构里面的多个值分别赋给相应的变量。
  • unpacking在Python中应用广泛,凡是可迭代的对象都能拆分,无论它里面还有多少层迭代结构。
  • 尽量通过unpacking来拆解序列之中的数据,而不要通过下标访问,这样可以让代码更简洁、更清晰。

拓展:enumerate

enumerate() 函数用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,一般用在 for 循环当中。

enumerate() 方法:

语法:

  • enumerate(sequence, [start=0])

参数:

  • sequence:一个序列、迭代器或其他支持迭代对象。
  • start:下标起始位置。 返回值

返回:

  • enumerate(枚举) 对象。

基本用法:

  • 字符串
sample = "abcd"

for i, j in enumerate(sample):  # 输出的是元组内的元素
    print(i, j)

# 0 a
# 1 b
# 2 c
# 3 d

for i in enumerate(sample):  # 输出的是元组
    print(i)

# (0, 'a')
# (1, 'b')
# (2, 'c')
# (3, 'd')
  • 元组
sample = ("abcd", "hijk")

for i, j in enumerate(sample):
    print(i, j)
# 0 abcd
# 1 hijk

sample = ("abcd", "hijk")

for i, j in enumerate(sample, 2):
    print(i, j)
# 2 abcd
# 3 hijk
  • 数组
sample = ["abcd", "hijk"]

for i, j in enumerate(sample):
    print(i, j)
# 0 abcd
# 1 hijk
  • 字典
sample = {"abcd": 1, "hijk": 2}

for i, j in enumerate(sample):
    print(i, j)
# 0 abcd
# 1 hijk