5 доказательств силы итерируемых объектов в Python

Что такое итерируемые объекты? 

Итерируемые (перебираемые) объекты — это коллекция важных структур данных в Python. Например, к ним относятся такие встроенные типы, как строки, списки и словари. Если вам приходилось применять функции высшего порядка (map и filter), то, скорее всего, вам известно, что они также создают итерируемые объекты (а именно, объекты map и filter). Кроме того, вы могли слышать и о генераторах, которые также являются высокопроизводительными объектами данного типа с эффективным использованием памяти.  

Эти типы данных — общеизвестные примеры итерируемых объектов в Python. Но что именно они из себя представляют? Воспользуемся определением, предложенным Лучано Рамальо в его замечательной книге о продвинутом программирования на Python “Fluent Python: Clear, Concise, and Effective Programming”. По сути, итерируемыми являются такие объекты, которые могут быть преобразованы в итераторы, чьи элементы в дальнейшем можно перебирать (итерировать) для выполнения определенных операций. 

Любой объект со встроенной функцией iter может получить итератор. Объекты, реализующие метод __iter__ , который возвращает итератор, называются итерируемыми объектами”, —  Лучано Рамальо, “Fluent Python: Clear, Concise, and Effective Programming

Итерируемые объекты настолько важны, что повсеместно используются для разных целей. Здесь предпринята попытка представить систематический, но не исчерпывающий обзор самых распространенных случаев их использования в Python. Надеюсь, что знакомство с перебираемыми объектами поможет вам эффективнее применять их в своих проектах.

1. Итерация циклами for 

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

  1. Строки являются итерируемыми объектами, а отдельные символы считаются элементами строковых объектов. 
  2. По умолчанию словари итерируются по ключам. Если вам нужно провести итерацию по значениям и элементам (например, по парам ключ-значение), то следует использовать такие методы словаря, как values() и items()
>>> i_str = 'Hello'
... i_list = [0, 1]
... i_tuple = (404, 'No Data')
... i_dict = {'zero': 0, 'one': 1}
... i_set = {'John', 'Danny'}
... iterables = [i_str, i_list, i_tuple, i_dict, i_set]
... 
... for iterable in iterables:
...     print(f"*** Start to iterate over an iterable of {type(iterable)}")
...     for i, item in enumerate(iterable, 1):
...         print(item, end='__' if i < len(iterable) else '\n')
... 
*** Start to iterate over an iterable of <class 'str'>
H__e__l__l__o
*** Start to iterate over an iterable of <class 'list'>
0__1
*** Start to iterate over an iterable of <class 'tuple'>
404__No Data
*** Start to iterate over an iterable of <class 'dict'>
zero__one
*** Start to iterate over an iterable of <class 'set'>
John__Danny

2. Создание коллекций разных типов данных 

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

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

