Операторы in и not in в Python

Операторы in и not in в Python позволяют быстро определить, является ли данное значение частью коллекции значений. Такой тип проверки часто встречается в программировании, и в Python он известен как тест на принадлежность. Поэтому эти операторы известны как операторы принадлежности.

БЕСПЛАТНО СКАЧАТЬ КНИГИ по Python на русском языке можно у нас в телеграм канале "Python книги на русском"

Содержание

Проверка принадлежности в Python

Иногда требуется выяснить, присутствует ли значение в коллекции. Другими словами, вам нужно проверить, является ли данное значение членом коллекции значений. Такой тип проверки обычно называют тестом на принадлежность.

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

Рассмотрим следующую функцию is_member():

>>> def is_member(value, iterable):
...     for item in iterable:
...         if value is item or value == item:
...             return True
...     return False
...

Эта функция принимает два аргумента: целевое значение и коллекцию значений (итерируемый объект). Цикл перебирает итерируемый объект, а условный оператор проверяет, равно ли целевое значение текущему. Обратите внимание, что условие проверяет идентичность объектов с помощью оператора is или равенство значений с помощью оператора равенства ==. Это немного разные, но взаимодополняющие проверки.

Если условие истинно, то функция возвращает True, выходя из цикла. Этот ранний возврат замыкает работу цикла. Если цикл завершается без какого-либо совпадения, то функция возвращает False:

>>> is_member(5, [2, 3, 5, 9, 7])
True

>>> is_member(8, [2, 3, 5, 9, 7])
False

Первый вызов функции is_member() возвращает True, потому что целевое значение 5 является членом рассматриваемого списка [2, 3, 5, 9, 7]. Второй вызов функции возвращает False, потому что 8 не присутствует во входном списке значений.

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

ОператорОписаниеСинтаксис
inВозвращает True, если целевое значение присутствует в коллекции значений. В противном случае возвращает Falsevalue in collection
not inВозвращает True, если целевое значение НЕ присутствует в коллекции значений. В противном случае возвращает Falsevalue not in collection

Как и в случае с булевыми операторами, Python предпочитает удобство чтения, используя в качестве операторов обычные английские слова, а не потенциально непонятные символы.


Примечание: Не путайте ключевое слово in, когда оно работает как оператор членства, с ключевым словом in в синтаксисе цикла for. Они имеют совершенно разные значения. Оператор in в Python проверяет, входит ли значение в коллекцию значений, а ключевое слово in в цикле for указывает на итерируемый объект.


Как и многие другие операторы, in и not in являются бинарными. Это означает, что вы можете создавать выражения, соединяя два операнда. В данном случае это:

  • Левый операнд – целевое значение, которое вы хотите найти в коллекции значений
  • Правый операнд – коллекция значений, в которой может быть найдено целевое значение.

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

value in collection
или
value not in collection

В этих выражениях value может быть любым объектом Python. А collection может быть любым типом данных, который может содержать коллекции значений, включая списки, кортежи, строки, множества и словари. Это также может быть класс, реализующий метод .__contains__(), или пользовательский класс, явно поддерживающий тесты принадлежности или итерацию.

Если вы правильно используете операторы in и not in, то выражения, которые вы построите с их помощью, всегда будут иметь булево значение. Другими словами, эти выражения всегда будут возвращать либо True, либо False.

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

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

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

Примечание редакции. О других операторах можно почитать в статье “Операторы в Python”.

Оператор in в Python

Чтобы лучше понять оператор in в Python, начните с написания небольших наглядных примеров. Попробуйте определить, есть ли указанное значение в списке:

>>> 5 in [2, 3, 5, 9, 7]
True

>>> 8 in [2, 3, 5, 9, 7]
False

Первое выражение возвращает True, потому что 5 есть в списке чисел. Второе выражение возвращает False, потому что 8 не присутствует в списке.

Согласно документации по оператору in, выражение типа value in collection эквивалентно следующему коду:

