10.05.2019       Выпуск 281 (06.05.2019 - 12.05.2019)       Статьи

Должны ли строки в Python быть итерируемы?

И сотворил Гвидо строки по образу C, по образу массивов символов сотворил их. И увидел Гвидо, что это хорошо. Или нет?

Представьте, что вы пишете совершенно идиоматичный код по обходу неких данных с вложенностью. Beautiful is better than ugly, simple is better than complex, так что вы останавливаетесь на следующем варианте кода

Читать>>




Экспериментальная функция:

Ниже вы видите текст статьи по ссылке. По нему можно быстро понять ссылка достойна прочтения или нет

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

И сотворил Гвидо строки по образу C, по образу массивов символов сотворил их. И увидел Гвидо, что это хорошо.

Или нет?

Представьте, что вы пишете совершенно идиоматичный код по обходу неких данных с вложенностью. 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()

? Это решило бы обе обозначенные проблемы.

Время для пятничного обсуждения в комментариях!






Разместим вашу рекламу

Пиши: mail@pythondigest.ru

Нашли опечатку?

Выделите фрагмент и отправьте нажатием Ctrl+Enter.

Система Orphus