Vale a pena usar o re.compile do Python?

461

Existe algum benefício no uso de compilação para expressões regulares em Python?

h = re.compile('hello')
h.match('hello world')

vs

re.match('hello', 'hello world')
Esteira
fonte
8
Além disso, o fato de que na re.sub
versão
58
Acabei de encontrar um caso em que o uso re.compiledeu uma melhoria de 10 a 50x. A moral é que, se você tem muitas regexes (mais de MAXCACHE = 100) e as usa várias vezes cada (e separadas por mais de MAXCACHE regexes entre elas, para que cada uma seja liberada do cache: use o mesmo muitas vezes e depois passar para o próximo não conta), então definitivamente ajudaria a compilá-los. Caso contrário, não faz diferença.
precisa
8
Uma pequena coisa a notar é que para cordas que não precisam de regex, o inteste de cadeia CadeiaSecundária é muito mais rápido:>python -m timeit -s "import re" "re.match('hello', 'hello world')" 1000000 loops, best of 3: 1.41 usec per loop >python -m timeit "x = 'hello' in 'hello world'" 10000000 loops, best of 3: 0.0513 usec per loop
Gamrix
@ShreevatsaR Interesting! Você pode postar uma resposta com um exemplo que mostra uma melhoria de 10x-50x? A maioria das respostas dadas aqui mostra uma melhoria de 3x em alguns casos precisos e, em outros casos, quase nenhuma melhoria.
Basj
1
@Basj Done, postou uma resposta . Não me preocupei em descobrir o que estava usando o Python em dezembro de 2013, mas a primeira coisa simples que tentei mostra o mesmo comportamento.
ShreevatsaR

Respostas:

436

Eu tive muita experiência executando um regex compilado milhares de vezes versus compilação on-the-fly e não notei nenhuma diferença perceptível. Obviamente, isso é anedótico, e certamente não é um grande argumento contra a compilação, mas achei a diferença insignificante.

EDIT: Após uma rápida olhada no código real da biblioteca Python 2.5, vejo que o Python compila internamente as regexes AND CACHES sempre que você as usa de qualquer maneira (incluindo chamadas para re.match()), então você está realmente apenas alterando QUANDO a regex é compilada e não deveria ' não economize muito tempo - apenas o tempo necessário para verificar o cache (uma pesquisa de chave em um dicttipo interno ).

Do módulo re.py (os comentários são meus):

def match(pattern, string, flags=0):
    return _compile(pattern, flags).match(string)

def _compile(*key):

    # Does cache check at top of function
    cachekey = (type(key[0]),) + key
    p = _cache.get(cachekey)
    if p is not None: return p

    # ...
    # Does actual compilation on cache miss
    # ...

    # Caches compiled regex
    if len(_cache) >= _MAXCACHE:
        _cache.clear()
    _cache[cachekey] = p
    return p

Ainda costumo pré-compilar expressões regulares, mas apenas para vinculá-las a um nome agradável e reutilizável, sem nenhum ganho de desempenho esperado.

Tríptico
fonte
12
Sua conclusão é inconsistente com sua resposta. Se regexs são compilados e armazenados automaticamente, na maioria dos casos não há necessidade de fazê-lo manualmente.
JFS
84
JF Sebastian, serve como um sinal para o programador de que o regexp em questão será muito usado e não deve ser descartado.
kaleissin
40
Mais do que isso, eu diria que, se você não deseja sofrer o impacto de compilação e cache em alguma parte crítica do desempenho do seu aplicativo, é melhor compilá-los antes da mão em uma parte não crítica do seu aplicativo .
Eddie Parker
20
Vejo a principal vantagem de usar regex compilado se você estiver reutilizando o mesmo regex várias vezes, reduzindo assim a possibilidade de erros de digitação. Se você está apenas ligando uma vez, então descompilado é mais legível.
monkut
18
Portanto, a principal diferença será quando você estiver usando muitas expressões regulares diferentes (mais que _MAXCACHE), algumas delas apenas uma vez e outras várias vezes ... então é importante manter suas expressões compiladas para aquelas que são usadas mais para que elas não é liberado do cache quando estiver cheio.
fortran
133

Para mim, o maior benefício re.compileé poder separar a definição do regex do seu uso.

Mesmo uma expressão simples como 0|[1-9][0-9]*(número inteiro na base 10 sem zeros à esquerda) pode ser complexa o suficiente para que você não precise redigitá-la, verifique se você fez algum erro de digitação e depois verifique novamente se há erros de digitação ao iniciar a depuração . Além disso, é melhor usar um nome de variável como num ou num_b10 que 0|[1-9][0-9]*.

Certamente é possível armazenar strings e passá-las para re.match; no entanto, isso é menos legível:

num = "..."
# then, much later:
m = re.match(num, input)

Contra a compilação:

num = re.compile("...")
# then, much later:
m = num.match(input)

Embora esteja bem perto, a última linha do segundo parece mais natural e mais simples quando usada repetidamente.

Acumenus
fonte
5
Eu concordo com esta resposta; muitas vezes, o uso de re.compile resulta em código mais, não menos legível.
Carl Meyer
1
Às vezes, porém, o oposto é verdadeiro - por exemplo, se você definir a regex em um local e usar seus grupos correspondentes em outro local distante.
Ken Williams
1
@KenWilliams Não necessariamente, um regex bem nomeado para uma finalidade específica deve ser claro, mesmo quando usado longe da definição original. Por exemplo us_phone_numberou social_security_numberetc
Brian M. Sheldon
2
@ BrianM.Sheldon nomear bem o regex realmente não ajuda a saber o que seus vários grupos de captura representam.
Ken Williams
68

FWIW:

$ python -m timeit -s "import re" "re.match('hello', 'hello world')"
100000 loops, best of 3: 3.82 usec per loop

$ python -m timeit -s "import re; h=re.compile('hello')" "h.match('hello world')"
1000000 loops, best of 3: 1.26 usec per loop

portanto, se você usar muito o mesmo regex, pode valer a pena fazer re.compile(especialmente para regexes mais complexos).