any(value is item or value == item for item in collection)

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

Вызов any() проверяет, является ли хоть одно из полученных булевых значений True. В этом случае функция возвращает True. Если все значения False, то any() возвращает False.

Оператор not in в Python

Оператор not in при проверке принадлежности делает прямо противоположное. С помощью этого оператора вы можете проверить, не входит ли заданное значение в набор значений:

>>> 5 not in [2, 3, 5, 9, 7]
False

>>> 8 not in [2, 3, 5, 9, 7]
True

В первом примере вы получите False, потому что в списке [2, 3, 5, 9, 7] есть число 5. Во втором примере вы получите True, потому что 8 нет в списке значений. Чтобы избежать путаницы, помните: вы пытаетесь проверить, что значение НЕ является частью заданного набора значений.

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

Примечание: Конструкция not value in collection работает так же, как и конструкция value not in collection. Однако первый вариант сложнее для чтения. Поэтому используйте not in как единый оператор, а не not для отрицания результата in.

Использование in и not in с различными типами Python

Все встроенные последовательности, такие как списки, кортежи, объекты диапазона и строки, поддерживают тесты принадлежности с помощью операторов in и not in.

Коллекции, такие как множества и словари, также поддерживают эти тесты.

По умолчанию операции членства над словарями проверяют, есть ли в словаре заданный ключ. Однако словари также имеют методы, которые позволяют использовать операторы членства с ключами, значениями и парами ключ-значение.

В следующих разделах вы узнаете о некоторых особенностях использования in и not in с различными встроенными типами данных. Начнем со списков, кортежей и объектов диапазона.

Использование in со списками, кортежами и диапазонами в Python

Мы уже рассматривали примеры использования операторов in и not in для определения наличия заданного значения в существующем списке значений. То есть вы уже знакомы с тем, как тесты членства работают со списками.

С кортежами операторы членства работают так же, как и со списками:

>>> 5 in (2, 3, 5, 9, 7)
True

>>> 5 not in (2, 3, 5, 9, 7)
False

Здесь нет никаких сюрпризов. В первом примере оператор in возвращает True, потому что целевое значение (5) есть в кортеже. Во втором примере оператор not in возвращает противоположный результат.

Для списков и кортежей операторы членства используют алгоритм поиска, который перебирает элементы базовой коллекции. Поэтому, по мере увеличения длины итерируемого объекта, время поиска увеличивается прямо пропорционально. Используя нотацию большого “О”, можно сказать, что операции членства над этими типами данных имеют временную сложность O(n).

Если вы используете операторы in и not in с объектами диапазона (range object – результат работы функции range()), то получите аналогичный результат:

>>> 5 in range(10)
True

>>> 5 not in range(10)
False

>>> 5 in range(0, 10, 2)
False

>>> 5 not in range(0, 10, 2)
True

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


Примечание: При создании объектов диапазона в range() можно передать до трех аргументов. Аргумент start определяет число, с которого начинается диапазон. Аргумент stop – число, на котором диапазон должен прекратить генерировать значения. А step – это шаг между генерируемыми значениями. Эти три аргумента называют смещениями.


Рассмотрим следующие примеры, в которых шаг определяется случайным образом во время выполнения:

>>> from random import randint

>>> 50 in range(0, 100, randint(1, 10))
False

>>> 50 in range(0, 100, randint(1, 10))
False

>>> 50 in range(0, 100, randint(1, 10))
True

>>> 50 in range(0, 100, randint(1, 10))
True

На вашей машине вы можете получить другие результаты, поскольку вы работаете со случайными смещениями диапазона. В данном примере step – единственное изменяемое смещение. В реальном коде смещения start и stop тоже могут быть вариативными.

Для объектов диапазона алгоритм, лежащий в основе тестов на принадлежность, вычисляет наличие заданного значения в range object. Делается это с помощью выражения (value - start) % step) == 0, которое учитывает смещения, использованные для создания данного диапазона. Благодаря этому тесты на принадлежность очень эффективны при работе с объектами диапазона. В этом случае можно сказать, что их временная сложность равна O(1).


