Кортежи — одна из наиболее широко используемых структур данных в Python. Подобно спискам, они могут содержать значения разных типов, к которым вы можете получить доступ, используя нотацию с квадратными скобками. Основное различие между кортежами и списками заключается в неизменяемости, а это означает, что попытка установить элемент во время выполнения вызовет гнев интерпретатора, который отомстит, выпалив это печально известное исключение:
TypeError: ‘tuple’ object does not support item assignment
Но, несмотря на свой свирепый нрав и бескомпромиссный характер, кортежи — это гораздо больше, чем просто неизменяемые списки!
Еще немного о неизменности.
Начнем с того, что поговорим о преимуществах неизменяемых структур данных. Представьте, что вам нужно получить название города, используя его координаты. Вы можете использовать словарь, где ключами будут координаты, а соответствующим значением будет название города. Но могли бы произойти жуткие вещи, если бы Python позволял использовать в качестве ключей неизменяемые (или фактически не хэшируемые) объекты.
Конечно, вы могли бы создать свой собственный класс Coordinates, реализующий метод хеширования, что дало бы некую чудовищность, подобную этой:
class Coordinates: def __init__(self, latitude, longitude): self.latitude = latitude self.longitude = longitude def hash(self): return str(self.latitude) + " " + str(self.longitude) def __repr__(self): return f"({self.latitude}, {self.longitude})" cities = { Coordinates(48.864716, 2.349014): "Paris", Coordinates(51.509865, -0.118092): "London" } my_position = Coordinates(51.509865, -0.118092) print(f"The city at {my_position} is {cities[my_position]}")
Но ты действительно хочешь написать это… когда ты мог бы написать…
cities = { (48.864716, 2.349014): "Paris", (51.509865, -0.118092): "London" } my_position = (51.509865, -0.118092) print(f"The city at {my_position} is {cities[my_position]}")
это?
Нет, это не так.
Предположим теперь, что вы хотите создать функцию, которая принимает пару координат в качестве необязательного аргумента.
Что ж, на самом деле является законным использовать не хэшируемые объекты в качестве аргументов по умолчанию. Но вы должны быть осторожны, так как это может привести к неожиданному поведению. Я не буду слишком отвлекаться, но вот что вы читаете по теме:
- https://stackoverflow.com/questions/1132941/least-astonishment-and-the-mutable-default-argument
- https://florimond.dev/blog/articles/2018/08/python-mutable-defaults-are-the-source-of-all-evil/
Но кортежи, используемые в качестве аргументов по умолчанию, будут вести себя точно так же, как и любой другой примитивный тип (int, str…) и избавят вас от всех хлопот и неопределенности, связанных с изменчивостью…
Пакет. Сплат. Распаковать.
Кортежи предлагают множество небольших функций, которые делают ваш код аккуратным и чистым. К ним относятся упаковка и распаковка, которые позволяют создавать данные и манипулировать ими самым лаконичным способом, какой только можно придумать.
Упаковка состоит в присвоении нескольких значений одной или нескольким переменным, а распаковка — обратная операция: она «распутывает» кортежи, чтобы присвоить все их элементы нескольким переменным. Вот пример:
first_name, surname, age, city = "John", "Doe", 3.14, "Paris" person = first_name, surname, age, city first_name, surname = person[:2]
В первой строке мы создаем четыре переменные и инициализируем каждую из них значением.
Во второй строке мы создаем кортеж, присваивая несколько значений одной переменной (packing).
Затем мы распаковываем наш кортеж в третьей строке, чтобы извлечь первые два элемента и сохранить их в две отдельные переменные. (Ну, эта строка технически бесполезна, так как переменные first_name и surname уже существуют и содержат те же значения, но эй…)
Распространенным примером удобочитаемости и эффективности распаковки кортежей является замена двух переменных.
Без этих функций мы могли бы реализовать это следующим образом:
temporary = foo foo = bar bar = temporary
Нам нужно обменивать оба значения по отдельности, для чего требуется временная переменная. Используя распаковку, мы могли бы сделать этот код не только короче, но и гораздо более явным и удобным для чтения:
foo, bar = bar, foo
Смысл этой строчки ясен, как нос на лице любого человека, впервые в жизни читающего код. Берем бар и фу, втыкаем бар в фу и фу в бар и вуаля!
Упаковка и распаковка также позволяют создавать функции, которые возвращают несколько переменных и элегантно извлекают их:
from time import time def execution_time(f): """ Takes in a callable, calls it and returns the execution time and the output of the callable. """ start = time() result = f() end = time() return end — start, result exec_time, output = execution_time(my_function)
Если вам не нужно хранить все переменные, обычной практикой является использование символов подчеркивания для именования ненужных переменных:
exec_time, _ = execution_time(my_function)
Упаковка и распаковка не были бы такими полезными и мощными без сильного и красивого оператора Splat(*). Его первая работа была как раз за именем параметра в определении функции, которой он позволял передавать неопределенное количество аргументов:
def join_strings(*strings, separator): result = "" if len(strings) > 0: result += strings[0] for string in strings[1:]: result += separator + string return result
Параметр strings представляет собой кортеж и содержит все значения, переданные функции. Обратите внимание, что поскольку параметр разделитель определяется после параметра splatted, он автоматически считается только ключевым словом, чтобы избежать двусмысленности:
join_strings(" ") # TypeError: join_strings() missing 1 required keyword-only argument: 'separator' join_strings(separator=" ") # ""
Но Сплат вырос в умного и сильного взрослого Оператора и получил повышение с выпуском Python 3. Давайте посмотрим на все его новые способности.
[примечание: похоже, люди в этой ветке StackOverflow не одобряют название «splat». «Звездочка» или «звездочка» может быть более понятным и более распространенным, но «знак» звучит мило, поэтому я буду придерживаться его:
https://stackoverflow.com/questions/2322355/правильное-имя-для-питона-оператора]
Помните мистера Доу? Вот его идентификационный кортеж:
person = “John”, “Doe”, 3.14, “Paris”
Оператор splat может помочь нам получить несколько смежных полей одновременно:
*name, age, city = person
что дает вам:
name = [“John”, “Doe”] # a list age = 3.14 city = “Paris”
Оператор splat можно использовать только один раз в операторе (ну, это полуправда, но мы к этому еще вернемся). Имя, связанное с оператором знака, может находиться в любой позиции и может содержать ноль, одно или несколько значений. В приведенном ниже примере показано его поведение в различных обстоятельствах.
values = 1, 2, 3, 4, 5 a, *b, c = values # a = 1; b = [2, 3, 4]; c = 5 *a, b, c = values # a = [1, 2, 3]; b = 4; c = 5 a, b, *c = values # a = 1; b = 2; c = [3, 4, 5] a, b, c, d, e, *f = values # a = 1, b = 2; c = 3; d = 4; e = 5; f = [] a, *b, c, d, e, f = values # a = 1; b = []; c = 2; d = 3; e = 4; f = 5
Распаковка кортежей также работает с вложенными кортежами. Давайте добавим два новых поля в наш кортеж person:
person = “John”, “Doe”, 3.14, “Paris”, (“Jane”, “Doe”, 2.73, “London”), (“Jack”, “Doe”, “2.41”, “Trifouillis-les-Oies”)
При распаковке такого кортежа внутренние кортежи обрабатываются (почти) как любой другой тип, что означает, что вы можете получить к ним доступ следующим образом:
*name, age, city, mother, father = person # mother and father are tuples
и что вы не можете пытаться хранить их элементы отдельно:
*name, age, city, mother_first_name, mother_surname, mother_age, mother_city, father_first_name, father_surname, father_age, father_city = person # ValueError: not enough values to unpack (expected at least 10, got 6)
Однако вы все еще можете использовать скобки для прямой распаковки внутренних кортежей:
*name, age, city, (mother_first_name, mother_surname, mother_age, mother_city), (father_first_name, father_surname, father_age, father_city) = person
Поскольку вы распаковываете каждый внутренний кортеж в другой области, можно использовать оператор splat один раз внутри каждой пары скобок:
*name, age, city, (*mother_name, mother_age, mother_city), (*father_name, father_age, father_city) = person
Мы также можем использовать оператор splat для распаковки итерируемых объектов и передачи их значений функции или создания списка из нескольких итерируемых объектов:
a = [1, 2, 3] b = ["one", "two", "three"] c = ["un", "deux", "trois"] [a, b, c] # a 2-dimensional list [*a, *b, *c] # a 1-dimensional list containing all the elements of a, all the elements of b and all the elements of c.
Как мы видели ранее, данные, переданные в функцию через параметр splatted, становятся кортежем. Но нам может понадобиться передать его функции, которая требует распаковки с помощью оператора splat. Чтобы проиллюстрировать это, вернемся к нашей функции execution_time:
def execution_time(f): """ Takes in a callable, calls it and returns the execution time and the output of the function. """ start = time() result = f() end = time() return end — start, result
Проблема этой функции в том, что in может вызвать только функцию, которая не принимает аргументов. Эту проблему можно решить, добавив новый параметр, который будет распаковываться и затем передаваться в функцию:
def execution_time(f, *arguments): """ Takes in a callable, calls it and returns the execution time and the output of the function. """ start = time() result = f(*arguments) end = time() return end — start, result
Наконец, оператор splat позволяет распаковать ключи словаря:
french_to_english = {"coquelicot": "poppy", "jonquille": "daffodil", "lilas": "lily"} print("Translate the following words into English:", *french_to_english) # prints: "Translate the following words into English: coquelicot jonquille lilas"
Именованные кортежи
Последняя часть этой статьи посвящена классу namedtuple из встроенного модуля collections. Именованные кортежи похожи на структуры, которые мы можем найти в таких языках, как C. Они могут содержать значения, к которым вы можете получить доступ, используя их имя, и, как следует из названия, сохраняют большинство свойств кортежи, включая неизменяемость и распаковку.
Вы можете определить новый именованный кортеж следующим образом:
from collections import namedtuple Person = namedtuple("Person", "first_name surname age city")
Конструктор принимает имя кортежа, а также список атрибутов, разделенных пробелом. Затем вы можете создать экземпляр как класс:
a = Person("John", "Doe", 3.14, "Paris") b = Person("Jane", "Doe", 2.73, "London") c = Person("John", "Doe", 2.41, "Trifouillis-les-Oies")
Вы можете получить доступ к их полям через их имена, как объект:
if a.age > b.age: print(f"{a.first_name} is older than {b.first_name}")
Но вы также можете распаковать их как кортеж:
*name, age, city = a
И лучшая часть распаковки — это когда вы хотите перебрать список (именованных) кортежей.
Попробуем вывести личность каждого члена семьи Доу:
family = [a, b, c] for first_name, surname, age, city in family: print(f"Hello, my name is {first_name} {surname}, I am {age} and I live in {city}!") # Hello, my name is John Doe, I am 3.14 and I live in Paris! # Hello, my name is Jane Doe, I am 2.73 and I live in London! # Hello, my name is John Doe, I am 2.41 and I live in Trifouillis-les-Oies!
Вместо того, чтобы извлекать каждый атрибут один за другим внутри блока с отступом, распаковка namedtuples позволяет нам выбирать и называть все поля, которые мы хотим использовать непосредственно при объявлении цикла for, делая наш код много, много яснее и лаконичнее.