Os argumentos padrão contra a otimização prematura se aplicam, mas não acho que você realmente perca muita clareza / franqueza usando re.compilese suspeitar que seus regexps podem se tornar um gargalo de desempenho.

Atualizar:

No Python 3.6 (suspeito que os tempos acima foram feitos usando o Python 2.x) e o hardware de 2018 (MacBook Pro), agora recebo os seguintes tempos:

% python -m timeit -s "import re" "re.match('hello', 'hello world')"
1000000 loops, best of 3: 0.661 usec per loop

% python -m timeit -s "import re; h=re.compile('hello')" "h.match('hello world')"
1000000 loops, best of 3: 0.285 usec per loop

% python -m timeit -s "import re" "h=re.compile('hello'); h.match('hello world')"
1000000 loops, best of 3: 0.65 usec per loop

% python --version
Python 3.6.5 :: Anaconda, Inc.

Também adicionei um caso (observe as diferenças entre aspas entre as duas últimas execuções) que mostra que isso re.match(x, ...)é literalmente [aproximadamente] equivalente a re.compile(x).match(...), ou seja, nenhum cache nos bastidores da representação compilada parece acontecer.

dF.
fonte
5
Principais problemas com sua metodologia aqui, pois o argumento de instalação NÃO está incluído no tempo. Assim, você removeu o tempo de compilação do segundo exemplo e apenas calculou a média no primeiro exemplo. Isso não significa que o primeiro exemplo seja compilado sempre.
Triptych
1
Sim, concordo que essa não é uma comparação justa dos dois casos.
Kiv
7
Entendo o que você quer dizer, mas não é exatamente o que aconteceria em um aplicativo real em que o regexp é usado muitas vezes?
dF.
26
@ Triptych, @ Kiv: O objetivo de compilar regexps separados do uso é minimizar a compilação; removê-lo do tempo é exatamente o que o dF deveria ter feito, porque representa o uso no mundo real com mais precisão. O tempo de compilação é especialmente irrelevante com a maneira como timeit.py faz seus tempos aqui; ele executa várias execuções e relata apenas a mais curta, quando o regexp compilado é armazenado em cache. O custo extra que você está vendo aqui não é o custo de compilar o regexp, mas o custo de procurá-lo no cache do regexp compilado (um dicionário).
precisa saber é o seguinte
3
@ Triptych Deve import reser retirado da configuração? É tudo sobre onde você deseja medir. Se eu executar um script python várias vezes, ele terá o import retempo atingido. Ao comparar os dois, é importante separar as duas linhas para cronometrar. Sim, como você diz que é quando você terá o tempo acertado. A comparação mostra que você pega o tempo atingido uma vez e repete o menor tempo compilando ou toma cada vez que o cache é limpo entre as chamadas, o que, como foi apontado, pode acontecer. Adicionar um horário de h=re.compile('hello')ajuda a esclarecer.
precisa saber é o seguinte
39

Aqui está um caso de teste simples:

~$ 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

com 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

Portanto, parece que a compilação é mais rápida com este caso simples, mesmo que você corresponda apenas uma vez .

david king
fonte
2
Qual versão do Python é essa?
Kyle Strand
2
isso realmente não importa, o ponto é tentar o ponto de referência no ambiente onde você vai estar executando o código
david rei
1
Para mim, o desempenho é quase exatamente o mesmo para 1000 loops ou mais. A versão compilada é mais rápida para 1-100 loops. (Nos pítons 2.7 e 3.4).
Zitrax
2
Na minha configuração do Python 2.7.3, quase não há diferença. Às vezes a compilação é mais rápida, às vezes é mais lenta. A diferença é sempre <5%, por isso considero a diferença como medida de incerteza, já que o dispositivo possui apenas uma CPU.
Dakkaron
1
No Python 3.4.3, visto em duas execuções separadas: o uso de compilado era ainda mais lento que o não compilado.
Zelphir Kaltstahl
17

Eu apenas tentei isso sozinho. Para o simples caso de analisar e somar um número de uma string, o uso de um objeto de expressão regular compilado é duas vezes mais rápido que o uso dos remétodos.

Como outros já apontaram, os remétodos (inclusive re.compile) pesquisam a cadeia de expressão regular em um cache de expressões compiladas anteriormente. Portanto, no caso normal, o custo extra do uso dos remétodos é simplesmente o custo da pesquisa em cache.

No entanto, o exame do código mostra que o cache está limitado a 100 expressões. Isso levanta a questão: quão doloroso é estourar o cache? O código contém uma interface interna para o compilador de expressões regulares re.sre_compile.compile,. Se chamamos, ignoramos o cache. Acontece que são duas ordens de magnitude mais lentas para uma expressão regular básica, como r'\w+\s+([0-9_]+)\s+\w*'.

Aqui está o meu teste:

#!/usr/bin/env python
import re
import time

def timed(func):
    def wrapper(*args):
        t = time.time()
        result = func(*args)
        t = time.time() - t
        print '%s took %.3f seconds.' % (func.func_name, t)
        return result
    return wrapper

regularExpression = r'\w+\s+([0-9_]+)\s+\w*'
testString = "average    2 never"

@timed
def noncompiled():
    a = 0
    for x in xrange(1000000):
        m = re.match(regularExpression, testString)
        a += int(m.group(1))
    return a

@timed
def compiled():
    a = 0
    rgx = re.compile(regularExpression)
    for x in xrange(1000000):
        m = rgx.match(testString)
        a += int(m.group(1))
    return a

@timed
def reallyCompiled():
    a = 0
    rgx = re.sre_compile.compile(regularExpression)
    for x in xrange(1000000):
        m = rgx.match(testString)
        a += int(m.group(1))
    return a


@timed
def compiledInLoop():
    a = 0
    for x in xrange(1000000):
        rgx = re.compile(regularExpression)
        m = rgx.match(testString)
        a += int(m.group(1))
    return a

@timed
def reallyCompiledInLoop():
    a = 0
    for x in xrange(10000):
        rgx = re.sre_compile.compile(regularExpression)
        m = rgx.match(testString)
        a += int(m.group(1))
    return a

