Представьте, что вы пишете совершенно идиоматичный код по обходу неких данных с вложенностью. Beautiful is better than ugly, simple is better than complex, так что вы останавливаетесь на следующем варианте кода:
from collections.abc import Iterable def traverse(list_or_value, callback): if isinstance(list_or_value, Iterable): for item in list_or_value: traverse(item, callback) else: callback(list_or_value)
Вы пишите юнит-тест, и что бы вы думали? Он не работает, причём не просто не работает, а
>>> traverse({"status": "ok"}, print) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 4, in traverse File "<stdin>", line 4, in traverse File "<stdin>", line 4, in traverse [Previous line repeated 989 more times] File "<stdin>", line 2, in traverse File "/usr/local/opt/python/libexec/bin/../../Frameworks/Python.framework/Versions/3.7/lib/python3.7/abc.py", line 139, in __instancecheck__ return _abc_instancecheck(cls, instance) RecursionError: maximum recursion depth exceeded in comparison
Как? Почему? В поисках ответа вы погрузитесь в удивительный мир коллекций бесконечной глубины.
В самом деле, строка — это единственный встроенный
Iterable, всегда возвращающий Iterable в качестве элемента! Мы можем, конечно, сконструировать другой пример, создав список и добавив его в себя разик-два, но часто ли вы встречаете такое в своём коде? А строка — это Iterable бесконечной глубины, пробравшийся под покровом ночи прямо в ваш продакшн.Другой пример. Где-то в коде вам потребовалось многократно проверять наличие элементов в контейнерах. Вы решаете написать хелпер, который ускоряет это разными способами. Вы пишете универсальное решение, использующее только метод
__contains__ (единственный метод в абстрактном базовом классе Container), но потом решаете добавить супер-оптимизацию для особого случая — коллекции. Ведь по ней можно просто пройтись и составить set!import functools from typing import Collection, Container def faster_container(c: Container) -> Container: if isinstance(c, Collection): return set(c) return CachedContainer(c) class CachedContainer(object): def __init__(self, c: Container): self._contains = functools.lru_cache()(c.__contains__) def __contains__(self, stuff): return self._contains(stuff)
Иии… ваше решение не работает! Ну вот! Опять!
>>> c = faster_container(othello_text) >>> "Have you pray'd to-night, Desdemona?" in c False
(Зато неправильный ответ был выдан реально быстро...)
Почему? Потому что строка в Python — это удивительная коллекция, в которой семантика метода
__contains__ не согласована с семантикой __iter__ и __len__.В самом деле, строка — это коллекция:
>>> from collections.abc import Collection >>> issubclass(str, Collection) True
Но коллекция… чего?
__iter__ и __len__ считают, что это коллекция символов:>>> s = "foo" >>> len(s) 3 >>> list(s) ['f', 'o', 'o']
Но
__contains__ считает, что это коллекция подстрок!>>> "oo" in s True >>> "oo" in list(s) False
Что можно сделать?
Хотя поведение
str.__contains__ может показаться странным в контексте реализаций __contains__ другими стандартными типами, это поведение — одна из многих мелочей, делающих Python таким удобным, как скриптовый язык; позволяющих писать на нём быстрый и литературный код. Предлагать изменять поведение этого метода я бы не стал, тем более что почти никогда мы не пользуемся им, чтобы проверить наличие единственного символа в строке.А, кстати, знаете, почему? Потому что мы почти никогда не пользуемся строкой как коллекцией символов в скриптовом языке! Манипуляции конкретными символами в строке, доступ по индексу — чаще всего удел задач на собеседованиях. Так, может, из строки стоит убрать
__iter__, спрятать его за какой-нибудь метод вроде .chars()? Это решило бы обе обозначенные проблемы.Время для пятничного обсуждения в комментариях!

