>>> from env_helper import info; info()
页面更新时间: 2024-04-04 19:42:43
运行环境:
Linux发行版本: Debian GNU/Linux 12 (bookworm)
操作系统内核: Linux-6.1.0-18-amd64-x86_64-with-glibc2.36
Python版本: 3.11.2
2.3. Prefer Catch-All Unpacking Over Slicing¶
One limitation of basic unpacking (see Item 6: “Prefer Multiple Assignment Unpacking Over Indexing”) is that you must know the length of the sequences you’re unpacking in advance. For example, here I have a list of the ages of cars that are being traded in at a dealership. When I try to take the first two items of the list with basic unpacking, an exception is raised at runtime:
>>> car_ages = [0, 9, 4, 8, 7, 20, 19, 1, 6, 15]
>>> car_ages_descending = sorted(car_ages, reverse=True)
oldest, second_oldest = car_ages_descending
>>>
Traceback ...
ValueError: too many values to unpack (expected 2)
Newcomers to Python often rely on indexing and slicing (see Item 11: “Know How to Slice Sequences”) for this situation. For example, here I extract the oldest, second oldest, and other car ages from a list of at least two items:
>>> oldest = car_ages_descending[0]
>>> second_oldest = car_ages_descending[1]
>>> others = car_ages_descending[2:]
>>> print(oldest, second_oldest, others)
20 19 [15, 9, 8, 7, 6, 4, 1, 0]
This works, but all of the indexing and slicing is visually noisy. In practice, it’s also error prone to divide the members of a sequence into various subsets this way because you’re much more likely to make off-by-one errors; for example, you might change boundaries on one line and forget to update the others.
To better handle this situation, Python also supports catch-all unpacking through a starred expression. This syntax allows one part of the unpacking assignment to receive all values that didn’t match any other part of the unpacking pattern. Here, I use a starred expression to achieve the same result as above without indexing or slicing:
>>> oldest, second_oldest, *others = car_ages_descending
>>> print(oldest, second_oldest, others)
20 19 [15, 9, 8, 7, 6, 4, 1, 0]
This code is shorter, easier to read, and no longer has the error-prone brittleness of boundary indexes that must be kept in sync between lines.
A starred expression may appear in any position, so you can get the benefits of catch-all unpacking anytime you need to extract one slice:
>>> oldest, *others, youngest = car_ages_descending
>>> print(oldest, youngest, others)
>>>
>>> *others, second_youngest, youngest = car_ages_descending
>>> print(youngest, second_youngest, others)
20 0 [19, 15, 9, 8, 7, 6, 4, 1]
0 1 [20, 19, 15, 9, 8, 7, 6, 4]
However, to unpack assignments that contain a starred expression, you must have at least one required part, or else you’ll get a SyntaxError. You can’t use a catch-all expression on its own:
*others = car_ages_descending
>>>
Traceback ...
SyntaxError: starred assignment target must be in a list or
➥tuple
You also can’t use multiple catch-all expressions in a single-level unpacking pattern:
first, *middle, *second_middle, last = [1, 2, 3, 4]
>>>
Traceback ...
SyntaxError: two starred expressions in assignment
But it is possible to use multiple starred expressions in an unpacking assignment statement, as long as they’re catch-alls for different parts of the multilevel structure being unpacked. I don’t recommend doing the following (see Item 19: “Never Unpack More Than Three Variables When Functions Return Multiple Values” for related guidance), but understanding it should help you develop an intuition for how starred expressions can be used in unpacking assignments:
>>> car_inventory = {
>>> 'Downtown': ('Silver Shadow', 'Pinto', 'DMC'),
>>> 'Airport': ('Skyline', 'Viper', 'Gremlin', 'Nova'),
>>>
>>> }
>>>
>>> ((loc1, (best1, *rest1)),
>>> (loc2, (best2, *rest2))) = car_inventory.items()
>>> print(f'Best at {loc1} is {best1}, {len(rest1)} others')
>>> print(f'Best at {loc2} is {best2}, {len(rest2)} others')
Best at Downtown is Silver Shadow, 2 others
Best at Airport is Skyline, 3 others
Starred expressions become list instances in all cases. If there are no leftover items from the sequence being unpacked, the catch-all part will be an empty list. This is especially useful when you’re processing a sequence that you know in advance has at least N elements:
>>> short_list = [1, 2]
>>> first, second, *rest = short_list
>>> print(first, second, rest)
1 2 []
You can also unpack arbitrary iterators with the unpacking syntax. This isn’t worth much with a basic multiple-assignment statement. For example, here I unpack the values from iterating over a range of length 2. This doesn’t seem useful because it would be easier to just assign to a static list that matches the unpacking pattern (e.g., [1, 2]):
>>> it = iter(range(1, 3))
>>> first, second = it
>>> print(f'{first} and {second}')
1 and 2
But with the addition of starred expressions, the value of unpacking iterators becomes clear. For example, here I have a generator that yields the rows of a CSV file containing all car orders from the dealership this week:
>>> def generate_csv():
>>> yield ('Date', 'Make', 'Model', 'Year', 'Price')
>>> ...
Processing the results of this generator using indexes and slices is fine, but it requires multiple lines and is visually noisy:
>>> all_csv_rows = list(generate_csv())
>>> header = all_csv_rows[0]
>>> rows = all_csv_rows[1:]
>>> print('CSV Header:', header)
>>> print('Row count: ', len(rows))
CSV Header: ('Date', 'Make', 'Model', 'Year', 'Price')
Row count: 0
Unpacking with a starred expression makes it easy to process the first row—the header—separately from the rest of the iterator’s contents. This is much clearer:
>>> it = generate_csv()
>>> header, *rows = it
>>> print('CSV Header:', header)
>>> print('Row count: ', len(rows))
CSV Header: ('Date', 'Make', 'Model', 'Year', 'Price')
Row count: 0
Keep in mind, however, that because a starred expression is always turned into a list, unpacking an iterator also risks the potential of using up all of the memory on your computer and causing your program to crash. So you should only use catch-all unpacking on iterators when you have good reason to believe that the result data will all fit in memory (see Item 31: “Be Defensive When Iterating Over Arguments” for another approach).
2.3.1. Things to Remember¶
✦ Unpacking assignments may use a starred expression to catch all values that weren’t assigned to the other parts of the unpacking pattern into a list.
✦ Starred expressions may appear in any position, and they will always become a list containing the zero or more values they receive.
✦ When dividing a list into non-overlapping pieces, catch-all unpacking is much less error prone than slicing and indexing.