>>> # Создаем список целых чисел 
... integers = list(range(10))
... print(f'* List: {integers}')
... 
... # Создаем кортеж целых чисел 
... numbers = tuple(integers)
... print(f'* Tuple: {numbers}')
... 
... # Создаем словарь 
... items = [('zero', 0), ('one', 1), ('two', 2)]
... words = dict(items)
... print(f'* Dict: {words}')
... 
... # Создаем множество 
... evens = (-2, 4, 2, 0, 2, -4, 4)
... unique_evens = set(evens)
... print(f'* Set: {unique_evens}')
... 
* List: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
* Tuple: (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
* Dict: {'zero': 0, 'one': 1, 'two': 2}
* Set: {0, 2, 4, -4, -2}

Если вы занимаетесь наукой о данных или машинным обучением, то вам, вероятно, известны пакеты NumPy и Pandas, самые основные структуры данных которые создаются на основе перебираемых объектов. Следующий фрагмент кода отображает стандартные случаи использования:

>>> import numpy as np
... import pandas as pd
... 
... # Создаем массив numpy из списка 
... numbers = [1, 2, 3, 4, 5, 6]
... num_array = np.array(numbers)
... print('* Numpy Array:', num_array, '\n')
... 
... # Создаем pd Series из списка 
... names = ['John', 'Zack', 'Danny']
... names_series = pd.Series(names)
... print('* pd Series:\n', names_series, '\n')
... 
... # Создаем pd DataFrame из словаря  
... grades = {'name': names, 'grade': [99, 100, 98]}
... grades_df = pd.DataFrame(grades)
... print('* pd DataFrame:\n', grades_df)
... 
* Numpy Array: [1 2 3 4 5 6] 

* pd Series:
 0     John
1     Zack
2    Danny
dtype: object 

* pd DataFrame:
     name  grade
0   John     99
1   Zack    100
2  Danny     98

3. Представления (списков, словарей, множеств) и выражения-генераторы 

Одной особенно полезной возможностью Python является техника представления. Главным образом представления используются для создания списков, множеств и словарей, и соответствующие техники называются представлениями списков, представлениями множеств и словарей. Ниже представлены их основные формы. 

# Представление списков 
[expression for item in iterable if optional_condition]

# Представление словарей 
{key_expr: value_expr for item in iterable if optional_condition}

# Представление множеств 
{expression for item in iterable if optional_condition}

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

Следующий фрагмент демонстрирует примеры использования данных техник представления:

>>> # Создаем исходный итерируемый объект для работы со всеми представлениями 
... numbers = list(range(-3, 4))
... print(f'Initial Iterable: {numbers}')
... 
... # Представление списка 
... squares_list = [x*x for x in numbers]
... print(f'* listcomp: {squares_list}')
... 
... # Представление словаря 
... squares_dict = {x: x*x for x in numbers if x % 2 == 0}
... print(f'* dictcomp: {squares_dict}')
... 
... # Представление множества 
... squares_set = {x*x for x in numbers}
... print(f'* setcomp: {squares_set}')
... 
Initial Iterable: [-3, -2, -1, 0, 1, 2, 3]
* listcomp: [9, 4, 1, 0, 1, 4, 9]
* dictcomp: {-2: 4, 0: 0, 2: 4}
* setcomp: {0, 9, 4, 1}

Выражение-генератор, применяемое для создания генераторов, похоже на техники представления. На выходе оно также производит итерируемый объект (генератор). Но в отличие от перебираемых объектов представлений генераторы могут выдавать значения по запросу, таким образом экономно расходуя ресурсы памяти. Рассмотрим синтаксис и его применение для создания генератора. Сравнение генераторов и списков:

>>> # Синтаксис выражения-генератора 
... # (expression for x in iterable)
... 
... # Сравнение генераторов и списков 
... squares_gen = (x*x for x in range(1000000))
... print(f'* Size of Generator: {squares_gen.__sizeof__()}')
... 
... squares_list = [x*x for x in range(1000000)]
... print(f'* Size of List: {squares_list.__sizeof__()}')
... 
... sum_gen = sum(squares_gen)
... sum_list = sum(squares_list)
... print(f'# Sum of Squares Using Generator: {sum_gen}')
... print(f'# Sum of Squares Using List: {sum_list}')
... 
* Size of Generator: 96
* Size of List: 8697440
# Sum of Squares Using Generator: 333332833333500000
# Sum of Squares Using List: 333332833333500000

Как видно из примера, генераторы экономно расходуют память, поскольку они не загружают все элементы списка. Обратите внимание, что генераторы и списки могут выдавать одинаковый результат при вычислении суммы квадратов чисел с использованием встроенной функции sum()

4. Распаковка списков и кортежей 

При работе со списками и кортежами обычно требуется использовать отдельные части данных. С этой целью чаще всего применяются методы индексации/среза, которые позволяют получить либо конкретные элементы, либо блоки данных. Однако эти методы могут вызывать сложности в связи с использованием так называемых магических чисел (возможно, кому-то будет непросто разобраться, почему были задействованы те или иные индексы). Вместо вышеописанного подхода мы можем применить метод распаковки, подразумевающий последовательное присваивание элементов конкретным переменным. Обратимся к простому примеру:

>>> numbers = [0, 1, 2]
... letters = ('x', 'y', 'z')
... 
... number0, number1, number2 = numbers
... letter0, letter1, letter2 = letters
... 
... print(f'* Numbers: {number0}, {number1}, {number2}')
... print(f'* Letters: {letter0}, {letter1}, {letter2}')
... 
* Numbers: 0, 1, 2
* Letters: x, y, z

Здесь число использованных переменных соответствует числу элементов в итерируемом объекте. Однако Python предоставляет возможность повысить эффективность метода распаковки за счет применения принципа catch-all. Точнее говоря, если число используемых переменных меньше числа элементов в перебираемом объекте, то переменная с символом * подхватит все оставшиеся элементы. Обратите внимание, что она будет создана в виде итерируемого объекта (объекта списка). Вот конкретный пример:

>>> more_numbers = list(range(10))
... first_number, *middle_numbers, last_number = more_numbers
... print(f'* First Number: {first_number}')
... print(f'* Middle Numbers: {middle_numbers}')
... print(f'* Last Number: {last_number}')
... 
* First Number: 0
* Middle Numbers: [1, 2, 3, 4, 5, 6, 7, 8]
* Last Number: 9

Соответственно переменным first_number и last_number присваиваются первый и последний элементы списка. Примечательно, что переменная middle_numbers подхватывает все числа в середине последовательности и имеет тип списка

5. Функции высшего порядка 

Итерируемые объекты также используются в качестве входных (параметров) и выходных (возвращаемых значений) данных некоторых функций высшего порядка. К их числу относятся две хорошо известные функции map и filter. Функция filterпринимает итерируемый объект и функцию фильтрации, которую применяет ко всем элементам данного объекта. Если элемент удовлетворяет условию, то он сохраняется в перебираемом объекте. Рассмотрим на примере принцип действия функции. Как мы видим, она фильтрует последовательность чисел, оставляя только те, что делятся на 3. Обратите внимание, что созданный объект filterявляется итератором, поддерживающим итерацию.:

>>> # Принцип использования функции filter 
... # filter(filtering_func, iterable)
... 
... # Использование функции filter 
... fibonacci = [1, 1, 2, 3, 5, 8, 13, 21]
... filtered_fibonacci = filter(lambda x: x % 3 == 0, fibonacci)
... print(f'Filter Object: {filtered_fibonacci}')
... print(f'Filter Type: {type(filtered_fibonacci)}')
... 
... for item in filtered_fibonacci:
...     print(f'Item: {item}')
... 
Filter Object: <filter object at 0x10c462850>
Filter Type: <class 'filter'>
Item: 3
Item: 21

Функция mapпринимает функцию отображения и один или более итерируемых объектов. Число таких объектов должно совпадать с числом параметров, принимаемых функцией. Каждый элемент перебираемого объекта будет передаваться в отображающую функцию для обработки.Отображенные же элементы будут включены в получаемый на выходе объект-итератор map. Рассмотрим пример работы функции mapв следующем фрагменте кода. В отличие от функции filterона преобразует объект, используя функцию отображения без оценки того, какие элементы включены.

>>> # Принцип функции map 
... # map(mapping_func, *iterables)
... 
... # Функция map 
... fibonacci = [1, 1, 2, 3, 5, 8, 13, 21]
... mapped_fibonacci = map(lambda x: f'1/{x}', fibonacci)
... print(f'Map Object: {mapped_fibonacci}')
... print(f'Map Type: {type(mapped_fibonacci)}')
... 
... for item in mapped_fibonacci:
...     print(f'Mapped Item: {item}')
... 
Map Object: <map object at 0x10c462610>
Map Type: <class 'map'>
Mapped Item: 1/1
Mapped Item: 1/1
Mapped Item: 1/2
Mapped Item: 1/3
Mapped Item: 1/5
Mapped Item: 1/8
Mapped Item: 1/13
Mapped Item: 1/21

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

Заключение 

Статья была посвящена 5-ти самым распространенным случаям использования итерируемых объектов в Python. Это понятие довольно объемное и включает в себя различные встроенные типы данных (например, строки, списки и словари). Полагаю, что материал статьи расширит ваши знания по этой теме и поможет вам решать большинство задач, связанных с подобными объектами, в проектах Python. Благодарю за внимание! Счастья всем Python-программистам!

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

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


Перевод статьи Yong Cui, Ph.D.: Why Iterables Are Powerful in Python — Understand These 5 Distinct Usages