Python, являясь языком программирования общего назначения, предоставляет набор встроенных типов данных, включая int, str, tuple, list, dict и set. Четыре последних считаются контейнерами, так как могут содержать другие объекты данных. По сравнению с другими контейнерами (list, dict и set) разработчики меньше всего обсуждают кортежи, хотя это очень удобные в использовании структуры данных, которые нам следует хорошо знать. 

В этой статье мы уделим внимание 5 особенностям их использования помимо основных операций, таких как создание кортежей и извлечение элемента по индексу. Более того, вас ждет очень подробное объяснение каждого случая, чтобы лучше разобраться в сопутствующих понятиях. 

1. Множественное присваивание 

Многие статьи, перечисляющие ловкие приемы Python, включают следующий пример обмена значениями двух переменных без создания третьей. Однако большинство таких статей-перечислений не стремятся объяснить, что именно происходит в этом конкретном случае использования. Понимаете, что я имею в виду? Посмотрим код.

Обмен значений переменных:

>>> a = 5
>>> b = 'five'
>>> a, b = b, a
>>> a
'five'
>>> b
5

В приведенном выше коде (в 3-ей строке) показан способ обмена значений двух переменных в Python. Как видим, после обмена переменные получили новые значения, что нам и требовалось. Теперь рассмотрим синтаксис.

Собственно говоря, данный процесс состоит из двух понятных действий. С правой стороны присваивания (а вы, конечно же, помните, что знак равенства может быть оператором присваивания) мы создаем объект tuple, используя две переменные. Вас может удивить отсутствие круглых скобок, заключающих эти переменные для создания tuple. Дело в том, что их использование является необязательным при создании непустого объекта tuple. А вот и доказательство

Кортежи без круглых скобок:

>>> # При объявлении кортежа, состоящего из одного объекта, не забывайте ставить запятую. Помните об этом, так как мы будем использовать ее в дальнейшем. 
>>> c = 1,
>>> type(c)
<class 'tuple'>
>>> d = 3, 5
>>> type(d)
<class 'tuple'>

С левой стороны присваивания мы распаковываем или деструктурируем вновь созданный объект tuple с помощью двух переменных, каждая из которых относится к элементу в соответствующей позиции. Такое действие возможно, поскольку кортежи являютсятипом данных последовательностии, следовательно, отслеживают позиции своих элементов. Следующий код демонстрирует способ распаковки объекта tuple, состоящего из трех элементов, каждый из которых присваивается переменной.

Распаковка кортежа:

>>> e, f, g = (2, 4, 6)
>>> print(f"e is {e}\nf is {f}\ng is {g}")
e is 2
f is 4
g is 6

Примечателен тот факт, что, несмотря на принадлежность этих двух переменных к однимтипам данных, Python позволяет присвоить им и другие типы (например, для переменной a изначально int и затем str) благодаря динамической типизации. Таким образом, в процессе своего жизненного цикла переменная может иметь различные типы.

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

Множественное присваивание:

>>> code, message = 404, "Not Found"
>>> print(f"code is {code}\nmessage is {message!r}")
code is 404
message is 'Not Found'

Этот прием особенно полезен при объявлении множественных переменных в одной строке кода. Однако лучше его использовать только со взаимосвязанными переменными. Например, в данном коде обе из них принадлежат к части HTTP-запроса. Если они семантически разнородны, то лучше будет объявить их в разных строках, чтобы яснее отразить цель этой операции и облегчить понимание кода.  

2. Распаковка с символами _ и * 

В предыдущем разделе уже была затронута тема распаковки кортежа. Мы использовали то же самое число переменных для распаковки каждого элемента объекта tuple, что и было создано на правой стороне оператора присваивания. Однако это не единственный вариант данной операции. Один из способов подразумевает применение символа “_”, указывающего на то, что мы не собираемся использовать нераспакованную переменную в этой конкретной позиции. Рассмотрим следующий пример.

Распаковка кортежа с символом “_” :

>>> http_response = (200, "The email has been validated")
>>> _, message = http_response
>>> message
'The email has been validated'
>>> _
200

Как вы видите, мы заинтересованы в получении сообщения запроса, а не его кода. Следовательно, первую позицию занимает символ “_”. Однако стоит отметить, что, хотя при помощи этого знака мы заявляем о своем намерении не использовать соответствующее значение, он является действительным именем переменной и содержит ссылку на первый элемент. Но имейте в виду, что если в дальнейших операциях снова будет использоваться символ “_”, то ему будет присваиваться последнее значение, в связи с чем это значение не будет всегда одним и тем же (т.е. в нашем случае 200). 

