Подавление обработки строки как iterable

UPDATE:

Идея создания встроенных строк non-iterable была предложенная на python.org в 2006 году. Мой вопрос отличается тем, что я пытаюсь только подавлять эти функции время от времени; все же весь этот поток весьма уместен.

Вот критические комментарии Guido, которые внедрили non-iterable str на пробной основе:

[...] Я реализовал это (было действительно просто do), но затем я обнаружил, что мне пришлось исправить тонны мест, которые повторяются строки. Например:

  • sre parser и compiler используют такие вещи, как set ( "0123456789" ), а также перебирают символы входного regexp для его синтаксического анализа.

  • difflib имеет API, определенный для двух списков строк (типичный поэтапный разброс файла) или двух строк (типичный внутрилинейный diff) или даже два списка (для обобщенного последовательность diff).

  • небольшие изменения в optparse.py, textwrap.py, string.py.

И я даже не в том месте, где структура regrtest.py даже работает (из-за проблемы с difflib).

Я отказываюсь от этого проекта; патч SF patch 1471291. Я не дольше в пользу этой идеи; это просто не практично, а помещение что есть несколько веских причин для итерации по строке, было опровергнуты случаями использования, которые я нашел как в sre, так и в difflib.

ОРИГИНАЛЬНЫЙ ВОПРОС:

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

# record has to support [] operation to set/retrieve values
# fields has to be an iterable that contains the fields to be set
def set_fields(record, fields, value):
  for f in fields:
    record[f] = value

set_fields(weapon1, ('Name', 'ShortName'), 'Dagger')
set_fields(weapon2, ('Name',), 'Katana')
set_fields(weapon3, 'Name', 'Wand') # I was tired and forgot to put parentheses

Никакое исключение не будет поднято, и нет простого способа поймать его, кроме как путем тестирования для isinstance(fields, str) в бесчисленных местах. В некоторых случаях эта ошибка займет очень много времени.

Я хочу отключить строки, которые будут обрабатываться как итерируемые целиком в моем проекте. Это хорошая идея? Можно ли это сделать легко и безопасно?

Возможно, я мог бы подкласса встроенного str, так что мне нужно было бы явно вызвать get_iter(), если бы я хотел, чтобы его объект рассматривался как итерабельность. Тогда, когда мне нужен строковый литерал, я бы вместо этого создал объект этого класса.

Вот некоторые касательные вопросы:

Как определить, является ли переменная python строкой или списком?

как указать переменную, является итерируемой, но не строкой

Ответ 1

Нет никаких способов сделать это автоматически, к сожалению. Решение, которое вы предлагаете (подкласс str, который не является итерируемым), страдает от той же проблемы, что и isinstance()... а именно, вы должны помнить, что используете его везде, где используете строку, потому что нет возможности сделать Python используйте его вместо родного класса. И, конечно же, вы не можете обезопасить встроенные объекты.

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

На мой взгляд, наименее навязчивое дело - поставить чек в функцию и называть это, когда вы попадаете в цикл. Это, по крайней мере, приводит к изменению поведения, где вы, скорее всего, его увидите: в инструкции for, не скрытой где-то в классе.

def iterate_no_strings(item):
    if issubclass(item, str):   # issubclass(item, basestring) for Py 2.x
        return iter([item])
    else:
        return iter(item)

for thing in iterate_no_strings(things):
    # do something...

Ответ 2

Чтобы расширить и сделать ответ из этого:

Нет, вы не должны этого делать.

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

Вы можете это сделать, и методы, которые вы указали, вероятно, являются лучшими способами ( для записи, я думаю, что более подходящий вариант. Если вам нужно это сделать, см. @kindall метод), но это просто не стоит делать, и это не очень pythonic. Избегайте ошибок в первую очередь. В вашем примере вы можете спросить себя, является ли эта проблема более ясной в ваших аргументах, и могут ли быть более эффективными решения или аргументы splat.

Например: измените порядок.

def set_fields(record, value, *fields):
  for f in fields:
    record[f] = value

set_fields(weapon1, 'Dagger', *('Name', 'ShortName')) #If you had a tuple you wanted to use.
set_fields(weapon2, 'Katana', 'Name')
set_fields(weapon3, 'Wand', 'Name')

Например: Именованные аргументы.

def set_fields(record, fields, value):
  for f in fields:
    record[f] = value

set_fields(record=weapon1, fields=('Name', 'ShortName'), value='Dagger')
set_fields(record=weapon2, fields=('Name'), value='Katana')
set_fields(record=weapon3, fields='Name', value='Wand') #I find this easier to spot.

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

class Record:
    ...
    def set_fields(self, *fields, value):
        for f in fileds:
            self[f] = value

weapon1.set_fields("Name", "ShortName", value="Dagger")

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

В качестве альтернативы, если вы используете Python 3, у вас всегда есть возможность использовать расширенную распаковку:

def set_fields(*args):
      record, *fields, value = args
      for f in fields:
        record[f] = value

set_fields(weapon1, 'Name', 'ShortName', 'Dagger')
set_fields(weapon2, 'Name', 'Katana')
set_fields(weapon3, 'Name', 'Wand')

Или, для моего последнего примера:

class Record:
    ...
    def set_fields(self, *args):
        *fields, value = args
        for f in fileds:
            self[f] = value

weapon1.set_fields("Name", "ShortName", "Dagger")

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

Ответ 3

Проверка типов в этом случае не является беспризорной или плохой. Просто выполните:

if isinstance(var, (str, bytes)):
    var = [var]

В начале вызова. Или, если вы хотите обучить вызывающего:

if isinstance(var, (str, bytes)):
    raise TypeError("Var should be an iterable, not str or bytes")

Ответ 4

Что вы думаете о создании строки без итераций?

class non_iter_str(str):
    def __iter__(self):
        yield self

>>> my_str = non_iter_str('stackoverflow')
>>> my_str
'stackoverflow'
>>> my_str[5:]
'overflow'
>>> for s in my_str:
...   print s
... 
stackoverflow

Ответ 5

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

  • строка
  • ИНТ
  • пользовательский класс
  • и др.

Когда вы пишете свою функцию, первое, что вы делаете, это проверка ваших параметров, правильно?

def set_fields(record, fields, value):
    if isinstance(fields, str):
        fields = (fields, )  # tuple-ize it!
    for f in fields:
        record[f] = value

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