Стоит ли использовать re.compile в Python?
Вопрос: Есть ли преимущества в использовании re.compile
для регулярных выражений в Python?
Я изучаю использование регулярных выражений в Python и заметил, что можно использовать функцию re.compile()
для компиляции шаблона регулярного выражения, как в следующем примере:
import re
h = re.compile('hello')
h.match('hello world')
Вместо этого можно просто вызывать re.match()
напрямую:
import re
re.match('hello', 'hello world')
В чем разница между этими подходами? Есть ли какие-либо преимущества в использовании re.compile()
вместо простого вызова re.match()
? Буду благодарен за любые пояснения или рекомендации!
5 ответ(ов)
У меня есть довольно много опыта работы с компилированными регулярными выражениями и запуском их тысячи раз по сравнению с компиляцией "на лету", и я не заметил заметной разницы. Очевидно, это анекдотичный аргумент, и он, безусловно, не является хорошим доводом против компиляции, но я обнаружил, что разница незначительна.
EDIT:
После быстрого взгляда на код библиотеки Python 2.5 я вижу, что Python на самом деле компилирует и КЭШИРУЕТ регулярные выражения каждый раз, когда вы их используете (включая вызовы re.match()
), поэтому вы на самом деле только изменяете время компиляции регулярного выражения, и экономить время не следует, разве что время, затраченное на проверку кэша (поиск по ключу во внутреннем dict
).
Из модуля re.py (комментарии мои):
def match(pattern, string, flags=0):
return _compile(pattern, flags).match(string)
def _compile(*key):
# Проверка кэша в начале функции
cachekey = (type(key[0]),) + key
p = _cache.get(cachekey)
if p is not None: return p
# ...
# Происходит фактическая компиляция при отсутствии в кэше
# ...
# Кэширует скомпилированное регулярное выражение
if len(_cache) >= _MAXCACHE:
_cache.clear()
_cache[cachekey] = p
return p
Я все еще часто предварительно компилирую регулярные выражения, но только для того, чтобы привязать их к удобному, переиспользуемому имени, а не для ожидаемой выгоды по производительности.
Основное преимущество использования re.compile
заключается в возможности отделить определение регулярного выражения от его использования.
Даже такое простое выражение, как 0|[1-9][0-9]*
(целое число в десятичной системе без ведущих нулей), может быть достаточно сложным, и вы, вероятно, предпочли бы не вводить его снова, проверять на опечатки и потом снова проверять на ошибки, когда начнете отлаживать. Кроме того, приятнее использовать имя переменной, такое как num
или num_b10
, чем писать 0|[1-9][0-9]*
.
Конечно, можно хранить строки и передавать их в re.match
, но это менее читабельно:
num = "..."
# потом, гораздо позже:
m = re.match(num, input)
В сравнении с компиляцией:
num = re.compile("...")
# потом, гораздо позже:
m = num.match(input)
Хотя разница невелика, последняя строка второго примера звучит более естественно и проще, если вы собираетесь использовать ее повторно.
Для справки:
$ python -m timeit -s "import re" "re.match('hello', 'hello world')"
100000 циклов, лучший из 3: 3.82 микросекунд на цикл
$ python -m timeit -s "import re; h=re.compile('hello')" "h.match('hello world')"
1000000 циклов, лучший из 3: 1.26 микросекунд на цикл
Итак, если вам часто приходится использовать один и тот же регулярное выражение, имеет смысл воспользоваться re.compile
(особенно для более сложных выражений).
Применимы стандартные аргументы против преждевременной оптимизации, но я не думаю, что вы теряете много в ясности и понимании, используя re.compile
, если у вас есть подозрения, что ваши регулярные выражения могут стать узким местом в производительности.
Обновление:
На Python 3.6 (подозреваю, что ранее приведенные замеры были сделаны в Python 2.x) и на оборудовании 2018 года (MacBook Pro) я получаю следующие результаты:
% python -m timeit -s "import re" "re.match('hello', 'hello world')"
1000000 циклов, лучший из 3: 0.661 микросекунд на цикл
% python -m timeit -s "import re; h=re.compile('hello')" "h.match('hello world')"
1000000 циклов, лучший из 3: 0.285 микросекунд на цикл
% python -m timeit -s "import re" "h=re.compile('hello'); h.match('hello world')"
1000000 циклов, лучший из 3: 0.65 микросекунд на цикл
% python --version
Python 3.6.5 :: Anaconda, Inc.
Я также добавил тест (обратите внимание на различия в использовании кавычек между последними двумя запусками), который показывает, что re.match(x, ...)
по сути эквивалентно re.compile(x).match(...)
, т.е. никакого кэширования скомпилированного представления за кулисами, похоже, не происходит.
Вот простой тестовый случай:
~$ for x in 1 10 100 1000 10000 100000 1000000; do python -m timeit -n $x -s 'import re' 're.match("[0-9]{3}-[0-9]{3}-[0-9]{4}", "123-123-1234")'; done
1 loops, best of 3: 3.1 usec per loop
10 loops, best of 3: 2.41 usec per loop
100 loops, best of 3: 2.24 usec per loop
1000 loops, best of 3: 2.21 usec per loop
10000 loops, best of 3: 2.23 usec per loop
100000 loops, best of 3: 2.24 usec per loop
1000000 loops, best of 3: 2.31 usec per loop
С использованием re.compile
:
~$ for x in 1 10 100 1000 10000 100000 1000000; do python -m timeit -n $x -s 'import re' 'r = re.compile("[0-9]{3}-[0-9]{3}-[0-9]{4}")' 'r.match("123-123-1234")'; done
1 loops, best of 3: 1.91 usec per loop
10 loops, best of 3: 0.691 usec per loop
100 loops, best of 3: 0.701 usec per loop
1000 loops, best of 3: 0.684 usec per loop
10000 loops, best of 3: 0.682 usec per loop
100000 loops, best of 3: 0.694 usec per loop
1000000 loops, best of 3: 0.702 usec per loop
Как видно, использование re.compile
оказывается быстрее в этом простом случае, даже если вы выполняете матч всего один раз. Это связано с тем, что регулярные выражения компилируются в объект, что позволяет избежать повторной компиляции для каждого вызова. Поэтому, если вы планируете использовать одно и то же выражение несколько раз, стоит использовать re.compile
для оптимизации производительности.
Я согласен с Honest Abe, что match(...)
в приведённых примерах различаются. Это не одно и то же, и, соответственно, результаты будут разными. Чтобы упростить свой ответ, я использую A, B, C, D для указанных функций. О, да, на самом деле мы имеем дело с 4 функциями в re.py
, а не 3.
Запуск следующего кода:
h = re.compile('hello') # (A)
h.match('hello world') # (B)
равнозначен запуску этого кода:
re.match('hello', 'hello world') # (C)
Поскольку, если посмотреть на исходный код re.py
, (A + B) означает:
h = re._compile('hello') # (D)
h.match('hello world')
А (C) фактически является:
re._compile('hello').match('hello world')
Таким образом, (C) не является тем же, что (B). На самом деле (C) вызывает (B) после вызова (D), который также вызывается из (A). Другими словами, (C) = (A) + (B)
. Поэтому сравнение (A + B) внутри цикла даст тот же результат, что и (C) внутри цикла.
Тестовый скрипт George regexTest.py
это доказал:
noncompiled took 4.555 seconds. # (C) в цикле
compiledInLoop took 4.620 seconds. # (A + B) в цикле
compiled took 2.323 seconds. # (A) один раз + (B) в цикле
Всем интересно, как достигнуть результата в 2.323 секунды. Чтобы убедиться, что compile(...)
вызывается только один раз, нам нужно сохранить скомпилированный объект regex в памяти. Если мы используем класс, мы можем сохранить объект и повторно использовать его всякий раз, когда вызывается наша функция.
class Foo:
regex = re.compile('hello')
def my_function(self, text):
return self.regex.match(text)
Если мы не используем класс (что является моей просьбой сегодня), тогда у меня нет комментариев. Я все еще учусь использовать глобальную переменную в Python, и я осознаю, что глобальные переменные — это не лучший подход.
Еще один момент — я считаю, что подход с использованием (A) + (B)
имеет преимущество. Вот некоторые факты, которые я заметил (пожалуйста, поправьте меня, если я не прав):
- Вызов A один раз приведёт к одному поиску в
_cache
, за которым следует один вызовsre_compile.compile()
для создания объекта regex. При вызове A дважды будет два поиска и один компилирование (поскольку объект regex кэшируется). - Если
_cache
очищается между вызовами, объект regex будет освобождён из памяти, и Python нужно будет снова его компилировать. (Некоторые утверждают, что Python не пересчитывает.) - Если мы сохраняем объект regex, используя (A), он всё равно попадет в _cache и будет каким-то образом очищен. Но наш код сохраняет ссылку на него, и объект regex не будет освобождён из памяти. Поэтому Python не нужно будет компилировать его снова.
- Разница в 2 секунды в тесте George с циклом, использующим скомпилированный regex по сравнению с скомпилированным один раз, в основном обусловлена временем, необходимым для построения ключа и поиска в _cache. Это не значит, что это время компиляции regex.
- Тест George
reallycompile
показывает, что произойдет, если компиляция действительно выполняется каждый раз: это будет в 100 раз медленнее (он уменьшил количество итераций цикла с 1,000,000 до 10,000).
Вот случаи, когда (A + B) лучше, чем (C):
- Если мы можем кэшировать ссылку на объект regex внутри класса.
- Если нам нужно повторно вызывать (B) (внутри цикла или несколько раз), мы должны кэшировать ссылку на объект regex вне цикла.
Случаи, когда (C) достаточно хорош:
- Мы не можем кэшировать ссылку.
- Мы используем его лишь время от времени.
- В целом, у нас не так много regex (предполагается, что скомпилированный объект никогда не будет очищен).
Напоследок, вот A B C:
h = re.compile('hello') # (A)
h.match('hello world') # (B)
re.match('hello', 'hello world') # (C)
Спасибо за внимание.
Фильтрация DataFrame pandas по критериям подстроки
Преобразование списка словарей в DataFrame pandas
Проверка соответствия строки регулярному выражению в JS
Как отсортировать список/кортеж списков/кортежей по элементу на заданном индексе
Как отменить последнюю миграцию?