Если объект tuple содержит множественные элементы, есть вероятность, что нам могут понадобиться только некоторые из них. Предположим, у нас есть такой объект, в котором хранятся отсортированные оценки гимнаста, а нам нужны только средние из них. Мы можем сделать следующее.

Распаковка кортежа с символом * :

>>> scores = (8.9, 9.2, 9.3, 9.4, 9.5, 9.9)
>>> min_score, *valid_scores, max_score = scores
>>> print(f"min score: {min_score}\nvalid scores: {valid_scores}\nmax score: {max_score}")
min score: 8.9
valid scores: [9.2, 9.3, 9.4, 9.5]
max score: 9.9

Как видно из примера, для распаковки объекта tuple мы использовали три переменные. Переменным min_score и max_score соответствуют первый и последний элементы, а valid_scores представляет значения всех средних элементов. А происходит это потому, что при использовании символа * в качестве префикса для valid_scores она подхватывает все значения, не присвоенные другим переменным. Например, сделаем так.

Распаковка кортежа с использование символа * (другие варианты): 

>>> *valid_scores, max_score = scores
>>> valid_scores
>>> [8.9, 9.2, 9.3, 9.4, 9.5]
>>> _, *valid_scores = scores
>>> valid_scores
>>> [9.2, 9.3, 9.4, 9.5, 9.9]

3. Переменное количество позиционных аргументов в функциях 

В продолжении темы предыдущего примера кому-то может стать интересно, что произойдет, используй мы просто одну переменную с символом * для распаковки объекта tuple

Распаковка кортежа с символом * (ошибка): 

>>> *valid_scores = scores
  File "<stdin>", line 1
SyntaxError: starred assignment target must be in a list or tuple

Увы, не сработало. Но заметьте, что ошибка здесь относится к типу SyntaxError, и если мы ее устраним, то операция станет возможна. Но в действительности эта задачка не из простых, поскольку не совсем понятно, что точно означает сообщение об ошибке. 

Подсказка: проверьте комментарий, в котором был объявлен объект tuple (например, переменная c) только с одним элементом. Кроме того, важно обратить внимание на то, что вышеуказанный код пытается распаковать объект tuple, и обе переменные в этой операции присваивания являются объектами tuple без круглых скобок. 

Надеюсь, что вы уже нашли решение. Правильно — ответ кроется в магической запятой, наличие которой обязательно при условии использования только одной переменной внутри объекта tuple. Рассмотрим исправленный вариант фрагмента кода.

Распаковка кортежа с символом * (с исправленной ошибкой):

>>> *valid_scores, = scores
>>> valid_scores
[8.9, 9.2, 9.3, 9.4, 9.5, 9.9]

Наиболее важный случай использования одной переменной подразумевает захват неопределенного или переменного числа позиционных аргументов при объявлении функции. Думаю, что вам приходилось встречать *args в определениях некоторых функций. Обратимся к следующему типичному примеру.

Функция с *args:

>>> def calculate_mean(*numbers):
...     print(f"{type(numbers)} | {numbers}")
...     average = sum(numbers)/len(numbers)
...     return average
... 
>>> calculate_mean(3, 4, 5, 6)
<class 'tuple'> | (3, 4, 5, 6)
4.5

Как вы видите, для объявленной функции был использован аргумент *numbers (его можно называть *args, что непринципиально), обозначающий ее способность принимать переменное количество позиционных аргументов. Обратите внимание, что они будут захвачены аргументом, отмеченным *. В нашем примере кода мы вызвали функцию с четырьмя числами, все из которых были упакованы в объект tuple с именем numbers в функции. 

В этой связи, вы также могли встречать использование **kwargs в объявлениях функций, которое относится к переменному числу именных аргументов. 

4. Относительная неизменяемость 

В Python мы располагаем изменяемыми и неизменяемыми типами данных, которые указывают на то, могут или нет меняться значения конкретных объектов после их создания. В рамках данной статьи мы не будем подробно обсуждать такую обширную тему, как изменяемость типов данных. Здесь важно отметить, что кортежи относятся к неизменяемым данным. Обратимся к примеру кода.

Неизменяемый кортеж:

>>> http_response = (200, "Data Fetched")
>>> http_response[1] = "New Data Fetched"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

В этом примере у нас не получилось присвоить новое значение объекту tuple в указанной позиции. Однако все немного усложнится, если элемент объекта tuple будет изменяемым контейнером. Сможем ли мы его изменить? Без примера не обойтись.

Неизменяемый кортеж с изменяемыми элементами:

