# Глава 25. Распаковка и упаковка
Распаковка (unpacking, деструктуризация) — это разложение итерабельного объекта на отдельные значения. Упаковка (packing) — обратная операция по сбору переменных в коллекцию.
Эти приемы делают код простым и очень лаконичным.
## Распаковка
Допустим, отдельным переменным нужно присвоить значения из списка. Разработчики, не знакомые с синтаксисом распаковки, решают эту задачу примерно так:
```python
lst = ["a", "b", "c"]
a = lst[0]
b = lst[1]
c = lst[2]
```
С помощью распаковки то же самое делается куда проще:
```python {.example_for_playground}
lst = ["a", "b", "c"]
a, b, c = lst
print(a, b, c)
```
```
a b c
```
Главное — соблюдать правило: количество элементов в распаковываемом объекте **должно совпадать** с количеством переменных, в которые осуществляется распаковка. В нашем примере слева от оператора `=` находятся 3 переменные; справа — список из 3-х элементов.
Нарушение этого правила приведет к исключению `ValueError`:
```python {.example_for_playground}
lst = ["a", "b", "c", "d"]
a, b, c = lst
```
```
Traceback (most recent call last):
File "example.py", line 3, in <module>
a, b, c = lst
^^^^^^^
ValueError: too many values to unpack (expected 3)
```
Распакуйте символы строки `s` в переменные и выведите их в консоль. {.task_text}
```python {.task_source #python_chapter_0250_task_0010}
s = "abc"
```
При распаковке слева от оператора `=` должно быть три переменные, а справа — объект `s`. {.task_hint}
```python {.task_answer}
s = "abc"
c1, c2, c3 = s
print(c1, c2, c3)
```
## Множественное присваивание
Очень часто распаковку используют для обмена значениями переменных.
Наивный вариант обмена организуется через дополнительную переменную:
```python {.example_for_playground}
a = 0
b = 1
swap = a
a = b
b = swap
print(a, b)
```
```
1 0
```
А вот как выглядит обмен через распаковку:
```python {.example_for_playground}
a = 0
b = 1
a, b = b, a
print(a, b)
```
```
1 0
```
Этот подход называется **множественным присваиванием** (parallel assignment) или позиционным присваиванием. Значения присваиваются сразу нескольким переменным, по приоритету слева направо.
Что выведет этот код? {.task_text}
В случае исключения напишите `error`. {.task_text}
```python {.example_for_playground}
lst = [0, 0]
i = 0
i, lst[i] = 1, 2
print(lst)
```
```consoleoutput {.task_source #python_chapter_0250_task_0020}
```
Присваивания идут слева направо. Поэтому переменной `i` присвоится 1. Затем элементу списка по индексу 1 будет присвоено значение 2. {.task_hint}
```python {.task_answer}
[0, 2]
```
## Как устроена распаковка
В примере с обменом переменных через распаковку `a, b = b, a` CPython неявно завернул правую часть выражения в кортеж. **Запятая** между переменными в правой части равенства сигнализировала интерпретатору, что перед ним кортеж.
Убедимся в этом:
```python {.example_for_playground}
a = 0
b = 1
tpl = a, b = b, a
print(tpl)
print(type(tpl))
```
```
(1, 0)
<class 'tuple'>
```
Таким образом эти варианты абсолютно равнозначны:
```python
a, b = b, a
a, b = (b, a)
```
## Варианты распаковки
Распаковка применима к любому итерабельному объекту — множеству, генератору, словарю, кортежу...
При распаковке словаря перебираются его ключи:
```python {.example_for_playground}
d = {"a": 1, "b": 2}
k1, k2 = d
print(k1, k2)
```
```
a b
```
Чтобы распаковать пары ключ-значение из словаря, воспользуемся методом `items()`. Тогда в результирующие переменные сохранятся кортежи:
```python {.example_for_playground}
d = {"a": 1, "b": 2}
k1, k2 = d.items()
print(k1, k2)
```
```
('a', 1) ('b', 2)
```
Чтобы распаковать только значения без ключей, вместо `items()` используется метод `values()`.
Распакуйте в переменные значения, сгенерированные функцией `range()` от 100 до 106 включительно с шагом 3. Выведите переменные в консоль. {.task_text}
```python {.task_source #python_chapter_0250_task_0030}
```
Слева от оператора `=` должны быть перечислены 3 переменные. Справа должен быть указан `range()`. {.task_hint}
```python {.task_answer}
first, second, third = range(100, 107, 3)
print(first, second, third)
```
К распаковке часто прибегают при итерации в циклах.
Переберем список кортежей. Каждый кортеж распакуем в две переменные:
```python {.example_for_playground}
clicks = [
("main page", 14),
("news page", 3)
]
for page_name, count in clicks:
print(page_name, count)
```
```
main page 14
news page 3
```
Функция `enumerate()` на каждой итерации цикла генерирует кортеж из двух значений — индекс итерации и элемент итерируемого объекта:{#block-enumerate}
```python {.example_for_playground}
databases = ["mongo", "clickhouse", "postgres"]
for i, db in enumerate(databases):
print(i, db)
```
```
0 mongo
1 clickhouse
2 postgres
```
Напишите list comprehension, который создает последовательность квадратов целых чисел, лежащих на отрезке от 1 до 3. {.task_text}
Распакуйте из него значения в отдельные переменные и выведите их в консоль. {.task_text}
```python {.task_source #python_chapter_0250_task_0040}
```
Возведение в квадрат переменной `x`: `x**2`. {.task_hint}
```python {.task_answer}
a, b, c = [x**2 for x in range(1, 4)]
print(a, b, c)
```
## Упаковка
Итак, чтобы при распаковке итерируемого объекта в переменные не было ошибок, нужно, чтобы количество распаковываемых значений совпадало с количеством переменных, которым они присваиваются.
Как быть, если хочется сохранить в отдельные переменные не все значения, а только часть? Например, два элемента из середины кортежа. Или первый и последний символы строки.
Это делается просто:
- Если требуется пропустить один элемент, присваиваем его неиспользуемой переменной. По негласному правилу **неиспользуемые переменные** именуются символом подчеркивания (underscore): `_`.
- Если требуется пропустить сразу несколько элементов, то **упаковываем** их в одну неиспользуемую переменную-список.
Распакуем список из 3-х элементов, причем нам нужны только первый и третий:
```python {.example_for_playground}
a, _, b = ["a", "x", "b"]
print(a, _, b)
```
```
a x b
```
Как видите, `_` — это обычная переменная, такая же как `a` и `b`. Просто ее имя подсказывает разработчикам и линтерам, что дальше по коду не предполагается ее использовать.
Распакуем кортеж, из которого нам нужны второй и третий элементы:
```python {.example_for_playground}
_, second, third, _ = (1, 2, 3, 4)
print(second, third, _)
```
```
2 3 4
```
Переменной `_` было присвоено значение 1, затем 4. Его мы и видим в консольном выводе. Значит, в одну и ту же переменную можно распаковывать много элементов. Сохранится значение последнего из них.
Теперь посмотрим, как пропустить несколько элементов, запаковав их в список. Для этого нам нужен оператор `*`: он упаковывает объекты в коллекцию.
Распакуем список, чтобы достать из него последний элемент. Все предшествующие элементы **упакуем** в переменную `_`:
```python {.example_for_playground}
*_, last = [1, 2, 3, 4, 5]
print(_)
print(last)
print(type(_))
```
```
[1, 2, 3, 4]
5
<class 'list'>
```
Оператор `*` подсказал интерпретатору, что в переменную `_` нужно упаковать все элементы кроме последнего, который присвоился переменной `last`.
Распакуем первый и последний символы строки:
```python {.example_for_playground}
first, *_, last = "import this"
print(first, last)
```
```
i s
```
Посмотрите, как поведет себя упаковка, если для нее не осталось элементов. Распакуйте первые два элемента списка `lst` в переменные `a`, `b`, середину списка запакуйте в `_`, и последние два элемента распакуйте в `c`, `d`. {.task_text}
Выведите значение переменной `_` в консоль. {.task_text}
```python {.task_source #python_chapter_0250_task_0050}
lst = [1, 2, 3, 4]
```
Для запаковки в переменную `_` воспользуйтесь синтаксисом `*`: `*_`. {.task_hint}
```python {.task_answer}
lst = [1, 2, 3, 4]
a, b, *_, c, d = lst
print(_)
```
Вместо итерабельного объекта справа от `=` может идти простое перечисление объектов:
```python {.example_for_playground}
*vals, = 1, 2, 3
print(vals)
```
```
[1, 2, 3]
```
Обратите внимание, что после `*vals` **стоит запятая.** Она нужна, чтобы сформировать список. А запятая, разделяющая значения `1, 2, 3`, помогает интерпретатору неявно превратить правую часть выражения в кортеж.
Если в левой части убрать запятую, мы получим исключение:
```
File "example.py", line 5
*vals = 1, 2, 3
^^^^^
SyntaxError: starred assignment target must be in a list or tuple
```
Распаковка может быть вложенной! Что сохранится в переменную `tail`? {.task_text}
В случае исключения напишите `error`. {.task_text}
```python {.example_for_playground}
containers = ["tuple", "set", "list", "dict"]
_, (head, *tail), *_ = containers
```
```consoleoutput {.task_source #python_chapter_0250_task_0060}
```
В `_` распаковывается "tuple", в кортеж — "set", в `*_` все остальное: "list" и "dict". "s", то есть первая буква "set", распаковывается в `head`. Остальные буквы распаковываются в список `tail`. {.task_hint}
```python {.task_answer}
['e', 't']
```
Что сохранится в переменную `b`? {.task_text}
В случае исключения напишите `error`. {.task_text}
```python {.example_for_playground}
style = ["primary color", (3, 161, 252, 0.5)]
title, [r, g, b, a] = style
```
```consoleoutput {.task_source #python_chapter_0250_task_0070}
```
В `title` распакуется "primary color", в список — кортеж (3, 161, 252, 0.5). `b` — это третий элемент списка, и ему будет присвоен третий элемент кортежа. {.task_hint}
```python {.task_answer}
252
```
## Объединение итерабельных объектов
Допустим, требуется объединить 2 списка. Можем воспользоваться оператором `+`:
```python {.example_for_playground}
head = [1, 2]
tail = [3, 4, 5]
merged = head + tail
```
А можем применить синтаксис распаковки:
```python {.example_for_playground}
head = [1, 2]
tail = [3, 4, 5]
merged = [*head, *tail]
print(merged)
```
```
[1, 2, 3, 4, 5]
```
Мы вновь использовали оператор `*`: в зависимости от контекста он как упаковывает, так и распаковывает. В данном случае он распаковал списки `head` и `tail` в отдельные значения, которые сохранились в результирующий список `merged`.
Преимуществом объединения коллекций через распаковку является возможность добавлять к распаковываемым коллекциям отдельные объекты:
```python {.example_for_playground}
head = [1, 2]
tail = [3, 4, 5]
merged = [*head, 0, *tail, 0, 0]
print(merged)
```
```
[1, 2, 0, 3, 4, 5, 0, 0]
```
Объедините множества `s1`, `s2` и значения 10, 20. Результирующее множество выведите в консоль. {.task_text}
```python {.task_source #python_chapter_0250_task_0080}
s1 = {1, 3, 4}
s2 = {2, 3, 4, 5, 6}
merged = # Your code here
```
В объединенное множество нужно добавить `*s1`, `*s2`, значения 10 и 20. {.task_hint}
```python {.task_answer}
s1 = {1, 3, 4}
s2 = {2, 3, 4, 5, 6}
merged = {*s1, *s2, 10, 20}
print(merged)
```
Для распаковки и упаковки словарей предназначен оператор `**`. Работает он по такому же принципу, что и `*`, но применяется исключительно к коллекциям «ключ-значение».
Объедините словари `d1`, `d2`, `d3` в словарь `merged` и выведите его в консоль. Обратите внимание, по какому принципу перезаписываются значения для одинаковых ключей. {.task_text}
```python {.task_source #python_chapter_0250_task_0090}
d1 = {"a": 1, "b": 2, "c": 3}
d2 = {"c": 4, "d": 5}
d3 = {"d": 6, "e": 7}
```
Синтаксис: `{**d1, **d2, **d3}`. {.task_hint}
```python {.task_answer}
d1 = {"a": 1, "b": 2, "c": 3}
d2 = {"c": 4, "d": 5}
d3 = {"d": 6, "e": 7}
merged = {**d1, **d2, **d3}
print(merged)
```
При объединении словарей через `**` для совпадающих ключей сохраняется значение из последнего словаря.
## Распаковка аргументов функций
Распаковку коллекций можно использовать при передаче параметров в функции.
Распаковка списка или кортежа эквивалента передаче в функцию позиционных аргументов. Варианты вызова функции `weather_forecast()` с передачей в нее позиционных аргументов:
```python
def weather_forecast(city, date):
...
weather_forecast("Omsk", "tomorrow")
weather_forecast(*("Omsk", "tomorrow"))
lst = ["Omsk", "tomorrow"]
weather_forecast(*lst)
```
Также в аргументы можно распаковать словарь. Это будет эквивалентно передаче именованных аргументов: `f(**d)`.
По аналогии с примером выше о передаче позиционных аргументов напишите три варианта вызова функции `weather_forecast()` с именованными аргументами `city="Omsk"` и `date="tomorrow"`: {.task_text}
- Обычная передача именованных аргументов.
- Создание и распаковка словаря при вызове функции.
- Распаковка уже существующего объекта словаря при вызове функции.
```python {.task_source #python_chapter_0250_task_0100}
def weather_forecast(city, date):
...
```
По сути создание и распаковка словаря при вызове функци и распаковка уже существующего словаря практически не отличаются: в обоих случаях перед объектом словаря должно стоять `**`. {.task_hint}
```python {.task_answer}
def weather_forecast(city, date):
...
weather_forecast(city="Omsk",date="tomorrow")
weather_forecast(**{"city": "Omsk", "date": "tomorrow"})
d = {"city": "Omsk", "date": "tomorrow"}
weather_forecast(**d)
```
## Резюмируем
- Распаковка — это присваивание отдельным переменным значений из итерабельного объекта: `a, b, c = lst`
- Упаковка — это объединение переменных в коллекцию: `*lst, = 1, 2, 3`.
- Оператор `*` используется для распаковки и упаковки любых итерабельных объектов.
- Оператор `**` используется только для распаковки коллекций пар ключ-значение.
- Множественное присваивание позволяет обменивать значения переменных, не прибегая к временной переменной: `a, b = b, a`.
- Распаковку можно применить для объединения коллекций, передачи аргументов в функции.
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!