¿Vale la pena usar la recompilación de Python?

462

¿Hay algún beneficio en el uso de compilación para expresiones regulares en Python?

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

vs

re.match('hello', 'hello world')
Estera
fuente
8
Aparte del hecho de que en 2.6 re.subno tendrá un argumento banderas ...
new123456
58
Acabo de encontrarme con un caso en el que usar re.compiledio una mejora de 10-50x. La moraleja es que si tiene muchas expresiones regulares (más de MAXCACHE = 100) y las usa muchas veces cada una (y separadas por más de expresiones regulares de MAXCACHE en el medio, para que cada una se elimine del caché: así que use el mismo muchas veces y luego pasar al siguiente no cuenta), entonces definitivamente ayudaría compilarlos. De lo contrario, no hace la diferencia.
ShreevatsaR
8
Una pequeña cosa a tener en cuenta es que para las cadenas que no necesitan expresión regular, la inprueba de subcadena de cadenas es MUCHO más rápida:>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 ¡Interesante! ¿Puedes publicar una respuesta con un ejemplo que muestre una mejora de 10x-50x? La mayoría de las respuestas dadas aquí en realidad muestran una mejora de 3x en algunos casos precisos, y en otros casos casi ninguna mejora.
Basj
1
@Basj Hecho, publicó una respuesta . No me molesté en desenterrar para qué estaba usando Python en diciembre de 2013, pero lo primero que probé muestra el mismo comportamiento.
ShreevatsaR

Respuestas:

436

He tenido mucha experiencia ejecutando una expresión regular compilada 1000 veces en comparación con la compilación sobre la marcha, y no he notado ninguna diferencia perceptible. Obviamente, esto es anecdótico, y ciertamente no es un gran argumento en contra de la compilación, pero he encontrado que la diferencia es insignificante.

EDITAR: después de echar un vistazo rápido al código real de la biblioteca de Python 2.5, veo que Python compila internamente Y CACHE expresiones regulares cada vez que las usa (incluidas las llamadas a re.match()), por lo que realmente solo está cambiando CUANDO se compila la expresión regular, y no debería ' No ahorrará mucho tiempo, solo el tiempo que lleva verificar el caché (una búsqueda de clave en un dicttipo interno ).

Del módulo re.py (los comentarios son míos):

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

Todavía a menudo precompilo expresiones regulares, pero solo para vincularlas a un nombre agradable y reutilizable, no para obtener un aumento de rendimiento esperado.

Tríptico
fuente
12
Su conclusión es inconsistente con su respuesta. Si las expresiones regulares se compilan y almacenan automáticamente, no es necesario en la mayoría de los casos hacerlo a mano.
jfs
84
JF Sebastian, sirve como una señal para el programador de que la expresión regular en cuestión se usará mucho y no está destinada a ser desechable.
kaleissin
40
Más que eso, diría que si no desea sufrir el golpe de compilación y caché en alguna parte crítica del rendimiento de su aplicación, es mejor compilarlos de antemano en una parte no crítica de su aplicación .
Eddie Parker el
20
Veo la principal ventaja de usar expresiones regulares compiladas si reutilizas la misma expresión regular varias veces, reduciendo así la posibilidad de errores tipográficos. Si solo lo llama una vez, entonces sin compilar es más legible.
monkut
18
Entonces, la principal diferencia será cuando use muchas expresiones regulares diferentes (más de _MAXCACHE), algunas solo una vez y otras muchas veces ... entonces es importante mantener sus expresiones compiladas para aquellas que se usan más para que no se expulsa del caché cuando está lleno.
fortran
133

Para mí, el mayor beneficio re.compilees poder separar la definición de la expresión regular de su uso.

Incluso una expresión simple como 0|[1-9][0-9]*(entero en la base 10 sin ceros a la izquierda) puede ser lo suficientemente compleja como para que no tenga que volver a escribirla, verifique si cometió algún error tipográfico y luego tendrá que volver a verificar si hay errores al comenzar la depuración . Además, es mejor usar un nombre de variable como num o num_b10 que 0|[1-9][0-9]*.

Ciertamente es posible almacenar cadenas y pasarlas a re.match; sin embargo, eso es menos legible:

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

Versus compilación:

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

Aunque está bastante cerca, la última línea de la segunda se siente más natural y más simple cuando se usa repetidamente.