>>> http_response1 = (200, [1, 2, 3])
>>> http_response1[1].append(4)
>>> http_response1
(200, [1, 2, 3, 4])
>>> http_response2 = (200, {1: 'one'})
>>> http_response2[1][2] = 'two'
>>> http_response2
(200, {1: 'one', 2: 'two'})

Из примера становится понятно, что мы можем обновлять значение кортежа, если элемент объекта tuple является изменяемым, будь то объект list или dict. В этом смысле он подвержен изменениям, что дает нам право говорить об относительной неизменяемости кортежа. 

Но что же тогда мы имеем в виду, когда продолжаем характеризовать кортежи как неизменяемые типы данных? Дело в том, что неизменяемость кортежей связана не со значениями, которые они содержат, а со ссылками на объекты, и вот эти то ссылки как раз не меняются. Предлагаю проверить,так ли это. В примере ниже мы видим, что даже при изменении значения одного из списков оба они остаются в памяти всё тем же объектом, о чём свидетельствует один и тот же адрес памяти при вызове функции id() для интроспекции.

Изменяемый элемент кортежа:

>>> http_response1 = (200, [1, 2, 3])
>>> id(http_response1[1])
4312112096   
>>> http_response1[1].append(4)
>>> http_response1
(200, [1, 2, 3, 4])
>>> id(http_response1[1])
4312112096

5. Именованные кортежи 

Один из распространенных случаев использования кортежей заключается в хранении взаимосвязанных данных конкретного объекта, и тут на ум приходит мысль об объекте tuple как о содержащем сведения для строки в таблице данных. Например, создадим кортеж c информацией о студенте: 

>>> student = ("John Smith", 17, 178044)
>>> print(f"name: {student[0]}\nage: {student[1]}\nstudent id: {student[2]}")
name: John Smith
age: 17
student id: 178044

В этом коде нам приходится использовать правильный индекс для извлечения конкретного элемента данных, что не совсем удобно и чревато ошибками, упусти мы его из внимания, особенно если речь идет о гораздо большем объеме данных в реальном проекте. 

И хотя есть возможность реализовать пользовательский класс для отображения данных, мы воспользуемся преимуществом создания класса named tuple, доступного в модуле collections как часть стандартной библиотеки. С помощью функции namedtuple нам не составит труда создать облегченный тип, способный вместить взаимосвязанные данные как объект tuple. Удобство созданного класса named tuple состоит в том, что он позволяет нам использовать точечную нотацию для извлечения конкретного элемента. Если что-то еще непонятно, то следующий пример поможет всё прояснить.

Именованный кортеж — Student:

>>> from collections import namedtuple
>>> Student = namedtuple("Student", "name age student_id")
>>> student = Student("John Smith", 17, 178044)
>>> print(f"name: {student.name}\nage: {student.age}\nstudent id: {student.student_id}")
name: John Smith
age: 17
student id: 178044

Во 2-ой строке кода мы создали новый тип Student посредством вызова функции namedtuple, которая является фабричной функцией для создания класса named tuple. По сути, мы назвали тип конкретным именем Student и он содержит три элемента с атрибутами, перечисленными в строке и разделенными пробелами. Как вариант, атрибуты можно перечислить в виде объекта list, как: namedtuple(“Student”, [“name”, “age”, “student_id”])

В 3-ей и 4-ой строках был созданэкземпляр класса Student. Примечательно, что мы смогли использовать точечную нотацию для обращения к атрибутам Student, что более удобно и менее подвержено ошибкам, чем использование метода индексации с обычным объектом tuple в предыдущих примерах. 

Если мы заинтересованы в выполнении проверок интроспекции, можем использовать функции issubclass и isinstance для получения дополнительной информации о взаимосвязи класса Student и его экземпляров с типом данных tuple, как в следующем примере.

Именованный кортеж (интроспекция): 

>>> issubclass(Student, tuple)
True
>>> isinstance(student, tuple)
True

Итак, когда мы имеем дело с облегченным типом для хранения и извлечения данных, мы можем рассмотреть вариант использования фабричной функции namedtuple для создания класса named tuple, тем самым упростив процесс работы с данными. В отличие от пользовательских классов использование класса named tuple требует меньше ресурсов, что положительно отражается на памяти. 

Заключение 

В данной статье мы рассмотрели 5 важных особенностей использования кортежей в Python дополнительно к основным функциональностям, обычно применяемых с объектами tuple. Я уверен, что эти знания помогут вам качественно задействовать кортежи в повседневных проектах Python. 

Благодарю за внимание!

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Yong Cui, Ph.D.: 5 Things to Know to Level Up Your Skills With Tuples in Python

Предыдущая статьяLocalStack: запуск AWS на локальном компьютере
Следующая статьяПовышение дизайнерских навыков: советы и упражнения