Кортежи — одна из наиболее широко используемых структур данных в 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]}")

это?

Нет, это не так.

Предположим теперь, что вы хотите создать функцию, которая принимает пару координат в качестве необязательного аргумента.

Что ж, на самом деле является законным использовать не хэшируемые объекты в качестве аргументов по умолчанию. Но вы должны быть осторожны, так как это может привести к неожиданному поведению. Я не буду слишком отвлекаться, но вот что вы читаете по теме:

Но кортежи, используемые в качестве аргументов по умолчанию, будут вести себя точно так же, как и любой другой примитивный тип (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, делая наш код много, много яснее и лаконичнее.