r1 = noncompiled()
r2 = compiled()
r3 = reallyCompiled()
r4 = compiledInLoop()
r5 = reallyCompiledInLoop()
print "r1 = ", r1
print "r2 = ", r2
print "r3 = ", r3
print "r4 = ", r4
print "r5 = ", r5
</pre>
And here is the output on my machine:
<pre>
$ regexTest.py 
noncompiled took 4.555 seconds.
compiled took 2.323 seconds.
reallyCompiled took 2.325 seconds.
compiledInLoop took 4.620 seconds.
reallyCompiledInLoop took 4.074 seconds.
r1 =  2000000
r2 =  2000000
r3 =  2000000
r4 =  2000000
r5 =  20000

Os métodos 'reallyCompiled' usam a interface interna, que ignora o cache. Observe que aquele que compila em cada iteração de loop é iterado apenas 10.000 vezes, não um milhão.

George
fonte
Concordo com você que as regexes compiladas são executadas muito mais rapidamente do que as não compiladas. Executei mais de 10.000 frases e fiz um loop para iterar regexes quando as regexes não foram compiladas e foram calculadas cada vez que a previsão de uma execução completa era de 8 horas, depois de criar um dicionário de acordo com o índice com padrões de regex compilados que corro a coisa toda por 2 minutos. Eu não consigo entender as respostas acima ...
Eli Borodach
12

Concordo com Honest Abe que os match(...)exemplos apresentados são diferentes. Eles não são comparações individuais e, portanto, os resultados são variados. Para simplificar minha resposta, uso A, B, C, D para as funções em questão. Ah, sim, estamos lidando com 4 funções em re.pyvez de 3.

Executando este pedaço de código:

h = re.compile('hello')                   # (A)
h.match('hello world')                    # (B)

é o mesmo que executar este código:

re.match('hello', 'hello world')          # (C)

Porque, quando analisado na fonte re.py, (A + B) significa:

h = re._compile('hello')                  # (D)
h.match('hello world')

e (C) é realmente:

re._compile('hello').match('hello world')

Então, (C) não é o mesmo que (B). De fato, (C) chama (B) após chamar (D), que também é chamado por (A). Em outras palavras (C) = (A) + (B),. Portanto, comparar (A + B) dentro de um loop tem o mesmo resultado que (C) dentro de um loop.

George regexTest.pyprovou isso para nós.

noncompiled took 4.555 seconds.           # (C) in a loop
compiledInLoop took 4.620 seconds.        # (A + B) in a loop
compiled took 2.323 seconds.              # (A) once + (B) in a loop

O interesse de todos é, como obter o resultado de 2.323 segundos. Para garantir que compile(...)apenas seja chamado uma vez, precisamos armazenar o objeto regex compilado na memória. Se estivermos usando uma classe, poderíamos armazenar o objeto e reutilizá-lo sempre que nossa função for chamada.

class Foo:
    regex = re.compile('hello')
    def my_function(text)
        return regex.match(text)

Se não estamos usando a classe (que é minha solicitação hoje), não tenho comentários. Ainda estou aprendendo a usar variáveis ​​globais em Python e sei que variáveis ​​globais são uma coisa ruim.

Mais um ponto, acredito que o uso da (A) + (B)abordagem tem uma vantagem. Aqui estão alguns fatos, como observei (corrija-me se estiver errado):

  1. Chama uma vez uma vez, ele fará uma pesquisa na _cacheseguida por uma sre_compile.compile()para criar um objeto regex. Chamadas A duas vezes, ele faz duas pesquisas e uma compilação (porque o objeto regex está armazenado em cache).

  2. Se _cachefor liberado no meio, o objeto regex será liberado da memória e o Python precisará compilar novamente. (alguém sugere que o Python não recompilará.)

  3. Se mantivermos o objeto regex usando (A), o objeto regex ainda entrará no _cache e será liberado de alguma forma. Mas nosso código mantém uma referência e o objeto regex não será liberado da memória. Aqueles, o Python não precisa compilar novamente.

  4. As diferenças de 2 segundos no teste de George compiladoInLoop vs compilado é principalmente o tempo necessário para criar a chave e pesquisar o _cache. Isso não significa o tempo de compilação do regex.

  5. O teste realmente compilado de George mostra o que acontece se ele realmente refazer a compilação todas as vezes: será 100x mais lento (ele reduziu o loop de 1.000.000 para 10.000).

Aqui estão os únicos casos em que (A + B) é melhor que (C):

  1. Se pudermos armazenar em cache uma referência do objeto regex dentro de uma classe.
  2. Se precisarmos chamar (B) repetidamente (dentro de um loop ou várias vezes), devemos armazenar em cache a referência ao objeto regex fora do loop.

Caso (C) seja bom o suficiente:

  1. Não podemos armazenar em cache uma referência.
  2. Só o usamos de vez em quando.
  3. No geral, não temos muitos regex (suponha que o compilado nunca seja liberado)

Apenas uma recapitulação, aqui está o ABC:

h = re.compile('hello')                   # (A)
h.match('hello world')                    # (B)
re.match('hello', 'hello world')          # (C)

Obrigado pela leitura.

John Pang
fonte
8

Principalmente, há pouca diferença se você usa re.compile ou não. Internamente, todas as funções são implementadas em termos de uma etapa de compilação:

def match(pattern, string, flags=0):
    return _compile(pattern, flags).match(string)

def fullmatch(pattern, string, flags=0):
    return _compile(pattern, flags).fullmatch(string)

def search(pattern, string, flags=0):
    return _compile(pattern, flags).search(string)

def sub(pattern, repl, string, count=0, flags=0):
    return _compile(pattern, flags).sub(repl, string, count)

def subn(pattern, repl, string, count=0, flags=0):
    return _compile(pattern, flags).subn(repl, string, count)

def split(pattern, string, maxsplit=0, flags=0):
    return _compile(pattern, flags).split(string, maxsplit)

def findall(pattern, string, flags=0):
    return _compile(pattern, flags).findall(string)

