| You are here: Inicio > Inmersión en Python > Ajustes de rendimiento > Optimización de expresiones regulares | << >> | ||||
Inmersión en PythonPython de novato a experto |
|||||
Lo primero que comprueba la función Soundex es si la entrada es una cadena de letras que no esté vacía. ¿Cual es la mejor manera de hacerlo?
Si respondió: “expresiones regulares”, siéntese en esa esquina y medite sobre sus malos instintos. Las expresiones regulares no son la respuesta correcta casi nunca; se deben evitar siempre que se pueda. No sólo por razones de rendimiento, sino simplemente porque son difíciles de depurar y mantener. Además de por razones de rendimiento.
Este fragmento de código de soundex/stage1/soundex1a.py comprueba si el argumento de la función source es una palabra hecha enteramente por letras, o por al menos una letra (no una cadena vacía):
allChars = string.uppercase + string.lowercase
if not re.search('^[%s]+$' % allChars, source):
return "0000"
¿Qué tal se desempeña soundex1a.py? Por conveniencia, la sección __main__ del script contiene este código que llama al módulo timeit, prepara una prueba de cronometrado con tres nombres diferentes, prueba cada nombre tres veces y muestra el tiempo mínimo de cada una:
if __name__ == '__main__': from timeit import Timer names = ('Woo', 'Pilgrim', 'Flingjingwaller') for name in names: statement = "soundex('%s')" % name t = Timer(statement, "from __main__ import soundex") print name.ljust(15), soundex(name), min(t.repeat())
Así que, ¿cómo rinde soundex1a.py con esta expresión regular?
C:\samples\soundex\stage1>python soundex1a.py Woo W000 19.3356647283 Pilgrim P426 24.0772053431 Flingjingwaller F452 35.0463220884
Como era de esperar, el algoritmo tarda significativamente más cuando se le llama con nombres más largos. Hay pocas cosas que podamos hacer para hacer más pequeña esa brecha (hacer que la función tome menos tiempo relativo para entradas más largas), pero la naturaleza del algoritmo dicta que nunca se ejecutará en tiempo constante.
La otra cosa a tener en mente es que estamos probando una muestra representativa de nombres. Woo es algo así como un caso trivial, ya que se acorta a una sola letra y se rellena con ceros. Pilgrim es un caso normal, de longitud media y una mezcla de letras significativas e ignoradas. Flingjingwaller es extraordinariamente largo y contiene duplicados consecutivos. Otras pruebas podrían ser de ayuda igualmente, pero éstas ya cubren un buen rango de casos diferentes.
Entonces, ¿qué pasa con esa expresión regular? Bueno, no es eficiente. Dado que la expresión está buscando letras en rangos (A-Z en mayúsculas y a-z en minúsculas), podemos usar atajos en la sintaxis de la expresión regular. Éste es soundex/stage1/soundex1b.py:
if not re.search('^[A-Za-z]+$', source):
return "0000"
timeit dice que soundex1b.py es ligeramente más rápido que soundex1a.py, pero nada con lo que uno pueda excitarse terriblemente:
C:\samples\soundex\stage1>python soundex1b.py Woo W000 17.1361133887 Pilgrim P426 21.8201693232 Flingjingwaller F452 32.7262294509
Ya vimos en Sección 15.3, “Refactorización” que las expresiones regulares se pueden compilar y ser reutilizadas para obtener resultados más rápidos. Dado que esta expresión regular no cambia nunca entre llamadas a función, podemos compilarla una vez y usarla así. Éste es soundex/stage1/soundex1c.py:
isOnlyChars = re.compile('^[A-Za-z]+$').search def soundex(source): if not isOnlyChars(source): return "0000"
Usar una versión compilada de la expresión regular en soundex1c.py es significativamente más rápido:
C:\samples\soundex\stage1>python soundex1c.py Woo W000 14.5348347346 Pilgrim P426 19.2784703084 Flingjingwaller F452 30.0893873383
Pero, ¿es éste el camino equivocado? La lógica aquí es simple: la entrada source no puede estar vacía, y debe estar compuesta enteramente de letras. ¿No sería más rápido escribir un bucle que compruebe cada carácter y eliminar totalmente las expresiones regulares?
Here is soundex/stage1/soundex1d.py:
if not source:
return "0000"
for c in source:
if not ('A' <= c <= 'Z') and not ('a' <= c <= 'z'):
return "0000"
Resulta que esta técnica aplicada en soundex1d.py no es más rápida que la de usar una expresión regular compilada (aunque es más rápida que usar una expresión regular sin compilar):
C:\samples\soundex\stage1>python soundex1d.py Woo W000 15.4065058548 Pilgrim P426 22.2753567842 Flingjingwaller F452 37.5845122774
¿Por qué no es más rápido soundex1d.py? La respuesta está en la naturaleza interpretada de Python. El motor de expresiones regulares está escrito en C, y compilado para que funcione de forma nativa en su computador. Por otro lado, este bucle está escrito en Python y funciona mediante el intérprete. Incluso aunque el bucle es relativamente simple, no es suficientemente simple para compensar por el hecho de ser interpretado. Las expresiones regulares no son nunca la respuesta adecuada... excepto cuando lo son.
Resulta que Python ofrece un método de cadenas algo oscuro. Tiene excusa para no saberlo ya que no lo he mencionado en ninguna parte de este libro. El método se llama isalpha(), y comprueba si una cadena contiene sólo letras.
Éste es soundex/stage1/soundex1e.py:
if (not source) and (not source.isalpha()):
return "0000"
¿Cuánto hemos ganado usando este método específico en soundex1e.py? Bastante.
C:\samples\soundex\stage1>python soundex1e.py Woo W000 13.5069504644 Pilgrim P426 18.2199394057 Flingjingwaller F452 28.9975225902
import string, re charToSoundex = {"A": "9", "B": "1", "C": "2", "D": "3", "E": "9", "F": "1", "G": "2", "H": "9", "I": "9", "J": "2", "K": "2", "L": "4", "M": "5", "N": "5", "O": "9", "P": "1", "Q": "2", "R": "6", "S": "2", "T": "3", "U": "9", "V": "1", "W": "9", "X": "2", "Y": "9", "Z": "2"} def soundex(source): if (not source) and (not source.isalpha()): return "0000" source = source[0].upper() + source[1:] digits = source[0] for s in source[1:]: s = s.upper() digits += charToSoundex[s] digits2 = digits[0] for d in digits[1:]: if digits2[-1] != d: digits2 += d digits3 = re.sub('9', '', digits2) while len(digits3) < 4: digits3 += "0" return digits3[:4] if __name__ == '__main__': from timeit import Timer names = ('Woo', 'Pilgrim', 'Flingjingwaller') for name in names: statement = "soundex('%s')" % name t = Timer(statement, "from __main__ import soundex") print name.ljust(15), soundex(name), min(t.repeat())
<< Uso del módulo timeit |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | |
Optimización de búsquedas en diccionarios >> |