Acumenus
fuente
55
Estoy de acuerdo con esta respuesta; A menudo, el uso de re.compile da como resultado un código más, no menos legible.
Carl Meyer
1
Sin embargo, a veces ocurre lo contrario, por ejemplo, si define la expresión regular en un lugar y utiliza sus grupos coincidentes en otro lugar lejano.
Ken Williams
1
@KenWilliams No necesariamente, una expresión regular bien nombrada para un propósito específico debe ser clara incluso cuando se usa lejos de la definición original. Por ejemplo us_phone_numbero social_security_numberetc.
Brian M. Sheldon
2
@ BrianM.Sheldon nombrar bien la expresión regular realmente no le ayuda a saber qué representan sus diversos grupos de captura.
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

por lo tanto, si va a usar mucho la misma expresión regular, puede valer la pena hacerlo re.compile(especialmente para expresiones regulares más complejas).

Se aplican los argumentos estándar contra la optimización prematura, pero no creo que realmente pierda mucha claridad / re.compilesencillez si sospecha que sus expresiones regulares pueden convertirse en un cuello de botella en el rendimiento.

Actualizar:

En Python 3.6 (sospecho que los tiempos anteriores se realizaron con Python 2.x) y el hardware 2018 (MacBook Pro), ahora obtengo los siguientes tiempos:

% 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.

También agregué un caso (observe las diferencias entre comillas entre las dos últimas ejecuciones) que muestra que re.match(x, ...)es literalmente [aproximadamente] equivalente a re.compile(x).match(...), es decir, parece que no ocurre el almacenamiento en caché de la representación compilada detrás de escena.

dF.
fuente
55
Aquí hay problemas importantes con su metodología, ya que el argumento de configuración NO se incluye en el tiempo. Por lo tanto, ha eliminado el tiempo de compilación del segundo ejemplo, y solo lo promedia en el primer ejemplo. Esto no significa que el primer ejemplo se compila siempre.
Tríptico
1
Sí, estoy de acuerdo en que esta no es una comparación justa de los dos casos.
Kiv
77
Entiendo a qué te refieres, pero ¿no es eso exactamente lo que sucedería en una aplicación real donde la expresión regular se usa muchas veces?
dF.
26
@Triptych, @Kiv: El objetivo de compilar expresiones regulares separadas del uso es minimizar la compilación; eliminarlo del tiempo es exactamente lo que dF debería haber hecho, porque representa el uso en el mundo real con mayor precisión. El tiempo de compilación es especialmente irrelevante con la forma en que timeit.py hace sus tiempos aquí; realiza varias ejecuciones y solo informa la más corta, momento en el que se almacena en caché la expresión regular compilada. El costo adicional que está viendo aquí no es el costo de compilar regexp, sino el costo de buscarlo en el caché de regexp compilado (un diccionario).
jemfinch
3
@Triptych ¿Debería retirarse la import reconfiguración? Se trata de dónde quieres medir. Si ejecuto un script de Python varias veces, me tocaría el import retiempo. Al comparar los dos es importante separar las dos líneas para el tiempo. Sí, como dices, es cuando tendrás el tiempo deseado. La comparación muestra que, o bien toma el tiempo de golpe una vez y repite el tiempo de menor tiempo compilando o toma el golpe cada vez, suponiendo que el caché se borre entre las llamadas, lo que, como se ha señalado, podría suceder. Agregar un momento de h=re.compile('hello')ayudaría a aclarar.
Tom Myddeltyn
39

Aquí hay un caso de prueba simple:

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

con 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

Por lo tanto, parece que la compilación es más rápida con este caso simple, incluso si solo coincide una vez .

David King
fuente
2
¿Qué versión de Python es esta?
Kyle Strand
2
en realidad no importa, el punto es probar el punto de referencia en el entorno donde ejecutará el código
david king
1
Para mí, el rendimiento es casi exactamente el mismo para 1000 bucles o más. La versión compilada es más rápida para 1-100 bucles. (En ambas pitones 2.7 y 3.4).
Zitrax
2
En mi configuración de Python 2.7.3 casi no hay diferencia. A veces la compilación es más rápida, a veces es más lenta. La diferencia siempre es <5%, así que cuento la diferencia como medida de incertidumbre, ya que el dispositivo solo tiene una CPU.
Dakkaron
1
En Python 3.4.3 visto en dos ejecuciones separadas: usar compilado fue aún más lento que no compilado.
Zelphir Kaltstahl el
17

