Подавление обработки строки как итерируемой

ОБНОВЛЕНИЕ:

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

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

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

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

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

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

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

Я отказываюсь от этого проекта; это патч SF 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 является строкой или списком?

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


person max    schedule 06.02.2012    source источник
comment
Думаю, вы в основном ответили на свой вопрос. Два ваших метода - лучшие способы, если вам нужно это сделать, но лучший ответ - просто убедиться, что этого не происходит.   -  person Gareth Latty    schedule 07.02.2012
comment
Я бы просто придерживался проверки isinstance(fields, str) - вам вряд ли когда-нибудь понадобится способность создавать свои собственные типы, которые крякают, как строка. Либо сделайте fields последним аргументом varargs. (Хотя это не поможет, если вы устанете и забудете, что не должны заключать это в круглые скобки.)   -  person millimoose    schedule 07.02.2012
comment
Любая библиотека / язык, в которых строки определены как общие списки символов, будут иметь эту проблему. Это не похоже на Python.   -  person Apalala    schedule 13.02.2012


Ответы (5)


К сожалению, нет никаких способов сделать это автоматически. Предлагаемое вами решение (подкласс 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...
person kindall    schedule 06.02.2012
comment
+1. Это хороший ответ, если вы должны это сделать. Однако я все еще не рекомендую это делать. - person Gareth Latty; 07.02.2012
comment
А как насчет функции, которую я привел в качестве примера? Вы бы сказали, что это неправильный дизайн или этого нельзя избежать? - person max; 07.02.2012
comment
Я как бы колеблюсь взад и вперед. Иногда я хочу сказать, что будьте либеральны в том, что вы принимаете, и старайтесь делать то, что явно хочет пользователь, если это возможно. Однако в вашем конкретном случае, возможно, сначала возьмем значение, а имена, которые вы хотите установить, как *args? Тогда вы всегда будете получать итерацию, а вызывающая сторона просто указывает столько имен, сколько у них есть. Если у них уже есть кортеж, они просто распаковывают его при вызове. - person kindall; 07.02.2012
comment
... и, чтобы сыграть себе роль адвоката дьявола, лучше было бы сначала поставить имена (чтобы соответствовать таким вещам, как getattr() и setattr()). Как я уже сказал, я колеблюсь. Как насчет **kwargs, чтобы вы могли просто указать Name='Dagger', ShortName='Dagger', не слишком громоздко? - person kindall; 07.02.2012
comment
@kindall Это означает, что нужно повторять значение. - person Gareth Latty; 07.02.2012
comment
Да, это было бы некрасиво, если бы было много атрибутов, но если бы это была всего пара атрибутов, это могло бы быть наименьшим злом. Или вы можете использовать некоторую нотацию для получения значений из других аргументов (например, ShortName='@Name'). - person kindall; 07.02.2012
comment
@kindall Я думаю, что к тому времени, когда вы это сделаете, лучшим вариантом будет мой классовый путь или, если в python 3, примеры с расширенной распаковкой кортежей, которые я привел. - person Gareth Latty; 07.02.2012

Чтобы развернуть и дать ответ:

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

  1. Это меняет функциональность, которую люди ожидают от строк.
  2. Это означает дополнительные накладные расходы на всю вашу программу.
  3. В этом нет необходимости.
  4. Проверка типов очень непифонична.

Вы можете это сделать, и методы, которые вы предоставили, вероятно, являются лучшими способами (для записи, я думаю, что подклассификация - лучший вариант Если вам нужно это сделать, см. Метод @ kindall), но это просто не стоит делать, и это не очень питонично. Во-первых, избегайте ошибок. В вашем примере вы можете спросить себя, не больше ли проблема в ясности ваших аргументов, и могут ли именованные аргументы или знак 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-подобным элементом вместо dict (если это еще не сделано) и иметь:

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

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

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

В качестве альтернативы, если вы используете 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")

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

person Gareth Latty    schedule 06.02.2012
comment
Я знаю, что это непифонично, поэтому мне плохо это делать ... Но как мне избежать этих ошибок? Мы говорим о том, что буквально пропускаем пару скобок ... время от времени этого почти невозможно избежать, не так ли? - person max; 07.02.2012
comment
@max Как я уже сказал, я думаю, что проблема в том, как вы структурируете аргументы в своем методе, больше, чем проблема с итерацией строки. - person Gareth Latty; 07.02.2012

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

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

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

if isinstance(var, (str, bytes)):
    raise TypeError("Var should be an iterable, not str or bytes")
person Lennart Regebro    schedule 07.02.2012

Что вы думаете о создании не повторяющейся строки?

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
person juliomalegria    schedule 07.02.2012
comment
Это то, о чем я думал изначально; но @kindall упомянул этот недостаток, среди прочего: вы должны помнить, что использовать его везде, где вы используете строку, в том числе другими пользователями моего кода. - person max; 07.02.2012

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

  • нить
  • int
  • специальный класс
  • и Т. Д.

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

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

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

person Ethan Furman    schedule 07.02.2012
comment
Это очень непифонично. Считаете, что вы хотите использовать список или любой другой итератор, а не кортеж? Python - это язык с утиным типом, поэтому проверять типы - не лучшая идея, он бросает вызов идеалам языка. - person Gareth Latty; 07.02.2012
comment
Не проверяйте, что это кортеж. Убедитесь, что это не строка или байты. - person Lennart Regebro; 07.02.2012
comment
@LennartRegebro: Спасибо - я услышал это по-другому. Ответ обновлен. - person Ethan Furman; 07.02.2012
comment
@Lattyware: Как сказал Леннарт, моя ошибка заключалась в проверке tuple вместо проверки того, что это не str. isinstance имеет свое место, и это одно из них. Ответ обновлен. - person Ethan Furman; 07.02.2012