Примечание: Списки, кортежи и объекты диапазона имеют метод .index(), который возвращает индекс первого вхождения заданного значения в базовую последовательность. Этот метод полезен для определения местоположения значения в последовательности.

Некоторые могут подумать, что с помощью этого метода можно определить, находится ли значение в последовательности. Однако если значение не находится в последовательности, то .index() выдает ошибку ValueError:

>>> (2, 3, 5, 9, 7).index(8)
Traceback (most recent call last):
    ...
ValueError: tuple.index(x): x not in tuple

Вероятно, вы не захотите выяснять, находится ли значение в последовательности, вызывая исключения, поэтому для этой цели следует использовать оператор членства вместо .index().


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

# users.py

username = input("Username: ")
password = input("Password: ")

users = [("john", "secret"), ("jane", "secret"), ("linda", "secret")]

if (username, password) in users:
    print(f"Hi {username}, you're logged in!")
else:
    print("Wrong username or password")

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

Вот как этот код работает на практике:

$ python users.py
Username: john
Password: secret
Hi john, you're logged in!

$ python users.py
Username: tina
Password: secret
Wrong username or password

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

В этих примерах важно отметить, что порядок хранения данных в кортеже имеет решающее значение, потому что при сравнении кортежей что-то вроде ("john", "secret") не равно ("secret", "john"), несмотря на то, что элементы одинаковые.

В этом разделе мы рассмотрели примеры, демонстрирующие основное поведение операторов членства с обычными встроенными последовательностями Python. Однако осталась еще одна встроенная последовательность. Да, строки!

Использование операторов членства со строками

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

Вы можете использовать операторы in и not in со строками, когда вам нужно выяснить, присутствует ли данный символ в целевой строке. Предположим, что вы используете строки для установки и управления правами доступа к ресурсу:

>>> class User:
...     def __init__(self, username, permissions):
...         self.username = username
...         self.permissions = permissions
...

>>> admin = User("admin", "wrx")
>>> john = User("john", "rx")

>>> def has_permission(user, permission):
...     return permission in user.permissions
...

>>> has_permission(admin, "w")
True
>>> has_permission(john, "w")
False

Класс User принимает два аргумента: имя пользователя и набор разрешений. Для указания разрешений используется строка, в которой w означает, что пользователь имеет право на запись, r – право на чтение, а x – право на выполнение. Обратите внимание, что это те же буквы, что и в разрешениях файловой системы в стиле Unix.

Тест членства в has_permission() проверяет, имеет ли текущий пользователь заданное разрешение, возвращая True или False соответственно. Для этого оператор in ищет соответствующий символ в строке permissions. В данном примере вы хотите узнать, есть ли у пользователя разрешение на запись.

Однако в вашей системе разрешений есть скрытая проблема. Что произойдет, если вы вызовете функцию с пустой строкой? Вот вам ответ:

>>> has_permission(john, "")
True

Поскольку пустая строка всегда считается подстрокой любой другой строки, выражение типа "" в user.permissions вернет True. Это может привести к нарушению безопасности в вашей системе.

Вы также можете использовать операторы членства, чтобы определить, содержит ли строка подстроку:

>>> greeting = "Hi, welcome to Real Python!"

>>> "Hi" in greeting
True
>>> "Hi" not in greeting
False

>>> "Hello" in greeting
False
>>> "Hello" not in greeting
True

Для строкового типа данных выражение типа substring in string будет истинным, если substring является частью string. В противном случае выражение равно False.


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

>>> greeting.find("Python")
20

>>> greeting.find("Hello")
-1

Если подстрока присутствует в исходной строке, то .find() возвращает индекс, с которого она начинается. Если целевая строка не содержит подстроки, то в результате вы получите -1. Таким образом, выражение string.find(substring) >= 0 будет эквивалентно проверке substring in string.

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


