Базовый Python
Today

Set comprehensions в Python: как и когда их использовать

Set comprehensions в Python предоставляют лаконичный способ для того, чтобы создавать, преобразовывать и фильтровать множества Python. Этот инструмент позволит писать более чистый и эффективный код, который будет легко поддерживать и читать.

Чтобы получить максимальную пользу от этого статьи, желательно знать знать такие базовые концепции Python, как циклы for, итерируемые объекты, также пригодятся list comprehensions (списковые включения) и dictionary comprehensions (словарные включения).

Создание и преобразование множеств в Python

Для создания множеств в Python у нас есть несколько инструментов: литералы множеств, конструктор set() и циклы for.В следующих разделах мы кратко рассмотрим примеры с ними. И также узнаем о set comprehensions.

Создание множеств при помощи литералов и set()

Литералы множеств — перечисление элементов в фигурных скобках. Вот так:

{element_1, element_2,..., element_N}

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

# Пример множества
colors = {"blue", "red", "green", "orange", "green"}
print(colors)
# {'red', 'green', 'orange', 'blue'}

colors.add("purple")
print(colors) 
# {'red', 'green', 'orange', 'purple', 'blue'}

Тут мы создаем множества с различными цветами. Элементы в этом множестве являются уникальными строковыми объектами. Можно добавлять новые элементы с помощью метода .add().

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

Также можно создать новое множество с помощью конструктора set() из итерируемого объекта:

numbers = [2, 2, 1, 4, 2, 3]

print(set(numbers))
# {1, 2, 3, 4}

Здесь мы создаём новое множество с помощью set() и списка из чисел. Обратим внимание, что результирующее множество не содержит дублей. На практике конструктор set() часто используется для удаления дублирующихся значений в итерируемых объектах.

А чтобы создать пустое множество, просто вызываем конструктор set() без аргументов:

print(set())
# set()

Нельзя создать пустое множество с помощью литерала, так как пара фигурных скобок {} используется для создания пустого словаря, а не множества. Для создания пустого множества обязательно нужно использовать конструктор set().

Использование цикла for для наполнения множеств

Иногда нам нужно создать пустое множество и динамически заполнять его элементами. Для этого можно использовать цикл for. Предположим, что мы хотим создать множество уникальных слов из текста. Вот как это можно сделать с помощью цикла:

unique_words = set()

text = """
Beautiful is better than ugly
Explicit is better than implicit
Simple is better than complex
Complex is better than complicated
""".lower()

for word in text.split():
    unique_words.add(word)


print(unique_words)

# {
#    'beautiful',
#    'ugly',
#    'better',
#    'implicit',
#    'complicated',
#    'than',
#    'explicit',
#    'is',
#    'complex',
#    'simple'
# }

В этом примере сначала создаётся пустое множество с помощью set(). Затем к тексту применяется метод .lower(), чтобы привести текст к нижнему регистру. После этого запускается цикл по списку слов, извлеченных из текста при помощи split(). Метод .split() разбивает текст на список отдельных слова, используя пробел в качестве разделителя.

Цикл добавляет слова в множество на каждой итерации. Однако, если слово уже в нём присутствует, то ещё один такой же элемент не будет добавлен. Поэтому в этом примере результирующее множество содержит только один экземпляр слова better.

Цикл легко читается и понятен, но можно использовать и set comprehension, чтобы сделать код ещё более лаконичным.

Введение в set comprehensions

Set comprehensions позволяют создавать множества с помощью однострочного цикла for. Если вы знакомы с list comprehensions, то быстро поймете set comprehensions. Эти конструкции имеют схожий синтаксис. Основное отличие заключается в том, что set comprehensions используют фигурные скобки вместо квадратных:

{expression for member in iterable [if condition]}

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

Синтаксис set comprehension включает четыре основных элемента:

  • Фигурные скобки ({}) служат для определения set comprehension.
  • Выражение, которое возвращает элемент на каждой итерации.
  • Имя текущего элемента в итерируемом объекте.
  • Итерируемый объект может быть любым типом включая списки, кортежи, множества, генераторы и подобные структуры.

Рассмотрим следующий код, который демонстрирует, как создать множество уникальных слов с помощью set comprehension:

text = """
Beautiful is better than ugly
Explicit is better than implicit
Simple is better than complex
Complex is better than complicated
""".lower()

print({word for word in text.split()})
# {
#    'beautiful',
#    'ugly',
#    'better',
#    'implicit',
#    'complicated',
#    'than',
#    'explicit',
#    'is',
#    'complex',
#    'simple'
# }

В этом примере мы используем set comprehension для извлечения уникальных слов из исходного текста. Синтаксис лаконичен, код легко читается.

Но когда нам не нужно преобразовывать данные для создания множества, то можно заменить такой цикл следующим кодом:

print(set(text.split()))
# {
#    'beautiful',
#    'ugly',
#    'better',
#    'implicit',
#    'complicated',
#    'than',
#    'explicit',
#    'is',
#    'complex',
#    'simple'
# }

Здесь мы передаем разбитый по пробелу текст непосредственно в set() и получаем тот же результат, что и с циклом или comprehension. Так можно, потому что в данной ситуации элементы множества не нуждаются в каких-то преобразованиях.

Comprehensions могут включать более одного for. В таких случаях крайний левый for итерируется по внешней структуре, следующий for слева направо — по первому уровню вложенности и так далее.

Давайте рассмотрим пример, в котором у нас есть список списков, каждый из которых содержит числа. Нам нужно создать множество из их квадратных значений. Для этого можно использовать comprehension с двумя for:

matrix = [
    [9, 3, 8, 3],
    [4, 5, 2, 8],
    [6, 4, 3, 1],
    [1, 0, 4, 5],
]

print({value**2 for row in matrix for value in row})
# {64, 1, 0, 4, 36, 9, 16, 81, 25}

Здесь первый for итерируется по строкам "матрицы", а второй for итерируется по числам в каждой "строке". В результате мы получаете множество квадратных значений.

Отметим, что в результате мы получаем 9 элементов вместо 16. Это связано с тем, что элементы множества должны быть уникальны. Когда элемент дублируется, новая копия не добавляется в итоговое множество.

Использование set comprehensions в Python

С помощью set comprehensions можно создавать новые множества, преобразовывать существующие и фильтровать элементы по каким-либо условиям. Далее посмотрим, как это делать.

Создание множеств из итерируемых объектов

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

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

Множества являются более эффективными структурами для выполнения таких проверок, поэтому преобразуем исходный список во множество из строк в нижнем регистре:

tools = ["Python", "Django", "Flask", "pandas", "NumPy"]
tools_set = {tool.lower() for tool in tools}
print(tools_set)
# {'django', 'numpy', 'flask', 'pandas', 'python'}

print("python".lower() in tools_set)
# True
print("Pandas".lower() in tools_set)
# True
print("Numpy".lower() in tools_set)
# True

Затем мы используем оператор in для проверки, присутствует ли конкретный элемент в исходном списке.

Если вам не нужно применять какие-либо преобразования к элементам итерируемого объекта, то и set comprehension не требуется. Вместо этого можно просто использовать set(iterable). Но в примере выше мы приводили строки к нижнему регистру.

Преобразование существующих множеств

Set comprehensions также могут быть использованы для удобного преобразования существующих множеств. Предположим, что у нас есть список адресов электронной почты, введенных пользователем. Но некоторые адреса содержат пробелы и прописные буквы, и нам нужно исправить эту проблему. Кроме того, есть одинаковые адреса.

Чтобы исправить данные, мы можем использовать следующий comprehension:

emails = {
    " [email protected] ",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    " [email protected]",
    "[email protected]",
}

print({email.strip().lower() for email in emails})
# {
#    '[email protected]',
#    '[email protected]',
#    '[email protected]',
#    '[email protected]',
#    '[email protected]'
# }

Используемый set comprehension удаляет пробелы с начала и конца каждого адреса при помощи метода .strip(). Затем все адреса приводятся к нижнему регистру при помощи .lower(). В результате преобразований и дублирующийся адрес удалится, потому что множества могут содержать только уникальные элементы.

Фильтрация элементов во множествах

Иногда нам нужно отфильтровать существующее множество и создать новое множество с элементами, которые соответствуют какому-либо условию. Предположим, что мы хотим отфильтровать наше множество адресов электронной почты, чтобы оставить только адреса в зоне .com. Для этого можно использовать следующий comprehension:

emails_set = {
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
    "[email protected]",
}

print({email for email in emails_set if email.endswith(".com")})
# {'[email protected]', '[email protected]', '[email protected]'}

Здесь используется условие для создания множества, которое включает только адреса электронной почты в домене .com.

Когда применять set comprehensions?

При выборе между обычным циклом и set comprehension, нужно учитывать следующие факторы:

  • Краткость: set comprehensions сокращают объем кода по сравнению с эквивалентными циклами for.
  • Читаемость: они могут сделать код более очевидным и читаемым.

Можно использовать set comprehensions, когда нужно выполнить следующие операции:

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

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

print(set([2, 2, 1, 4, 2, 3]))
# {1, 2, 3, 4}

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

Плохие практики

Существует несколько плохих практик, которых следует избегать при использовании set comprehension. Вот некоторые из них:

  • использование сложных выражений в comprehensions;
  • написание вложенных comprehensions с несколькими for или условиями;
  • попытка доступа к переменным comprehensions извне;
  • выполнение дорогостоящих преобразований при создании элементов множества.

Иногда при использовании set comprehensions мы сталкиваемся с ситуациями, когда выражение в comprehension становится слишком сложным. Например, у нас есть список чисел, и мы хотим создать множество, которое будет содержать квадраты чётных чисел и кубы нечётных. Для этого можно применить следующий comprehension:

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

print({number**2 if number % 2 == 0 else number**3 for number in numbers})
# {64, 1, 4, 36, 100, 16, 343, 729, 27, 125}

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

result_set = set()

for number in numbers:
    if number % 2 == 0:
        value = number**2
    else:
        value = number**3
    result_set.add(value)

print(result_set)

Этот цикл дает тот же результат, что и comprehension, но он более понятен и читабелен.

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

Попытка использовать переменные, определенные в comprehension, вне самого comprehension приведёт к ошибке. Например:

set_ = {number**3 for number in range(1, 11)}
print(number)

Traceback (most recent call last):
    ...
NameError: name 'number' is not defined

Переменная number видна только внутри comprehension. Если попытаться использовать ее вне comprehension, то получим исключение NameError.

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

Заключение

Мы достаточно подробно познакомились с set comprehensions и узнали как:

  • создавать новые множества с помощью лаконичного синтаксиса;
  • преобразовывать существующие множества с помощью comprehensions;
  • фильтровать элементы из существующих множеств;
  • определять, когда стоит использовать set comprehensions, а когда лучше этого избегать.

Теперь вы сможете писать более читаемый и лаконичный код 😊

👉🏻Подписывайтесь на PythonTalk в Telegram 👈🏻

👨🏻‍💻Ещё больше полезного на OlegTalks💬

🍩 Поддержите канал 🫶

Источник: RealPython