Модуль Python re - сохранение состояния?

Одно из самых больших неудобств, которые я нахожу в Python, - это неспособность модуля re сохранять свое состояние без явного выполнения этого в объекте соответствия. Часто нужно анализировать строки, и если они соответствуют определенному регулярному выражению, извлекают из них значения с помощью того же регулярного выражения. Я хотел бы написать такой код:

if re.match('foo (\w+) bar (\d+)', line):
  # do stuff with .group(1) and .group(2)
elif re.match('baz whoo_(\d+)', line):
  # do stuff with .group(1)
# etc.

Но, к сожалению, невозможно добраться до объекта, совпадающего с предыдущим вызовом re.match, поэтому это записывается так:

m = re.match('foo (\w+) bar (\d+)', line)
if m:
  # do stuff with m.group(1) and m.group(2)
else:
  m = re.match('baz whoo_(\d+)', line)
  if m:
    # do stuff with m.group(1)

Что гораздо менее удобно и становится действительно громоздким по мере того, как список elif становится длиннее.

Хакерским решением было бы обернуть re.match и re.search в моих собственных объектах, которые где-то сохраняют состояние. Кто-нибудь этим пользовался? Вы знаете о полустандартных реализациях (в больших фреймворках или что-то в этом роде)?

Какие еще обходные пути вы можете порекомендовать? Или, возможно, я просто неправильно использую модуль и смогу удовлетворить свои потребности более чистым способом?

заранее спасибо

Хотелось бы, чтобы у модуля тоже была форма сохранения состояния. Но я не считаю хакерским создавать собственный объект, сохраняющий состояние. Мне кажется чистым, локализованным и четко определенным решением.

Brian Clapper 16.01.2009 18:35
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
12
1
1 271
7
Перейти к ответу Данный вопрос помечен как решенный

Ответы 7

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

Спасибо за указатель! Мне нравится основная концепция связанного рецепта, но ее можно улучшить, если вы используете более новую версию Python. У меня строго 2,5+, так что сейчас пойду поиграю в хаки.

Peter Rowell 15.01.2009 21:33

Вы можете написать служебный класс для выполнения операции «сохранить состояние и вернуть результат». Я не думаю, что это так хакерски. Реализовать довольно просто:

class Var(object):
    def __init__(self, val=None): self.val = val

    def set(self, result):
        self.val = result
        return result

А затем используйте его как:

lastMatch = Var()

if lastMatch.set(re.match('foo (\w+) bar (\d+)', line)):
    print lastMatch.val.groups()

elif lastMatch.set(re.match('baz whoo_(\d+)', line)):
    print lastMatch.val.groups()

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

Eli Bendersky 16.01.2009 09:34
Ответ принят как подходящий

Пробуем идеи ...

Похоже, вам в идеале нужно выражение с побочными эффектами. Если бы это было разрешено в Python:

if m = re.match('foo (\w+) bar (\d+)', line):
  # do stuff with m.group(1) and m.group(2)
elif m = re.match('baz whoo_(\d+)', line):
  # do stuff with m.group(1)
elif ...

... тогда вы четко и ясно выразите свое намерение. Но это не так. Если бы побочные эффекты были разрешены во вложенных функциях, вы могли бы:

m = None
def assign_m(x):
  m = x
  return x

if assign_m(re.match('foo (\w+) bar (\d+)', line)):
  # do stuff with m.group(1) and m.group(2)
elif assign_m(re.match('baz whoo_(\d+)', line)):
  # do stuff with m.group(1)
elif ...

Теперь это не только некрасиво, но и недействительный код Python - вложенной функции assign_m не разрешено изменять переменную m во внешней области видимости. Лучшее, что я могу придумать, - это уродливый В самом деле, использующий вложенный класс, который допускает побочные эффекты:

# per Brian's suggestion, a wrapper that is stateful
class m_(object):
  def match(self, *args):
    self.inner_ = re.match(*args)
    return self.inner_
  def group(self, *args):
    return self.inner_.group(*args)
m = m_()

# now 'm' is a stateful regex
if m.match('foo (\w+) bar (\d+)', line):
  # do stuff with m.group(1) and m.group(2)
elif m.match('baz whoo_(\d+)', line):
  # do stuff with m.group(1)
elif ...

Но это перебор четко.

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

def find_the_right_match():
  # now 'm' is a stateful regex
  m = re.match('foo (\w+) bar (\d+)', line)
  if m:
    # do stuff with m.group(1) and m.group(2)
    return # <== exit nested function only
  m = re.match('baz whoo_(\d+)', line)
  if m:
    # do stuff with m.group(1)
    return

find_the_right_match()

