Python, являясь языком программирования общего назначения, предоставляет набор встроенных типов данных, включая int
, str
, tuple
, list
, dict
и set
. Четыре последних считаются контейнерами, так как могут содержать другие объекты данных. По сравнению с другими контейнерами (list
, dict
и set
) разработчики меньше всего обсуждают кортежи, хотя это очень удобные в использовании структуры данных, которые нам следует хорошо знать.
В этой статье мы уделим внимание 5 особенностям их использования помимо основных операций, таких как создание кортежей и извлечение элемента по индексу. Более того, вас ждет очень подробное объяснение каждого случая, чтобы лучше разобраться в сопутствующих понятиях.
Многие статьи, перечисляющие ловкие приемы 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-запроса. Если они семантически разнородны, то лучше будет объявить их в разных строках, чтобы яснее отразить цель этой операции и облегчить понимание кода.
В предыдущем разделе уже была затронута тема распаковки кортежа. Мы использовали то же самое число переменных для распаковки каждого элемента объекта 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]
В продолжении темы предыдущего примера кому-то может стать интересно, что произойдет, используй мы просто одну переменную с символом * для распаковки объекта 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
в объявлениях функций, которое относится к переменному числу именных аргументов.
В 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
Один из распространенных случаев использования кортежей заключается в хранении взаимосвязанных данных конкретного объекта, и тут на ум приходит мысль об объекте 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.
Благодарю за внимание!
Перевод статьи Pier Paolo Ippolito: GPU Accelerated Data Analytics & Machine Learning
Комментарии