Главная / Курсы / Python / Глава 25. Распаковка и упаковка
# Глава 25. Распаковка и упаковка Распаковка (unpacking, деструктуризация) — это разложение итерабельного объекта на отдельные значения. Упаковка (packing) — обратная операция по сбору переменных в коллекцию. Эти приемы делают код простым и очень лаконичным. ## Распаковка Допустим, отдельным переменным нужно присвоить значения из списка. Разработчики, не знакомые с синтаксисом распаковки, решают эту задачу примерно так: ```python lst = ["a", "b", "c"] a = lst[0] b = lst[1] c = lst[2] ``` С помощью распаковки то же самое делается куда проще: ```python lst = ["a", "b", "c"] a, b, c = lst print(a, b, c) ``` ``` a b c ``` Главное — соблюдать правило: количество элементов в распаковываемом объекте **должно совпадать** с количеством переменных, в которые осуществляется распаковка. В нашем примере слева от оператора `=` находятся 3 переменные; справа — список из 3-х элементов. Нарушение этого правила приведет к исключению `ValueError`: ```python 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 a = 0 b = 1 swap = a a = b b = swap print(a, b) ``` ``` 1 0 ``` А вот как выглядит обмен через распаковку: ```python a = 0 b = 1 a, b = b, a print(a, b) ``` ``` 1 0 ``` Этот подход называется **множественным присваиванием** (parallel assignment) или позиционным присваиванием. Значения присваиваются сразу нескольким переменным, по приоритету слева направо. Что выведет этот код? {.task_text} В случае исключения напишите `error`. {.task_text} ```python 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 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 d = {"a": 1, "b": 2} k1, k2 = d print(k1, k2) ``` ``` a b ``` Чтобы распаковать пары ключ-значение из словаря, воспользуемся методом `items()`. Тогда в результирующие переменные сохранятся кортежи: ```python 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 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 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 a, _, b = ["a", "x", "b"] print(a, _, b) ``` ``` a x b ``` Как видите, `_` — это обычная переменная, такая же как `a` и `b`. Просто ее имя подсказывает разработчикам и линтерам, что дальше по коду не предполагается ее использовать. Распакуем кортеж, из которого нам нужны второй и третий элементы: ```python _, second, third, _ = (1, 2, 3, 4) print(second, third, _) ``` ``` 2 3 4 ``` Переменной `_` было присвоено значение 1, затем 4. Его мы и видим в консольном выводе. Значит, в одну и ту же переменную можно распаковывать много элементов. Сохранится значение последнего из них. Теперь посмотрим, как пропустить несколько элементов, запаковав их в список. Для этого нам нужен оператор `*`: он упаковывает объекты в коллекцию. Распакуем список, чтобы достать из него последний элемент. Все предшествующие элементы **упакуем** в переменную `_`: ```python *_, last = [1, 2, 3, 4, 5] print(_) print(last) print(type(_)) ``` ``` [1, 2, 3, 4] 5 <class 'list'> ``` Оператор `*` подсказал интерпретатору, что в переменную `_` нужно упаковать все элементы кроме последнего, который присвоился переменной `last`. Распакуем первый и последний символы строки: ```python 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 *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 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 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 head = [1, 2] tail = [3, 4, 5] merged = head + tail ``` А можем применить синтаксис распаковки: ```python head = [1, 2] tail = [3, 4, 5] merged = [*head, *tail] print(merged) ``` ``` [1, 2, 3, 4, 5] ``` Мы вновь использовали оператор `*`: в зависимости от контекста он как упаковывает, так и распаковывает. В данном случае он распаковал списки `head` и `tail` в отдельные значения, которые сохранились в результирующий список `merged`. Преимуществом объединения коллекций через распаковку является возможность добавлять к распаковываемым коллекциям отдельные объекты: ```python 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`. - Распаковку можно применить для объединения коллекций, передачи аргументов в функции.
Отправка...

Если вам нравится проект, вы можете поддержать его!

Задонатить