Acabo de probar esto yo mismo. Para el caso simple de analizar un número de una cadena y sumarlo, usar un objeto de expresión regular compilado es aproximadamente dos veces más rápido que usar los remétodos.

Como otros han señalado, los remétodos (incluido re.compile) buscan la cadena de expresión regular en un caché de expresiones compiladas previamente. Por lo tanto, en el caso normal, el costo adicional de usar los remétodos es simplemente el costo de la búsqueda de caché.

Sin embargo, el examen del código muestra que el caché está limitado a 100 expresiones. Esto plantea la pregunta, ¿qué tan doloroso es desbordar el caché? El código contiene una interfaz interna para el compilador de expresiones regulares, re.sre_compile.compile. Si lo llamamos, omitimos el caché. Resulta ser aproximadamente dos órdenes de magnitud más lento para una expresión regular básica, como r'\w+\s+([0-9_]+)\s+\w*'.

Aquí está mi prueba:

#!/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

Los métodos 'reallyCompiled' usan la interfaz interna, que evita el caché. Tenga en cuenta que el que se compila en cada iteración de bucle solo se repite 10,000 veces, no un millón.

Jorge
fuente
Estoy de acuerdo con usted en que las expresiones regulares compiladas se ejecutan mucho más rápido que las no compiladas. Ejecuté más de 10,000 oraciones e hice un ciclo en ellas para iterar para expresiones regulares cuando las expresiones regulares no se compilaron y se calcularon cada vez que la predicción de una ejecución completa fue de 8 horas, después de crear un diccionario de acuerdo con el índice con patrones de expresiones regulares compiladas. todo por 2 minutos. No puedo entender las respuestas anteriores ...
Eli Borodach
12

Estoy de acuerdo con Honest Abe en que los match(...)ejemplos dados son diferentes. No son comparaciones individuales y, por lo tanto, los resultados varían. Para simplificar mi respuesta, uso A, B, C, D para esas funciones en cuestión. Oh sí, estamos tratando con 4 funciones en re.pylugar de 3.

Ejecutando este fragmento de código:

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

es lo mismo que ejecutar este código:

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

Porque, cuando se mira en la fuente re.py, (A + B) significa:

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

y (C) es en realidad:

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

Entonces, (C) no es lo mismo que (B). De hecho, (C) llama (B) después de llamar (D), que también es llamado por (A). En otras palabras, (C) = (A) + (B). Por lo tanto, comparar (A + B) dentro de un ciclo tiene el mismo resultado que (C) dentro de un ciclo.

George ha regexTest.pydemostrado esto por nosotros.

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

El interés de todos es cómo obtener el resultado de 2.323 segundos. Para asegurarnos de que compile(...)solo se nos llame una vez, debemos almacenar el objeto regex compilado en la memoria. Si estamos usando una clase, podríamos almacenar el objeto y reutilizarlo cada vez que se llama a nuestra función.

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

Si no estamos usando la clase (que es mi solicitud hoy), entonces no tengo ningún comentario. Todavía estoy aprendiendo a usar la variable global en Python, y sé que la variable global es algo malo.

Un punto más, creo que usar el (A) + (B)enfoque tiene una ventaja. Aquí hay algunos hechos que observé (corrígeme si me equivoco):

  1. Llama a A una vez, realizará una búsqueda en la _cachesiguiente sre_compile.compile()para crear un objeto regex. Llama a A dos veces, realizará dos búsquedas y una compilación (porque el objeto regex está en caché).

  2. Si el _cacheenjuague en el medio, entonces el objeto regex se libera de la memoria y Python necesita compilarse nuevamente. (Alguien sugiere que Python no se recompilará).

  3. Si mantenemos el objeto regex usando (A), el objeto regex todavía entrará en _cache y se enjuagará de alguna manera. Pero nuestro código tiene una referencia y el objeto regex no se liberará de la memoria. Esos, Python no necesita compilar nuevamente.

  4. Las diferencias de 2 segundos en la prueba de George compiledInLoop vs compilado es principalmente el tiempo requerido para construir la clave y buscar en la caché. No significa el tiempo de compilación de expresiones regulares.

  5. La prueba de compilación realmente de George muestra lo que sucede si realmente vuelve a hacer la compilación cada vez: será 100 veces más lento (redujo el ciclo de 1,000,000 a 10,000).