def finditer(pattern, string, flags=0):
    return _compile(pattern, flags).finditer(string)

Além disso, re.compile () ignora a lógica extra de indireção e cache:

_cache = {}

_pattern_type = type(sre_compile.compile("", 0))

_MAXCACHE = 512
def _compile(pattern, flags):
    # internal: compile pattern
    try:
        p, loc = _cache[type(pattern), pattern, flags]
        if loc is None or loc == _locale.setlocale(_locale.LC_CTYPE):
            return p
    except KeyError:
        pass
    if isinstance(pattern, _pattern_type):
        if flags:
            raise ValueError(
                "cannot process flags argument with a compiled pattern")
        return pattern
    if not sre_compile.isstring(pattern):
        raise TypeError("first argument must be string or compiled pattern")
    p = sre_compile.compile(pattern, flags)
    if not (flags & DEBUG):
        if len(_cache) >= _MAXCACHE:
            _cache.clear()
        if p.flags & LOCALE:
            if not _locale:
                return p
            loc = _locale.setlocale(_locale.LC_CTYPE)
        else:
            loc = None
        _cache[type(pattern), pattern, flags] = p, loc
    return p

Além da pequena velocidade com o uso do re.compile , as pessoas também gostam da legibilidade resultante da nomeação de especificações de padrões potencialmente complexas e da separação da lógica de negócios em que são aplicadas:

#### Patterns ############################################################
number_pattern = re.compile(r'\d+(\.\d*)?')    # Integer or decimal number
assign_pattern = re.compile(r':=')             # Assignment operator
identifier_pattern = re.compile(r'[A-Za-z]+')  # Identifiers
whitespace_pattern = re.compile(r'[\t ]+')     # Spaces and tabs

#### Applications ########################################################

if whitespace_pattern.match(s): business_logic_rule_1()
if assign_pattern.match(s): business_logic_rule_2()

Observe que outro entrevistado acreditava incorretamente que os arquivos pyc armazenavam diretamente os padrões compilados; no entanto, na realidade, eles são reconstruídos sempre que o PYC é carregado:

>>> from dis import dis
>>> with open('tmp.pyc', 'rb') as f:
        f.read(8)
        dis(marshal.load(f))

  1           0 LOAD_CONST               0 (-1)
              3 LOAD_CONST               1 (None)
              6 IMPORT_NAME              0 (re)
              9 STORE_NAME               0 (re)

  3          12 LOAD_NAME                0 (re)
             15 LOAD_ATTR                1 (compile)
             18 LOAD_CONST               2 ('[aeiou]{2,5}')
             21 CALL_FUNCTION            1
             24 STORE_NAME               2 (lc_vowels)
             27 LOAD_CONST               1 (None)
             30 RETURN_VALUE

A desmontagem acima é proveniente do arquivo PYC para uma tmp.pycontendo:

import re
lc_vowels = re.compile(r'[aeiou]{2,5}')
Raymond Hettinger
fonte
1
é a "de def search(pattern, string, flags=0):"um erro de digitação?
Phuclv
1
Observe que, se patternjá é um padrão compilado, a sobrecarga do armazenamento em cache se torna significativa: o hash a SRE_Patterné caro e o padrão nunca é gravado no cache; portanto, a pesquisa falha sempre com a KeyError.
precisa
5

Em geral, acho mais fácil usar sinalizadores (pelo menos mais fácil lembrar como), como re.Iao compilar padrões do que usar sinalizadores inline.

>>> foo_pat = re.compile('foo',re.I)
>>> foo_pat.findall('some string FoO bar')
['FoO']

vs

>>> re.findall('(?i)foo','some string FoO bar')
['FoO']
ptone
fonte
Você também pode usar sinalizadores como o terceiro argumento do re.findallmesmo.
aderchox 29/01
5

Usando os exemplos dados:

h = re.compile('hello')
h.match('hello world')

O método de correspondência no exemplo acima não é o mesmo usado abaixo:

re.match('hello', 'hello world')

re.compile () retorna um objeto de expressão regular , o que significa que hé um objeto de expressão regular .

O objeto regex possui seu próprio método de correspondência com os parâmetros op pos e endpos opcionais :

regex.match(string[, pos[, endpos]])

pos

O segundo parâmetro opcional pos fornece um índice na string em que a pesquisa deve começar; o padrão é 0. Isso não é completamente equivalente a cortar a string; o '^'caractere padrão corresponde ao início real da sequência e nas posições logo após uma nova linha, mas não necessariamente no índice em que a pesquisa deve começar.

endpos

O parâmetro opcional endpos limita a distância que a string será pesquisada; será como se a sequência tivesse caracteres finais , portanto, apenas os caracteres de pos a endpos - 1serão pesquisados ​​por uma correspondência. Se os endpos forem menores que pos , nenhuma correspondência será encontrada; caso contrário, se rx for um objeto de expressão regular compilado, rx.search(string, 0, 50)será equivalente a rx.search(string[:50], 0).

Os métodos de pesquisa , findall e finditer do objeto regex também suportam esses parâmetros.

re.match(pattern, string, flags=0)não apoiá-los como você pode ver,
nem seus pesquisa , findall e finditer homólogos.

Um objeto de correspondência possui atributos que complementam estes parâmetros:

match.pos

O valor de pos que foi passado para o método search () ou match () de um objeto regex. Este é o índice da cadeia na qual o mecanismo do RE começou a procurar uma correspondência.

match.endpos

O valor dos endpos que foram passados ​​para o método search () ou match () de um objeto regex. Este é o índice da cadeia além da qual o mecanismo do RE não irá.


Um objeto regex possui dois atributos exclusivos, possivelmente úteis:

regex.groups

O número de grupos de captura no padrão.

regex.groupindex

Um dicionário que mapeia qualquer nome de grupo simbólico definido por (? P) para agrupar números. O dicionário está vazio se nenhum grupo simbólico foi usado no padrão.


E, finalmente, um objeto de correspondência possui este atributo:

match.re

O objeto de expressão regular cujo método match () ou search () produziu essa instância de correspondência.

