Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
И сотворил Гвидо строки по образу C, по образу массивов символов сотворил их. И увидел Гвидо, что это хорошо. Или нет?
Представьте, что вы пишете совершенно идиоматичный код по обходу неких данных с вложенностью. Beautiful is better than ugly, simple is better than complex, так что вы останавливаетесь на следующем варианте кода:
Вы пишите юнит-тест, и что бы вы думали? Он не работает, причём не просто не работает, а
Как? Почему? В поисках ответа вы погрузитесь в удивительный мир коллекций бесконечной глубины.
В самом деле, строка — это единственный встроенный
Другой пример. Где-то в коде вам потребовалось многократно проверять наличие элементов в контейнерах. Вы решаете написать хелпер, который ускоряет это разными способами. Вы пишете универсальное решение, использующее только метод
Иии… ваше решение не работает! Ну вот! Опять!
(Зато неправильный ответ был выдан реально быстро...)
Почему? Потому что строка в Python — это удивительная коллекция, в которой семантика метода
В самом деле, строка — это коллекция:
Но коллекция… чего?
Но
Хотя поведение
А, кстати, знаете, почему? Потому что мы почти никогда не пользуемся строкой как коллекцией символов в скриптовом языке! Манипуляции конкретными символами в строке, доступ по индексу — чаще всего удел задач на собеседованиях. Так, может, из строки стоит убрать
Время для пятничного обсуждения в комментариях!
Представьте, что вы пишете совершенно идиоматичный код по обходу неких данных с вложенностью. 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()
? Это решило бы обе обозначенные проблемы.Время для пятничного обсуждения в комментариях!