Estos son los únicos casos en que (A + B) es mejor que (C):

  1. Si podemos almacenar en caché una referencia del objeto regex dentro de una clase.
  2. Si necesitamos llamar a (B) repetidamente (dentro de un ciclo o varias veces), debemos almacenar en caché la referencia al objeto regex fuera del ciclo.

Caso que (C) es lo suficientemente bueno:

  1. No podemos almacenar en caché una referencia.
  2. Solo lo usamos de vez en cuando.
  3. En general, no tenemos demasiadas expresiones regulares (supongamos que la compilada nunca se descarga)

Solo un resumen, aquí están los ABC:

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

Gracias por leer.

John Pang
fuente
8

Principalmente, hay poca diferencia si usa re.compile o no. Internamente, todas las funciones se implementan en términos de un paso de compilación:

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)

Además, re.compile () omite la lógica indirecta y de almacenamiento en caché adicionales:

_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

Además del pequeño beneficio de velocidad del uso de re.compile , a las personas también les gusta la legibilidad que resulta de nombrar especificaciones de patrones potencialmente complejas y separarlas de la lógica comercial donde se aplican:

#### 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()

Tenga en cuenta que otro encuestado creía incorrectamente que los archivos pyc almacenaban patrones compilados directamente; sin embargo, en realidad se reconstruyen cada vez que se carga el PYC:

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

El desmontaje anterior proviene del archivo PYC que tmp.pycontiene:

import re
lc_vowels = re.compile(r'[aeiou]{2,5}')
Raymond Hettinger
fuente
1
es el "en def search(pattern, string, flags=0):"un error tipográfico?
phuclv
1
Tenga en cuenta que si patternya es un patrón compilado, la sobrecarga de almacenamiento en caché se vuelve significativa: el hashing a SRE_Patternes costoso y el patrón nunca se escribe en caché, por lo que la búsqueda falla cada vez con a KeyError.
Eric Duminil
5

En general, creo que es más fácil usar banderas (al menos más fácil de recordar cómo), como re.Icuando se compilan patrones que usar banderas en línea.

>>> 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
fuente
De re.findalltodos modos, podría usar banderas como el tercer argumento de la .
aderchox
5

Usando los ejemplos dados:

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

El método de coincidencia en el ejemplo anterior no es el mismo que se usa a continuación:

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

re.compile () devuelve un objeto de expresión regular , lo que significa que hes un objeto regex.

El objeto regex tiene su propio método de coincidencia con los parámetros opcionales pos y endpos :

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

pos

El segundo parámetro opcional pos da un índice en la cadena donde comenzará la búsqueda; su valor predeterminado es 0. Esto no es completamente equivalente a cortar la cadena; el '^'carácter del patrón coincide en el comienzo real de la cadena y en las posiciones justo después de una nueva línea, pero no necesariamente en el índice donde comenzará la búsqueda.

endpos

El parámetro opcional endpos limita qué tan lejos se buscará la cadena; será como si la cadena tuviera caracteres endpos largos, por lo que solo se buscarán coincidencias entre los caracteres de pos a endpos - 1. Si endpos es menor que pos , no se encontrará coincidencia; de lo contrario, si rx es un objeto de expresión regular compilado, rx.search(string, 0, 50)es equivalente a rx.search(string[:50], 0).

Los métodos de búsqueda , findall y finditer del objeto regex también admiten estos parámetros.

re.match(pattern, string, flags=0)no los admite como se puede ver,
ni su búsqueda , findall y finditer homólogos.

Un objeto de coincidencia tiene atributos que complementan estos parámetros:

match.pos

El valor de pos que se pasó al método search () o match () de un objeto regex. Este es el índice en la cadena en la que el motor RE comenzó a buscar una coincidencia.

match.endpos

El valor de endpos que se pasó al método search () o match () de un objeto regex. Este es el índice en la cadena más allá del cual el motor RE no irá.


Un objeto regex tiene dos atributos únicos, posiblemente útiles:

regex.groups

El número de grupos de captura en el patrón.

regex.groupindex

Un diccionario que asigna cualquier nombre de grupo simbólico definido por (? P) a números de grupo. El diccionario está vacío si no se utilizaron grupos simbólicos en el patrón.