Это позволяет вам сгладить nesting = (2 * N-1) до nesting = 1, но вы, возможно, только что переместили проблему с побочными эффектами, а вложенные функции, скорее всего, запутают большинство программистов Python.

Наконец, есть способы справиться с этим без побочных эффектов:

def cond_with(*phrases):
  """for each 2-tuple, invokes first item.  the first pair where
  the first item returns logical true, result is passed to second
  function in pair.  Like an if-elif-elif.. chain"""
  for (cond_lambda, then_lambda) in phrases:
    c = cond_lambda()
    if c:
      return then_lambda(c) 
  return None


cond_with( 
  ((lambda: re.match('foo (\w+) bar (\d+)', line)), 
      (lambda m: 
          ... # do stuff with m.group(1) and m.group(2)
          )),
  ((lambda: re.match('baz whoo_(\d+)', line)),
      (lambda m:
          ... # do stuff with m.group(1)
          )),
  ...)

А теперь код едва ли даже выглядит, как у Python, не говоря уже о понятном программистам Python (это Лисп?).

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

LOL и ++ о коде Lispy. Хорошо продумано :-) Но я был бы очень недоволен любым программистом, который пишет такой реальный код ;-)

Eli Bendersky 16.01.2009 09:36

@eliben - хех, спасибо. могло быть и хуже ... по крайней мере, я не пробовал использовать call / cc!

Aaron 29.01.2009 00:29
class last(object):
  def __init__(self, wrapped, initial=None):
    self.last = initial
    self.func = wrapped

  def __call__(self, *args, **kwds):
    self.last = self.func(*args, **kwds)
    return self.last

def test():
  """
  >>> test()
  crude, but effective: (oYo)
  """
  import re
  m = last(re.compile("(oYo)").match)
  if m("abc"):
    print("oops")
  elif m("oYo"): #A
    print("crude, but effective: (%s)" % m.last.group(1)) #B
  else:
    print("mark")

if __name__ == "__main__":
  import doctest
  doctest.testmod()

last тоже подходит в качестве декоратора.

Понимал, что в моих усилиях по самотестированию и работе в 2.5, 2.6 и 3.0 я несколько затенял реальное решение. Важные строки отмечены #A и #B выше, где вы используете один и тот же объект для тестирования (назовите его match или is_somename) и извлекаете его последнее значение. Легко злоупотребить, но также легко настроить и, если не зайти слишком далеко, получить удивительно чистый код.

Основываясь на прекрасных ответах на этот вопрос, я придумал следующий механизм. Это похоже на общий способ решить ограничение Python «без присваивания в условиях». Основное внимание уделяется прозрачности, реализованной посредством тихого делегирования:

class Var(object):
    def __init__(self, val=None):
        self._val = val

    def __getattr__(self, attr):
        return getattr(self._val, attr)

    def __call__(self, arg):
        self._val = arg
        return self._val


if __name__ == "__main__":
    import re

    var = Var()

    line = 'foo kwa bar 12'

    if var(re.match('foo (\w+) bar (\d+)', line)):
        print var.group(1), var.group(2)
    elif var(re.match('baz whoo_(\d+)', line)):
        print var.group(1)

В общем случае это поточно-ориентированное решение, потому что вы можете создавать свои собственные экземпляры Var. Для большей простоты использования, когда многопоточность не является проблемой, можно импортировать и использовать объект Var по умолчанию. Вот модуль, содержащий класс Var:

class Var(object):
    def __init__(self, val=None):
        self._val = val

    def __getattr__(self, attr):
        return getattr(self._val, attr)

    def __call__(self, arg):
        self._val = arg
        return self._val

var = Var()

А вот код пользователя:

from var import Var, var
import re

line = 'foo kwa bar 12'

if var(re.match('foo (\w+) bar (\d+)', line)):
    print var.group(1), var.group(2)
elif var(re.match('baz whoo_(\d+)', line)):
    print var.group(1)

Хотя это не является поточно-ориентированным, для многих простых сценариев это дает полезный ярлык.

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

def get_results(line):
    m = re.match('foo (\w+) bar (\d+)', line)
    if m:
      # do stuff with .group(1) and .group(2)
      return result
    m = re.match('baz whoo_(\d+)', line)
    if m:
      # do stuff with .group(1)
      return other_result
    # etc.

Таким образом вы избежите чрезмерной вложенности.

Python 3.8 предоставил нам изящное решение: := (моржовый оператор).

Он присвоит правое значение левой переменной, а затем вернет значение.

По сути, мы можем наконец исполнить желание @aaron и просто написать:

if m := re.match('foo (\w+) bar (\d+)', line):
  # do stuff with m.group(1) and m.group(2)
elif m := re.match('baz whoo_(\d+)', line):
  # do stuff with m.group(1)
elif ...

Другие вопросы по теме