Honesto Abe
fonte
4

Diferença de desempenho à parte, o uso de re.compile e o objeto de expressão regular compilado para fazer a correspondência (quaisquer operações relacionadas à expressão regular) tornam a semântica mais clara para o tempo de execução do Python.

Eu tive uma experiência dolorosa de depurar algum código simples:

compare = lambda s, p: re.match(p, s)

e depois eu usaria comparar em

[x for x in data if compare(patternPhrases, x[columnIndex])]

onde patternPhrasesé suposto ser uma variável que contém string de expressão regular, x[columnIndex]é uma variável que contém string.

Tive um problema que patternPhrasesnão correspondia a uma sequência esperada!

Mas se eu usasse o formulário re.compile:

compare = lambda s, p: p.match(s)

então em

[x for x in data if compare(patternPhrases, x[columnIndex])]

Python teria reclamou que "string não tem atributo do jogo", como por mapeamento argumento posicional no compare, x[columnIndex]é usada como expressão regular !, quando eu realmente quis dizer

compare = lambda p, s: p.match(s)

No meu caso, o uso de re.compile é mais explícito do objetivo da expressão regular, quando seu valor está oculto a olho nu, portanto, eu poderia obter mais ajuda na verificação em tempo de execução do Python.

Portanto, a moral da minha lição é que, quando a expressão regular não é apenas uma string literal, devo usar re.compile para permitir que o Python me ajude a afirmar minha suposição.

Yu Shen
fonte
4

Há uma vantagem adicional de usar re.compile (), na forma de adicionar comentários aos meus padrões de regex usando re.VERBOSE

pattern = '''
hello[ ]world    # Some info on my pattern logic. [ ] to recognize space
'''

re.search(pattern, 'hello world', re.VERBOSE)

Embora isso não afete a velocidade de execução do seu código, eu gosto de fazê-lo dessa maneira, pois faz parte do meu hábito de comentar. Eu não gosto de gastar tempo tentando lembrar a lógica que ficou atrás do meu código dois meses depois, quando eu quero fazer modificações.

cyneo
fonte
1
Eu editei sua resposta. Acho que mencionar re.VERBOSEvale a pena, e acrescenta algo que as outras respostas parecem ter deixado de fora. No entanto, conduzir sua resposta com "Estou postando aqui porque ainda não posso comentar" certamente será excluído. Por favor, não use a caixa de respostas para nada além de respostas. Você tem apenas uma ou duas boas respostas para poder comentar em qualquer lugar (50 repetições); portanto, seja paciente. Colocar comentários nas caixas de respostas quando você sabe que não deve chegar lá mais rapidamente. Você receberá votos negativos e respostas excluídas.
skrrgwasme
4

De acordo com a documentação do Python :

A sequência

prog = re.compile(pattern)
result = prog.match(string)

é equivalente a

result = re.match(pattern, string)

mas usar re.compile()e salvar o objeto de expressão regular resultante para reutilização é mais eficiente quando a expressão será usada várias vezes em um único programa.

Portanto, minha conclusão é que, se você corresponder ao mesmo padrão para muitos textos diferentes, é melhor pré-compilá-lo.

Chris Wu
fonte
3

Curiosamente, a compilação se mostra mais eficiente para mim (Python 2.5.2 no Win XP):

import re
import time

rgx = re.compile('(\w+)\s+[0-9_]?\s+\w*')
str = "average    2 never"
a = 0

t = time.time()

for i in xrange(1000000):
    if re.match('(\w+)\s+[0-9_]?\s+\w*', str):
    #~ if rgx.match(str):
        a += 1

print time.time() - t

Executando o código acima uma vez como está, e uma vez com as duas iflinhas comentadas ao contrário, o regex compilado é duas vezes mais rápido

Eli Bendersky
fonte
2
O mesmo problema da comparação de desempenho do dF. Não é realmente justo, a menos que você inclua o custo de desempenho da própria declaração de compilação.
Carl Meyer
6
Carl, eu discordo. A compilação é executado somente uma vez, enquanto o loop correspondente é executado um milhão de vezes
Eli Bendersky
@eliben: Eu concordo com Carl Meyer. A compilação ocorre nos dois casos. Triptych menciona que o cache está envolvido; portanto, em um caso ideal (permanece no cache), as duas abordagens são O (n + 1), embora a parte +1 esteja meio oculta quando você não usa o re.compile explicitamente.
paprika
1
Não escreva seu próprio código de benchmarking. Aprenda a usar o timeit.py, que está incluído na distribuição padrão.
jemfinch
Quanto tempo você está recriando a sequência de padrões no loop for. Essa sobrecarga não pode ser trivial.
precisa saber é o seguinte
3

Fiz esse teste antes de tropeçar na discussão aqui. No entanto, ao executá-lo, pensei em publicar pelo menos meus resultados.

Eu roubei e bastardo do exemplo em "Mastering Regular Expressions" de Jeff Friedl. Este é um macbook executando o OSX 10.6 (2Ghz intel core 2 duo, 4GB de RAM). A versão do Python é 2.6.1.

Execução 1 - usando re.compile

import re 
import time 
import fpformat
Regex1 = re.compile('^(a|b|c|d|e|f|g)+$') 
Regex2 = re.compile('^[a-g]+$')
TimesToDo = 1000
TestString = "" 
for i in range(1000):
    TestString += "abababdedfg"
StartTime = time.time() 
for i in range(TimesToDo):
    Regex1.search(TestString) 
Seconds = time.time() - StartTime 
print "Alternation takes " + fpformat.fix(Seconds,3) + " seconds"

StartTime = time.time() 
for i in range(TimesToDo):
    Regex2.search(TestString) 
Seconds = time.time() - StartTime 
print "Character Class takes " + fpformat.fix(Seconds,3) + " seconds"

Alternation takes 2.299 seconds
Character Class takes 0.107 seconds

Execução 2 - Não usando re.compile

import re 
import time 
import fpformat

TimesToDo = 1000
TestString = "" 
for i in range(1000):
    TestString += "abababdedfg"