Y finalmente, un objeto de coincidencia tiene este atributo:

match.re

El objeto de expresión regular cuyo método match () o search () produjo esta instancia de coincidencia.

Abe honesto
fuente
4

Dejando a un lado la diferencia de rendimiento, el uso de re.compile y el objeto compilado de expresión regular para hacer coincidir (cualesquiera que sean las operaciones relacionadas con la expresión regular) aclara la semántica del tiempo de ejecución de Python.

Tuve una experiencia dolorosa de depurar un código simple:

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

y luego usaría comparar en

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

donde patternPhrasesse supone que es una variable que contiene una cadena de expresión regular,x[columnIndex] es una variable que contiene una cadena.

Tuve problemas para que patternPhrasesno coincida con alguna cadena esperada!

Pero si usé el formulario re.compile:

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

entonces en

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

Python se habría quejado de que "la cadena no tiene el atributo de coincidencia", ya que mediante el mapeo de argumentos posicionales compare, ¡ x[columnIndex]se usa como expresión regular !, cuando realmente quise decir

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

En mi caso, el uso de re.compile es más explícito del propósito de la expresión regular, cuando su valor está oculto a simple vista, por lo que podría obtener más ayuda de la comprobación de tiempo de ejecución de Python.

Entonces, la moraleja de mi lección es que cuando la expresión regular no es solo una cadena literal, entonces debería usar re.compile para permitir que Python me ayude a afirmar mi suposición.

Yu Shen
fuente
4

Hay una ventaja adicional de usar re.compile (), en forma de agregar comentarios a mis patrones de expresiones regulares usando re.VERBOSE

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

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

Aunque esto no afecta la velocidad de ejecución de su código, me gusta hacerlo de esta manera, ya que es parte de mi hábito de comentar. No me gusta pasar tiempo tratando de recordar la lógica detrás de mi código 2 meses después cuando quiero hacer modificaciones.

cyneo
fuente
1
He editado tu respuesta. Creo que re.VERBOSEvale la pena mencionarlo , y agrega algo que las otras respuestas parecen haber dejado de lado. Sin embargo, liderar su respuesta con "Estoy publicando aquí porque aún no puedo comentar" seguramente lo eliminará. No utilice el cuadro de respuestas para nada más que respuestas. Estás a solo una o dos buenas respuestas de poder comentar en cualquier lugar (50 repeticiones), así que ten paciencia. Poner comentarios en los cuadros de respuesta cuando sepas que no deberías llegar allí más rápido. Te dará votos negativos y respuestas eliminadas.
skrrgwasme
4

De acuerdo con la documentación de Python :

La secuencia

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

es equivalente a

result = re.match(pattern, string)

pero usando re.compile() y guardar el objeto de expresión regular resultante para su reutilización es más eficiente cuando la expresión se usará varias veces en un solo programa.

Entonces, mi conclusión es que si va a hacer coincidir el mismo patrón para muchos textos diferentes, es mejor que lo precompile.

Chris Wu
fuente
3