Важный момент, который следует учитывать при использовании тестов членства в строках: сравнения строк чувствительны к регистру:

>>> "PYTHON" in greeting
False

Этот тест на принадлежность возвращает False, поскольку сравнение строк чувствительно к регистру, а “PYTHON” в верхнем регистре не присутствует в приветствии. Чтобы это обойти, можно нормализовать все ваши строки, используя метод .upper() или .lower():

>>> "PYTHON".lower() in greeting.lower()
True

В этом примере вы используете .lower() для перевода целевой подстроки и исходной строки в нижний регистр. Это преобразование обманывает чувствительность к регистру в неявном сравнении строк.

Использование операторов in и not in с генераторами

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

На практике генераторная функция – это функция, которая использует оператор yield в своем теле. Допустим, вам нужна функция-генератор, которая принимает список чисел и возвращает итератор, выдающий значения из исходных данных, возведенные в квадрат. В этом случае можно поступить следующим образом:

>>> def squares_of(values):
...     for value in values:
...         yield value ** 2
...

>>> squares = squares_of([1, 2, 3, 4])

>>> next(squares)
1
>>> next(squares)
4
>>> next(squares)
9
>>> next(squares)
16
>>> next(squares)
Traceback (most recent call last):
    ...
StopIteration

Эта функция возвращает объект-итератор, который выдает квадраты чисел по запросу. Для получения последовательных значений из итератора можно использовать встроенную функцию next(). Когда объект-итератор полностью исчерпан, он вызывает исключение StopIteration, чтобы сообщить, что больше значений не осталось.

В функции-генераторе типа squares_of() можно использовать операторы членства:

>>> 4 in squares_of([1, 2, 3, 4])
True
>>> 9 in squares_of([1, 2, 3, 4])
True
>>> 5 in squares_of([1, 2, 3, 4])
False

Оператор in работает, как и ожидалось. При использовании с итератором он возвращает True, если значение присутствует в итераторе, и False в противном случае.

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

Когда вы используете in или not in для итератора, при поиске целевого значения они будут исчерпывать итератор. Если значение присутствует в итераторе, то оператор употребит все значения итератора до целевого. Остальные значения по-прежнему будут доступны в итераторе:

>>> squares = squares_of([1, 2, 3, 4])

>>> 4 in squares
True

>>> next(squares)
9
>>> next(squares)
16
>>> next(squares)
Traceback (most recent call last):
    ...
StopIteration

В этом примере в итераторе есть значение 4, потому что это квадрат числа 2. Поэтому in возвращает True. Когда вы используете next() для получения следующего значения итератора, вы получаете 9 (квадрат числа 3). Этот результат подтверждает, что у вас больше нет доступа к первым двум значениям. Вы можете продолжать вызывать next() до тех пор, пока не получите исключение StopIteration, когда итератор будет исчерпан.

Аналогично, если значение не присутствует в итераторе, то оператор полностью исчерпает итератор, и вы не будете иметь доступа ни к одному из его значений:

>>> squares = squares_of([1, 2, 3, 4])

>>> 5 in squares
False

>>> next(squares)
Traceback (most recent call last):
    ...
StopIteration

В этом примере оператор in полностью использует все значения и возвращает False, потому что целевое значение отсутствует. Поскольку итератор теперь исчерпан, вызов next() приводит к ошибке StopIteration.

Итераторы также можно создавать с помощью генераторных выражений. Эти выражения используют тот же синтаксис, что и list comprehension,но квадратные скобки [] заменяются круглыми (). С результатом работы генераторного выражения можно использовать операторы in и not in:

>>> squares = (value ** 2 for value in [1, 2, 3, 4])
>>> squares
<generator object <genexpr> at 0x1056f20a0>

>>> 4 in squares
True

>>> next(squares)
9
>>> next(squares)
16
>>> next(squares)
Traceback (most recent call last):
    ...
StopIteration

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