StartTime = time.time() 
for i in range(TimesToDo):
    re.search('^(a|b|c|d|e|f|g)+$',TestString) 
Seconds = time.time() - StartTime 
print "Alternation takes " + fpformat.fix(Seconds,3) + " seconds"

StartTime = time.time() 
for i in range(TimesToDo):
    re.search('^[a-g]+$',TestString) 
Seconds = time.time() - StartTime 
print "Character Class takes " + fpformat.fix(Seconds,3) + " seconds"

Alternation takes 2.508 seconds
Character Class takes 0.109 seconds
netricar
fonte
3

Esta resposta pode estar chegando tarde, mas é uma descoberta interessante. O uso da compilação pode economizar muito tempo se você planeja usar o regex várias vezes (isso também é mencionado nos documentos). Abaixo, você pode ver que o uso de uma regex compilada é o mais rápido quando o método de correspondência é chamado diretamente nela. a passagem de um regex compilado para re.match torna-o ainda mais lento e a passagem de re.match com a string patter está em algum lugar no meio.

>>> ipr = r'\D+((([0-2][0-5]?[0-5]?)\.){3}([0-2][0-5]?[0-5]?))\D+'
>>> average(*timeit.repeat("re.match(ipr, 'abcd100.10.255.255 ')", globals={'ipr': ipr, 're': re}))
1.5077415757028423
>>> ipr = re.compile(ipr)
>>> average(*timeit.repeat("re.match(ipr, 'abcd100.10.255.255 ')", globals={'ipr': ipr, 're': re}))
1.8324008992184038
>>> average(*timeit.repeat("ipr.match('abcd100.10.255.255 ')", globals={'ipr': ipr, 're': re}))
0.9187896518778871
Akilesh
fonte
3

Além da performance.

Usar compileajuda-me a distinguir os conceitos de
1. module (re) ,
2. regex object
3. match object
Quando comecei a aprender regex

#regex object
regex_object = re.compile(r'[a-zA-Z]+')
#match object
match_object = regex_object.search('1.Hello')
#matching content
match_object.group()
output:
Out[60]: 'Hello'
V.S.
re.search(r'[a-zA-Z]+','1.Hello').group()
Out[61]: 'Hello'

Como complemento, fiz uma extensa lista de dicas do módulo repara sua referência.

regex = {
'brackets':{'single_character': ['[]', '.', {'negate':'^'}],
            'capturing_group' : ['()','(?:)', '(?!)' '|', '\\', 'backreferences and named group'],
            'repetition'      : ['{}', '*?', '+?', '??', 'greedy v.s. lazy ?']},
'lookaround' :{'lookahead'  : ['(?=...)', '(?!...)'],
            'lookbehind' : ['(?<=...)','(?<!...)'],
            'caputuring' : ['(?P<name>...)', '(?P=name)', '(?:)'],},
'escapes':{'anchor'          : ['^', '\b', '$'],
          'non_printable'   : ['\n', '\t', '\r', '\f', '\v'],
          'shorthand'       : ['\d', '\w', '\s']},
'methods': {['search', 'match', 'findall', 'finditer'],
              ['split', 'sub']},
'match_object': ['group','groups', 'groupdict','start', 'end', 'span',]
}
Cálculo
fonte
2

Eu realmente respeito todas as respostas acima. Da minha opinião sim! Com certeza, vale a pena usar o re.compile em vez de compilar o regex várias vezes.

O uso do re.compile torna seu código mais dinâmico, como você pode chamar o regex já compilado, em vez de compilar novamente e novamente. Essa coisa beneficia você nos casos:

  1. Esforços do processador
  2. Complexidade de tempo.
  3. Torna regex Universal. (Pode ser usado em busca, busca, correspondência)
  4. E faz seu programa parecer legal.

Exemplo:

  example_string = "The room number of her room is 26A7B."
  find_alpha_numeric_string = re.compile(r"\b\w+\b")

Usando no Findall

 find_alpha_numeric_string.findall(example_string)

Usando na pesquisa

  find_alpha_numeric_string.search(example_string)

Da mesma forma, você pode usá-lo para: Corresponder e Substituir

O Gr8 Adakron
fonte
1

Essa é uma boa pergunta. Você costuma ver as pessoas usarem o re.compile sem motivo. Diminui a legibilidade. Mas com certeza há muitas vezes em que é necessário pré-compilar a expressão. Como quando você usa repetidas vezes em um loop ou algo parecido.

É como tudo sobre programação (tudo na vida, na verdade). Aplique bom senso.

PEZ
fonte
Tanto quanto eu posso dizer pelo meu breve movimento, Python in a Nutshell não menciona o uso sem re.compile (), o que me deixou curioso.
Mat
O objeto regex adiciona mais um objeto ao contexto. Como eu disse, existem muitas situações em que re.compile () tem seu lugar. O exemplo dado pelo OP não é um deles.
PEZ
1

(meses depois), é fácil adicionar seu próprio cache em torno de re.match ou qualquer outra coisa,

""" Re.py: Re.match = re.match + cache  
    efficiency: re.py does this already (but what's _MAXCACHE ?)
    readability, inline / separate: matter of taste
"""

import re

cache = {}
_re_type = type( re.compile( "" ))

def match( pattern, str, *opt ):
    """ Re.match = re.match + cache re.compile( pattern ) 
    """
    if type(pattern) == _re_type:
        cpat = pattern
    elif pattern in cache:
        cpat = cache[pattern]
    else:
        cpat = cache[pattern] = re.compile( pattern, *opt )
    return cpat.match( str )

# def search ...

Um wibni, não seria bom se: cachehint (size =), cacheinfo () -> size, hits, nclear ...

denis
fonte
1

Eu tive muita experiência executando um regex compilado milhares de vezes versus compilação on-the-fly e não percebi nenhuma diferença perceptível

Os votos na resposta aceita levam à suposição de que o que o @Triptych diz é verdadeiro para todos os casos. Isto não é necessariamente verdade. Uma grande diferença é quando você precisa decidir se aceita uma sequência de caracteres regex ou um objeto regex compilado como parâmetro para uma função:

>>> timeit.timeit(setup="""
... import re
... f=lambda x, y: x.match(y)       # accepts compiled regex as parameter
... h=re.compile('hello')
... """, stmt="f(h, 'hello world')")
0.32881879806518555
>>> timeit.timeit(setup="""
... import re
... f=lambda x, y: re.compile(x).match(y)   # compiles when called
... """, stmt="f('hello', 'hello world')")
0.809190034866333

É sempre melhor compilar seus regexs caso você precise reutilizá-los.

Observe o exemplo no timeit acima simula a criação de um objeto regex compilado uma vez no momento da importação versus "on-the-fly" quando necessário para uma correspondência.

lonetwin
fonte
1

Como resposta alternativa, como vejo que não foi mencionado antes, vou adiante e cito os documentos do Python 3 :

Você deve usar essas funções no nível do módulo ou deve obter o padrão e chamar seus métodos você mesmo? Se você estiver acessando um regex dentro de um loop, a pré-compilação salvará algumas chamadas de função. Fora dos loops, não há muita diferença graças ao cache interno.

Michael Kiros
fonte
1

Aqui está um exemplo em que o uso re.compileé 50 vezes mais rápido, conforme solicitado .

O argumento é exatamente o que fiz no comentário acima, ou seja, o uso re.compilepode ser uma vantagem significativa quando seu uso é para não se beneficiar muito do cache de compilação. Isso acontece pelo menos em um caso específico (que eu encontrei na prática), a saber, quando tudo o que se segue é verdadeiro:

  • Você tem muitos padrões de expressão regular (mais de re._MAXCACHE, cujo padrão é atualmente 512) e
  • você usa essas expressões regulares várias vezes e
  • os usos consecutivos do mesmo padrão são separados por mais do que re._MAXCACHEoutras expressões regulares no meio, para que cada um seja liberado do cache entre usos consecutivos.
import re
import time

def setup(N=1000):
    # Patterns 'a.*a', 'a.*b', ..., 'z.*z'
    patterns = [chr(i) + '.*' + chr(j)
                    for i in range(ord('a'), ord('z') + 1)
                    for j in range(ord('a'), ord('z') + 1)]
    # If this assertion below fails, just add more (distinct) patterns.
    # assert(re._MAXCACHE < len(patterns))
    # N strings. Increase N for larger effect.
    strings = ['abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'] * N
    return (patterns, strings)

def without_compile():
    print('Without re.compile:')
    patterns, strings = setup()
    print('searching')
    count = 0
    for s in strings:
        for pat in patterns:
            count += bool(re.search(pat, s))
    return count

def without_compile_cache_friendly():
    print('Without re.compile, cache-friendly order:')
    patterns, strings = setup()
    print('searching')
    count = 0
    for pat in patterns:
        for s in strings:
            count += bool(re.search(pat, s))
    return count

def with_compile():
    print('With re.compile:')
    patterns, strings = setup()
    print('compiling')
    compiled = [re.compile(pattern) for pattern in patterns]
    print('searching')
    count = 0
    for s in strings:
        for regex in compiled:
            count += bool(regex.search(s))
    return count

start = time.time()
print(with_compile())
d1 = time.time() - start
print(f'-- That took {d1:.2f} seconds.\n')

start = time.time()
print(without_compile_cache_friendly())
d2 = time.time() - start
print(f'-- That took {d2:.2f} seconds.\n')

start = time.time()
print(without_compile())
d3 = time.time() - start
print(f'-- That took {d3:.2f} seconds.\n')

print(f'Ratio: {d3/d1:.2f}')

Exemplo de saída que recebo no meu laptop (Python 3.7.7):

With re.compile:
compiling
searching
676000
-- That took 0.33 seconds.

Without re.compile, cache-friendly order:
searching
676000
-- That took 0.67 seconds.

Without re.compile:
searching
676000
-- That took 23.54 seconds.

Ratio: 70.89

Eu não me incomodei timeitporque a diferença é tão acentuada, mas recebo números qualitativamente similares a cada vez. Observe que mesmo sem re.compileusar a mesma regex várias vezes e passar para a próxima não era tão ruim (apenas duas vezes mais lento que com re.compile), mas na outra ordem (repetindo muitas regexes), é significativamente pior , como esperado. Além disso, aumentando o tamanho do cache funciona também: simplesmente definindo re._MAXCACHE = len(patterns)na setup()acima (é claro que eu não recomendo fazer essas coisas na produção como nomes com sublinhados são convencionalmente “privado”) deixa cair a ~ 23 segundos de volta para ~ 0,7 segundos, o que também corresponde ao nosso entendimento.

ShreevatsaR
fonte
PS: se eu usar apenas três padrões de regex em todo o meu código, cada um deles usado (sem nenhuma ordem específica) centenas de vezes, o cache de regex manterá o regex pré-compilado automaticamente, certo?
Basj
@ Basj Eu acho que você poderia tentar e ver :) Mas a resposta, tenho certeza, é sim: o único custo adicional nesse caso, o AFAICT, é apenas o de procurar o padrão no cache . Observe também que o cache é global (no nível do módulo); portanto, em princípio, você pode ter alguma biblioteca de dependência fazendo pesquisas de regex entre as suas, por isso é difícil ter certeza absoluta de que seu programa só usa 3 (ou qualquer número de) regex padrões, mas seria muito estranho ser diferente :)
ShreevatsaR
0

Expressões regulares são compiladas antes de serem usadas ao usar a segunda versão. Se você for executá-lo várias vezes, é definitivamente melhor compilá-lo primeiro. Se não estiver compilando toda vez que você corresponder a uma partida, tudo bem.

Adam Peck
fonte
0

Legibilidade / preferência de carga cognitiva

Para mim, o principal ganho é que eu só preciso lembrar e ler uma forma da sintaxe complicada da API do regex - a <compiled_pattern>.method(xxx)forma em vez disso e a re.func(<pattern>, xxx)forma.