Curiosamente, la compilación me resulta más eficiente (Python 2.5.2 en 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

Ejecutando el código anterior una vez como está, y una vez con las dos iflíneas comentadas al revés, la expresión regular compilada es dos veces más rápida

Eli Bendersky
fuente
2
El mismo problema que con la comparación de rendimiento de dF. No es realmente justo a menos que incluya el costo de rendimiento de la declaración de compilación en sí.
Carl Meyer
66
Carl, no estoy de acuerdo. La compilación sólo se ejecuta una vez, mientras que el bucle correspondiente se ejecuta un millón de veces
Eli Bendersky
@eliben: Estoy de acuerdo con Carl Meyer. La compilación tiene lugar en ambos casos. Triptych menciona que el almacenamiento en caché está involucrado, por lo que en un caso óptimo (re permanece en caché) ambos enfoques son O (n + 1), aunque la parte +1 está oculta cuando no usa re.compile explícitamente.
pimentón
1
No escriba su propio código de evaluación comparativa. Aprenda a usar timeit.py, que se incluye en la distribución estándar.
jemfinch
¿Cuánto tiempo de ese tiempo está recreando la cadena de patrón en el bucle for? Esta sobrecarga no puede ser trivial.
IceArdor
3

Realicé esta prueba antes de tropezar con la discusión aquí. Sin embargo, después de ejecutarlo, pensé que al menos publicaría mis resultados.

Robé y bastardeé el ejemplo en "Dominar expresiones regulares" de Jeff Friedl. Esto está en un macbook con OSX 10.6 (2Ghz intel core 2 duo, 4GB ram). La versión de Python es 2.6.1.

Ejecutar 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

Ejecutar 2: no utiliza 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
intrincado
fuente
3

Esta respuesta puede llegar tarde, pero es un hallazgo interesante. El uso de la compilación realmente puede ahorrarle tiempo si planea usar la expresión regular varias veces (esto también se menciona en los documentos). A continuación puede ver que el uso de una expresión regular compilada es la más rápida cuando se llama directamente al método de coincidencia. pasar un regex compilado a re.match lo hace aún más lento y pasar re.match con la cadena de patrones está en algún lugar en el medio.

>>> 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
fuente
3

Además de la actuación.

El uso compileme ayuda a distinguir los conceptos de
1. módulo (re) ,
2. objeto regex
3. objeto de coincidencia
Cuando comencé 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, hice una exhaustiva hoja de trucos del módulo repara su referencia.

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
fuente
2

Realmente respeto todas las respuestas anteriores. Desde mi opinion si! Seguro que vale la pena usar re.compile en lugar de compilar la expresión regular, una y otra vez, cada vez.

El uso de re.compile hace que su código sea más dinámico, ya que puede llamar a la expresión regular ya compilada, en lugar de volver a compilar de nuevo. Esto te beneficia en los casos:

  1. Esfuerzos del procesador
  2. Complejidad del tiempo.
  3. Hace que regex sea universal (se puede usar en findall, búsqueda, coincidencia)
  4. Y hace que su programa se vea genial.

Ejemplo:

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

Utilizando en Findall

 find_alpha_numeric_string.findall(example_string)

Usando en la búsqueda

  find_alpha_numeric_string.search(example_string)

Del mismo modo, puede usarlo para: Emparejar y sustituir

El Gr8 Adakron
fuente
1

Esta es una buena pregunta. A menudo ves que la gente usa re.compile sin razón. Reduce la legibilidad. Pero seguro que hay muchas veces que se requiere precompilar la expresión. Como cuando lo usas repetidas veces en un bucle o algo así.

Es como todo sobre programación (todo en la vida en realidad). Aplica el sentido común.

PEZ
fuente
Por lo que puedo ver en mi breve vistazo, Python in a Nutshell no menciona el uso sin re.compile (), lo que me hizo sentir curiosidad.
Mat
El objeto regex agrega un objeto más al contexto. Como dije, existen muchas situaciones donde re.compile () tiene su lugar. El ejemplo dado por el OP no es uno de ellos.
PEZ
1

(meses después) es fácil agregar su propio caché alrededor de re.match, o cualquier otra cosa,

""" 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 ...

Un wibni, ¿no sería bueno si: cachehint (size =), cacheinfo () -> size, hits, nclear ...

denis
fuente
1

He tenido mucha experiencia ejecutando una expresión regular compilada 1000 veces en comparación con la compilación sobre la marcha, y no he notado ninguna diferencia perceptible

Los votos sobre la respuesta aceptada llevan a suponer que lo que dice @Triptych es cierto para todos los casos. Esto no necesariamente es cierto. Una gran diferencia es cuando tiene que decidir si acepta una cadena de expresiones regulares o un objeto de expresiones regulares compilado como parámetro de una función:

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

Siempre es mejor compilar sus expresiones regulares en caso de que necesite reutilizarlas.

Tenga en cuenta que el ejemplo en el tiempo anterior simula la creación de un objeto regex compilado una vez en el momento de la importación frente a "sobre la marcha" cuando es necesario para una coincidencia.

solitario
fuente
1

Como respuesta alternativa, como veo que no se ha mencionado antes, seguiré citando los documentos de Python 3 :

¿Debería usar estas funciones de nivel de módulo, o debería obtener el patrón y llamar a sus métodos usted mismo? Si está accediendo a una expresión regular dentro de un ciclo, precompilarla ahorrará algunas llamadas a funciones. Fuera de los bucles, no hay mucha diferencia gracias al caché interno.

Michael Kiros
fuente
1

Aquí hay un ejemplo donde el uso re.compilees más de 50 veces más rápido, según lo solicitado .

El punto es el mismo que hice en el comentario anterior, es decir, usar re.compilepuede ser una ventaja significativa cuando su uso es tal que no se beneficia mucho de la memoria caché de compilación. Esto sucede al menos en un caso particular (que me encontré en la práctica), es decir, cuando todo lo siguiente es cierto:

  • Tiene muchos patrones de expresiones regulares (más de re._MAXCACHE, cuyo valor predeterminado es es actualmente 512), y
  • usa estas expresiones regulares muchas veces, y
  • Sus usos consecutivos del mismo patrón están separados por más que re._MAXCACHEotras expresiones regulares intermedias, de modo que cada uno se vacía del caché 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}')

Ejemplo de salida que obtengo en mi computadora portátil (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

No me molesté timeitporque la diferencia es muy marcada, pero cada vez obtengo números cualitativamente similares. Tenga en cuenta que incluso sin re.compileusar la misma expresión regular varias veces y pasar a la siguiente no fue tan malo (solo alrededor de 2 veces más lento que con re.compile), pero en el otro orden (recorrer muchas expresiones regulares), es significativamente peor , como se esperaba. Además, el aumento del tamaño de cache trabaja demasiado: simplemente poniendo re._MAXCACHE = len(patterns)en setup()más arriba (por supuesto que no recomiendo hacer este tipo de cosas en la producción como nombres con guiones son convencionalmente “privado”) deja caer el ~ 23 segundos de nuevo a ~ 0,7 segundos, lo que también coincide con nuestro entendimiento.

ShreevatsaR
fuente
PD: si uso solo 3 patrones de expresiones regulares en todo mi código, cada uno de ellos utilizado (sin ningún orden específico) cientos de veces, el caché de expresiones regulares mantendrá la expresión regular precompilada automáticamente, ¿es así?
Basj
@Basj Creo que podrías probarlo y ver :) Pero la respuesta, estoy bastante seguro, es sí: el único costo adicional en ese caso AFAICT es simplemente buscar el patrón en el caché . Tenga en cuenta también que el caché es global (nivel de módulo), por lo que, en principio, podría tener alguna biblioteca de dependencias que realice búsquedas de expresiones regulares entre las suyas, por lo que es difícil estar completamente seguro de que su programa solo usa 3 (o cualquier cantidad de) expresiones regulares patrones, pero sería bastante extraño ser de otra manera :)
ShreevatsaR
0

Las expresiones regulares se compilan antes de usarse cuando se usa la segunda versión. Si va a ejecutarlo muchas veces, definitivamente es mejor compilarlo primero. Si no compila cada vez que coincida con uno, está bien.

Adam Peck
fuente
0

Legibilidad / preferencia de carga cognitiva

Para mí, la ganancia principal es que solo necesito recordar y leer una forma de la sintaxis de la API de expresiones regulares complicadas: la <compiled_pattern>.method(xxx)forma en lugar de eso y lare.func(<pattern>, xxx) forma.

los re.compile(<pattern>) es un poco extra repetitivo, cierto.

Pero en lo que respecta a la expresión regular, es poco probable que ese paso de compilación adicional sea una gran causa de carga cognitiva. Y de hecho, en patrones complicados, incluso puede obtener claridad al separar la declaración de cualquier método de expresión regular que luego invoque.

Primero tiendo a ajustar patrones complicados en un sitio web como Regex101, o incluso en un script de prueba mínimo por separado, luego los incluyo en mi código, por lo que separar la declaración de su uso también se adapta a mi flujo de trabajo.

JL Peyret
fuente
-1

Me gustaría motivar que la compilación previa sea ventajosa tanto conceptual como "alfabetizada" (como en "programación alfabetizada"). Echa un vistazo a este fragmento 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()

en su solicitud, escribiría:

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

Esto es tan simple en términos de funcionalidad como puede ser. porque este es un ejemplo tan corto, combiné la forma de obtener _text_has_foobar_re_searchtodo en una línea. La desventaja de este código es que ocupa un poco de memoria durante la vida útil del TYPOobjeto de la biblioteca; La ventaja es que al realizar una búsqueda de foobar, se saldrá con dos llamadas a funciones y dos búsquedas de diccionario de clase. cuántas expresiones regulares se almacenan en caché rey la sobrecarga de ese caché son irrelevantes aquí.

Compare esto con el estilo más habitual, a continuación:

import re

class Typo:

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

En la aplicacion:

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

Admito que mi estilo es muy inusual para Python, tal vez incluso discutible. sin embargo, en el ejemplo que coincide más estrechamente con la forma en que se usa principalmente Python, para hacer una sola coincidencia, debemos crear una instancia de un objeto, realizar búsquedas de diccionario de tres instancias y realizar tres llamadas a funciones; Además, podríamos entrar enre problemas de almacenamiento caché al usar más de 100 expresiones regulares. Además, la expresión regular se oculta dentro del cuerpo del método, que la mayoría de las veces no es una buena idea.

se diga que cada subconjunto de medidas --- declaraciones de importación dirigidas y con alias; métodos con alias cuando corresponda; La reducción de llamadas a funciones y búsquedas en el diccionario de objetos puede ayudar a reducir la complejidad computacional y conceptual.

fluir
fuente
2
WTF No solo sacaste una vieja pregunta respondida. Su código tampoco es idiomático y está equivocado en muchos niveles: (ab) usar clases como espacios de nombres donde un módulo es suficiente, poner en mayúscula los nombres de clase, etc ... Consulte pastebin.com/iTAXAWen para mejores implementaciones. Sin mencionar que la expresión regular que usa también está rota. En general, -1
2
culpable. Esta es una vieja pregunta, pero no me importa ser el # 100 en una conversación lenta. La pregunta no ha sido cerrada. Advertí que mi código podría ser adverso para algunos gustos. Creo que si pudieras verlo como una mera demostración de lo que es factible en Python, como: si tomamos todo, todo lo que creemos, como opcional, y luego jugamos juntos de cualquier manera, cómo se ven las cosas que podemos ¿obtener? Estoy seguro de que puede discernir los méritos y las desventajas de esta solución y puede quejarse de manera más articulada. de lo contrario, debo concluir que su reclamo de error se basa en poco más que PEP008
flujo del
2
No, no se trata de PEP8. Eso es solo nombrar convenciones, y nunca votaría negativamente por no seguirlas. Te rechacé porque el código que mostraste está simplemente mal escrito. Desafía las convenciones y las expresiones idiomáticas sin razón alguna, y es una encarnación de la optimización permanente: tendrías que optimizar la luz del día de todos los demás códigos para que esto se convierta en un cuello de botella, e incluso la tercera reescritura que ofrecí es más corta, más idiomático e igual de rápido por su razonamiento (mismo número de acceso a atributos).
"mal escrito", ¿por qué exactamente? "desafía las convenciones y los modismos" - te lo advertí. "sin razón": sí, tengo una razón: simplificar donde la complejidad no sirve para nada; "encarnación de la optimización prematura": estoy muy a favor de un estilo de programación que elija un equilibrio entre legibilidad y eficiencia; OP solicitó la obtención de "beneficio en el uso de re.compile", que entiendo como una pregunta sobre la eficiencia. "(ab) usando clases como espacios de nombres" - son tus palabras las que son abusivas. la clase está ahí para que tenga un punto de referencia "propio". Intenté usar módulos para este propósito, las clases funcionan mejor.
fluya el
"poner en mayúscula los nombres de clase", "No, no se trata de PEP8", aparentemente estás tan escandalosamente enojado que ni siquiera puedes decir qué discutir primero. "WTF", " mal " --- ¿ves lo emocional que eres? Más objetividad y menos espuma, por favor.
fluye el
-5

Tengo entendido que esos dos ejemplos son efectivamente equivalentes. La única diferencia es que en el primero, puede reutilizar la expresión regular compilada en otro lugar sin hacer que se vuelva a compilar.

Aquí hay una referencia para usted: http://diveintopython3.ep.io/refactoring.html

Llamar a la función de búsqueda del objeto de patrón compilado con la cadena 'M' logra lo mismo que llamar a re.search con la expresión regular y la cadena 'M'. Solo que mucho, mucho más rápido. (De hecho, la función re.search simplemente compila la expresión regular y llama al método de búsqueda del objeto de patrón resultante por usted).

Matthew Maravillas
fuente
1
No te downvote, pero técnicamente esto es falso: Python no recompilar todos modos
Tríptico