При использовании операторов in и not in с итераторами может возникнуть еще одна критическая проблема. В частности, она может возникнуть при работе с бесконечными итераторами. Приведенная ниже функция возвращает итератор, который дает бесконечное количество целых чисел:

>>> def infinite_integers():
...     number = 0
...     while True:
...         yield number
...         number += 1
...

>>> integers = infinite_integers()
>>> integers
<generator object infinite_integers at 0x1057e8c80>

>>> next(integers)
0
>>> next(integers)
1
>>> next(integers)
2
>>> next(integers)
3
>>> next(integers)

Функция infinite_integers() возвращает итератор, выдающий по запросу целые числа. Но помните, что значений будет бесконечное множество. Поэтому не стоит использовать операторы членства с этим итератором. Почему? Ну, если целевого значения не окажется в итераторе, то вы столкнетесь с бесконечным циклом, из-за которого ваша программа зависнет.

Использование in и not in со словарями и множествами в Python

Операторы членства Python также работают со словарями и множествами. Если вы примените операторы in или not in непосредственно к словарю, то они будут проверять наличие заданного ключа. Такую проверку можно выполнить с применением метода .keys(), который более явно выразит ваши намерения.

Вы также можете проверить, находится ли заданное значение или пара ключ-значение в словаре. Для этих проверок можно использовать методы .values() и .items() соответственно:

>>> likes = {"color": "blue", "fruit": "apple", "pet": "dog"}

>>> "fruit" in likes
True
>>> "hobby" in likes
False
>>> "blue" in likes
False

>>> "fruit" in likes.keys()
True
>>> "hobby" in likes.keys()
False
>>> "blue" in likes.keys()
False

>>> "dog" in likes.values()
True
>>> "drawing" in likes.values()
False

>>> ("color", "blue") in likes.items()
True
>>> ("hobby", "drawing") in likes.items()
False

В этих примерах вы используете оператор in непосредственно для словаря likes, чтобы проверить, есть ли в словаре ключи “fruit”, “hobby” и “blue”. Обратите внимание: несмотря на наличие значения “blue” тест возвращает False, потому что он учитывает только ключи.

Далее вы используете метод .keys(), чтобы получить те же результаты. В данном случае явное название метода делает ваши намерения гораздо более понятными для других программистов, читающих ваш код.

Чтобы проверить, присутствует ли в likes значение типа “dog” или “drawing”, используется метод .values(), который возвращает объект представления со значениями в базовом словаре. Аналогично, чтобы проверить, содержится ли пара ключ-значение в likes, используется метод .items(). Обратите внимание, что целевые пары ключ-значение должны быть кортежами из двух элементов с ключом и значением в указанном порядке.

Что касается множеств, с ними операторы членства работают так же, как и со списками или кортежами:

>>> fruits = {"apple", "banana", "cherry", "orange"}

>>> "banana" in fruits
True
>>> "banana" not in fruits
False

>>> "grape" in fruits
False
>>> "grape" not in fruits
True

Эти примеры показывают, что вы можете проверить, содержится ли данное значение во множестве, используя операторы членства in и not in.

Теперь, когда вы знаете, как операторы in и not in работают с различными встроенными типами данных, давайте попробуем их применить.

Применение операторов in и not in в Python

Тесты на членство с in и not in – довольно распространенные операции в программировании. Вы найдете такие проверки во многих кодовых базах Python, и в своем коде вы тоже будете их использовать.

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

Замена цепочек операторов or

Использование теста принадлежности для замены сложного булева выражения c несколькими операторами or – полезный прием, позволяющий упростить код и сделать его более читабельным.

Предположим, что вам нужно написать функцию, которая принимает название цвета в виде строки и определяет, является ли этот цвет основным. Для этого вы будете использовать цветовую модель RGB (красный, зеленый и синий в этой модели – основные цвета):

>>> def is_primary_color(color):
...     color = color.lower()
...     return color == "red" or color == "green" or color == "blue"
...

