Операторы in и not in в Python позволяют быстро определить, является ли данное значение частью коллекции значений. Такой тип проверки часто встречается в программировании, и в Python он известен как тест на принадлежность. Поэтому эти операторы известны как операторы принадлежности.
БЕСПЛАТНО СКАЧАТЬ КНИГИ по Python на русском языке можно у нас в телеграм канале "Python книги на русском"
Содержание
- Проверка принадлежности в Python
- Оператор in в Python
- Оператор not in в Python
- Использование in и not in с различными типами Python
- Применение операторов in и not in в Python
- Использование operator.contains() для тестов членства
- Поддержка тестов членства в пользовательских классах
Проверка принадлежности в 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, если целевое значение присутствует в коллекции значений. В противном случае возвращает False | value in collection |
not in | Возвращает True, если целевое значение НЕ присутствует в коллекции значений. В противном случае возвращает False | value 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».
Пингбэк: Как перевести строку в нижний регистр в Python - pythonturbo
Пингбэк: Как проверить, содержит ли строка подстроку - pythonturbo
Пингбэк: Метод строк find() в Python
Пингбэк: Как посчитать уникальные символы в строке в Python