O re.compile(<pattern>)é um pouco de clichê extra, verdadeiro.

Mas no que diz respeito à regex, é improvável que essa etapa extra de compilação seja uma grande causa de carga cognitiva. De fato, em padrões complicados, você pode até obter clareza ao separar a declaração de qualquer método de expressão regular que você invocar nela.

Costumo ajustar primeiro padrões complicados em um site como o Regex101, ou mesmo em um script de teste mínimo separado, e depois trazê-los para o meu código; portanto, separar a declaração do uso também se ajusta ao meu fluxo de trabalho.

JL Peyret
fonte
-1

eu gostaria de motivar que a pré-compilação seja conceitualmente e 'literariamente' (como em 'programação alfabetizada') vantajosa. dê uma olhada neste snippet de código:

from re import compile as _Re

class TYPO:

  def text_has_foobar( self, text ):
    return self._text_has_foobar_re_search( text ) is not None
  _text_has_foobar_re_search = _Re( r"""(?i)foobar""" ).search

TYPO = TYPO()

no seu aplicativo, você escreveria:

from TYPO import TYPO
print( TYPO.text_has_foobar( 'FOObar ) )

isso é o mais simples possível em termos de funcionalidade. porque este é um exemplo tão curto, juntei o caminho para obter _text_has_foobar_re_searchtudo em uma linha. a desvantagem desse código é que ele ocupa um pouco de memória por qualquer que seja o tempo de vida do TYPOobjeto da biblioteca; a vantagem é que, ao fazer uma pesquisa no foobar, você terá duas chamadas de função e duas pesquisas no dicionário de classe. quantas regexes são armazenadas em cache ree a sobrecarga desse cache é irrelevante aqui.

compare isso com o estilo mais usual, abaixo:

import re

class Typo:

  def text_has_foobar( self, text ):
    return re.compile( r"""(?i)foobar""" ).search( text ) is not None

Na aplicação:

typo = Typo()
print( typo.text_has_foobar( 'FOObar ) )

Eu admito prontamente que meu estilo é altamente incomum para python, talvez até discutível. no entanto, no exemplo que corresponde mais à maneira como o python é usado principalmente, para fazer uma única correspondência, precisamos instanciar um objeto, fazer três pesquisas de dicionário de instância e executar três chamadas de função; Além disso, podemos entrar em reproblemas de armazenamento em cache ao usar mais de 100 regexes. Além disso, a expressão regular fica oculta no corpo do método, o que na maioria das vezes não é uma boa ideia.

seja dito que todo subconjunto de medidas - declarações de importação direcionadas e com alias; métodos alternativos, quando aplicável; redução de chamadas de função e pesquisas no dicionário de objetos --- pode ajudar a reduzir a complexidade computacional e conceitual.

fluxo
fonte
2
WTF. Você não apenas faz uma pergunta antiga e respondida. Seu código também não é idiomático e está errado em muitos níveis - (ab) usando classes como espaços de nomes nos quais um módulo é suficiente, colocar nomes em maiúsculas em maiúsculas, etc ... Consulte pastebin.com/iTAXAWen para obter melhores implementações. Sem mencionar que o regex que você usa também está quebrado. No geral, -1
2
culpado. Esta é uma pergunta antiga, mas não me importo de ser o número 100 em uma conversa mais lenta. a questão não foi encerrada. Eu avisei que meu código poderia ser adverso a alguns gostos. Eu acho que se você pudesse vê-lo como uma mera demonstração do que é possível em python, como: se pegarmos tudo, tudo o que acreditamos, como opcional e, em seguida, mexermos de qualquer maneira, como as coisas se parecem com o que podemos pegue? Tenho certeza de que você pode discernir méritos e desmerecimentos dessa solução e pode reclamar de maneira mais articulada. caso contrário, devo concluir o seu pedido de incorreção conta com pouco mais de PEP008
fluxo
2
Não, não é sobre PEP8. Isso é apenas convenções de nomenclatura, e eu nunca desisti de votar por não segui-las. Eu diminuí o voto porque o código que você mostrou é simplesmente mal escrito. Ele desafia convenções e idiomas sem motivo, e é uma encarnação da otimização permanente: você teria que otimizar a luz do dia de todos os outros códigos para que isso se tornasse um gargalo e, mesmo assim, a terceira reescrita que eu ofereci é mais curta e mais idiomática e com a mesma rapidez com o seu raciocínio (mesmo número de acesso a atributos).
"mal escrito" - como por que exatamente? "desafia convenções e expressões idiomáticas" - eu avisei. "sem motivo" - sim, tenho um motivo: simplifique onde a complexidade não serve para nada; "encarnação da otimização prematura" - sou a favor de um estilo de programação que escolhe um equilíbrio entre legibilidade e eficiência; O OP solicitou a obtenção de "benefício no uso do re.compile", que entendo como uma pergunta sobre eficiência. "(ab) usando classes como namespaces" - são suas palavras que são abusivas. classe existe para que você tenha um ponto de referência "auto". Eu tentei usar módulos para esse fim, as classes funcionam melhor.
flow
"capitalizando nomes de classes", "Não, não se trata do PEP8" - você parece estar tão escandalosamente irritado que nem consegue dizer o que discutir primeiro. "WTF", " errado " - vê como você é emocional? mais objetividade e menos espuma, por favor.
flow
-5

Meu entendimento é que esses dois exemplos são efetivamente equivalentes. A única diferença é que, no primeiro, você pode reutilizar a expressão regular compilada em outro lugar sem fazer com que ela seja compilada novamente.

Aqui está uma referência para você: http://diveintopython3.ep.io/refactoring.html

Chamar a função de pesquisa do objeto de padrão compilado com a string 'M' realiza o mesmo que chamar re.search com a expressão regular e a string 'M'. Apenas muito, muito mais rápido. (De fato, a função re.search simplesmente compila a expressão regular e chama o método de pesquisa do objeto padrão resultante para você.)

Matthew Maravillas
fonte
1
i não downvote você, mas tecnicamente isso é errado: Python não vai recompilar qualquer maneira
Tríptico