>>> is_primary_color("yellow")
False

>>> is_primary_color("green")
True

В функции is_primary_color() используется составное булево выражение. С помощью оператора or это выражение проверяет, является ли входной цвет красным, зеленым или синим. Несмотря на то, что эта функция работает, условие может быть запутанным и трудным для чтения и понимания.

Приведенное выше условие можно заменить компактным и читабельным тестом принадлежности:

>>> def is_primary_color(color):
...     primary_colors = {"red", "green", "blue"}
...     return color.lower() in primary_colors
...

>>> is_primary_color("yellow")
False

>>> is_primary_color("green")
True

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

Как видите, основные цвета мы сохранили во множестве. Почему? Ответ вы найдете в следующем разделе.

Написание эффективных тестов на членство

Для реализации словарей и множеств в Python используется структура данных, называемая хэш-таблицей. Хеш-таблицы обладают замечательным свойством: поиск любого заданного значения в структуре данных занимает примерно одинаковое время, независимо от того, сколько значений в таблице. Используя нотацию Big O, можно сказать, что поиск значений в хэш-таблицах имеет временную сложность O(1). То есть они супербыстрые.

Итак, какое отношение эта особенность хэш-таблиц имеет к тестам на принадлежность со словарями и множествами? Оказывается, с этими типами операторы in и not in работают очень быстро. Если в тестах на принадлежность отдавать предпочтение словарям и множествам, а не спискам и другим типам данных, это позволит оптимизировать производительность вашего кода.

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

# performance.py

from timeit import timeit

a_list = list(range(100_000))
a_set = set(range(100_000))

list_time = timeit("-1 in a_list", number=1, globals=globals())
set_time = timeit("-1 in a_set", number=1, globals=globals())

print(f"Sets are {(list_time / set_time):.2f} times faster than Lists")

Этот скрипт создает список целых чисел со ста тысячами значений и множество с таким же количеством элементов. Затем скрипт вычисляет время, необходимое для определения наличия числа -1 в списке и множестве. Предварительно известно, что -1 не встречается ни в списке, ни во множестве. Поэтому оператору членства придется проверить все значения, прежде чем получить окончательный результат.

Как вы уже знаете, когда оператор in ищет значение в списке, он использует алгоритм с временной сложностью O(n). А при поиске значения во множестве он использует алгоритм поиска в хэш-таблице, временная сложность которого равна O(1). Этот факт может иметь большое значение с точки зрения производительности.

Запустите свой скрипт из командной строки с помощью следующей команды:

$ python performance.py
Sets are 1563.33 times faster than Lists

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

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

Однако обратите внимание, что не стоит преобразовывать существующий список во множество только для того, чтобы выполнить несколько тестов на принадлежность. Помните, что преобразование списка в множество – это операция с временной сложностью O(n).

Использование operator.contains() для тестов членства

Оператор in имеет эквивалентную функцию в модуле operator, который входит в стандартную библиотеку Python. Эта функция называется contains(). Она принимает два аргумента – коллекцию значений и целевое значение. Функция возвращает True, если входная коллекция содержит целевое значение:

>>> from operator import contains

>>> contains([2, 3, 5, 9, 7], 5)
True

>>> contains([2, 3, 5, 9, 7], 8)
False

Первым аргументом функции contains() является коллекция значений, а вторым – целевое значение. Обратите внимание, что порядок аргументов отличается от обычной операции членства, где целевое значение стоит на первом месте.

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

>>> points = [
...     (1, 3),
...     (5, 0),
...     (3, 7),
...     (0, 6),
...     (8, 3),
...     (2, 0),
... ]

>>> list(filter(lambda point: not contains(point, 0), points))
[(1, 3), (3, 7), (8, 3)]

В этом примере вы при помощи функции filter() получаете точки, которые не содержат координату 0. Для этого вы используете contains() в лямбда-функции. Так как filter() возвращает итератор, вы оборачиваете все в вызов list(), чтобы преобразовать итератор в список точек.

Хотя эта конструкция работает, она довольно сложна, поскольку подразумевает импорт contains(), создание лямбда-функции поверх нее и вызов нескольких функций. Вы можете получить тот же результат, используя представление списка либо с contains(), либо с оператором not in:

>>> [point for point in points if not contains(point, 0)]
[(1, 3), (3, 7), (8, 3)]

>>> [point for point in points if 0 not in point]
[(1, 3), (3, 7), (8, 3)]

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

Поддержка тестов членства в пользовательских классах

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

Скорее всего, вы добавите метод .__contains__() только в классы, которые будут работать как коллекции значений. Таким образом пользователи вашего класса смогут определить, хранится ли заданное значение в конкретном экземпляре вашего класса.

Рассмотрим пример. Допустим, вам нужно создать минимальную стековую структуру данных для хранения значений по принципу LIFO (last in, first out). Одним из требований к вашей пользовательской структуре данных является поддержка тестов членства. В итоге вы пишете следующий класс:

# stack.py

class Stack:
    def __init__(self):
        self.items = []

    def push(self, item):
        self.items.append(item)

    def pop(self):
        return self.items.pop()

    def __contains__(self, item):
        return item in self.items

Ваш класс Stack поддерживает две основные функции стека. Вы можете поместить значение на вершину стека и извлечь значение с вершины стека. Обратите внимание, что под капотом ваша структура данных использует объект list для хранения и манипулирования фактическими данными.

Ваш класс также поддерживает тесты принадлежности с помощью операторов in и not in. Для этого в классе реализован метод .__contains__(), который опирается на сам оператор in.

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

>>> from stack import Stack

>>> stack = Stack()
>>> stack.push(1)
>>> stack.push(2)
>>> stack.push(3)

>>> 2 in stack
True
>>> 42 in stack
False
>>> 42 not in stack
True

Ваш класс полностью поддерживает операторы in и not in. Отличная работа! Теперь вы знаете, как поддерживать тесты членства в своих классах.

Обратите внимание, что если у данного класса есть метод .__contains__(), то класс не обязательно должен быть итерируемым, чтобы операторы членства работали. В приведенном выше примере Stack не является итерируемым, но операторы все равно работают, потому что они получают результат из метода .__contains__().

Кроме метода .__contains__() существует еще как минимум два способа поддержки тестов на членство в пользовательских классах. Если в вашем классе есть метод .__iter__() или .__getitem__(), то операторы in и not in также работают.

Рассмотрим следующую альтернативную версию Stack:

# stack.py

class Stack:
    def __init__(self):
        self.items = []

    def push(self, item):
        self.items.append(item)

    def pop(self):
        return self.items.pop()

    def __iter__(self):
        yield from self.items

Специальный метод .__iter__() делает ваш класс итерабельным, а этого достаточно для работы тестов на членство. Попробуйте!

Другой способ поддержки тестов на принадлежность – реализовать в ваших классах метод .__getitem__(), который обрабатывает операции индексирования с использованием целочисленных индексов:

# stack.py

class Stack:
    def __init__(self):
        self.items = []

    def push(self, item):
        self.items.append(item)

    def pop(self):
        return self.items.pop()

    def __getitem__(self, index):
        return self.items[index]

Python автоматически вызывает метод .__getitem__(), когда вы выполняете операции индексирования базового объекта. В этом примере, когда вы делаете stack[0], вы получаете первый элемент в экземпляре Stack. Python использует преимущества метода .__getitem__(), чтобы операторы членства работали правильно.

Заключение

Теперь вы знаете, как выполнять тесты на принадлежность, используя операторы in и not in в Python. Такие проверки позволяют выяснить, присутствует ли заданное значение в коллекции значений. Это довольно распространенная операция в программировании.

Перевод статьи Leodanis Pozo Ramos «Python’s “in” and “not in” Operators: Check for Membership».