| You are here: Inicio > Inmersión en Python > Ajustes de rendimiento > Optimización de búsquedas en diccionarios | << >> | ||||
Inmersión en PythonPython de novato a experto |
|||||
El segundo paso del algoritmo Soundex es convertir los caracteres en dígitos según un patrón específico. ¿Cual es la mejor manera de hacer esto?
La solución más obvia es definir un diccionario con caracteres individuales como claves y sus dígitos correspondientes como valores, y hacer consultas al diccionario por cada carácter. Esto es lo que tenemos en soundex/stage1/soundex1c.py (uno de los mejores resultados hasta ahora):
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):
# ... input check omitted for brevity ...
source = source[0].upper() + source[1:]
digits = source[0]
for s in source[1:]:
s = s.upper()
digits += charToSoundex[s]
Ya hemos cronometrado soundex1c.py; éste es su rendimiento:
C:\samples\soundex\stage1>python soundex1c.py Woo W000 14.5341678901 Pilgrim P426 19.2650071448 Flingjingwaller F452 30.1003563302
Este código es muy simple pero, ¿es la mejor solución? Invocar upper() sobre cada carácter individualmente no parece eficiente; probablemente sería mejor llamar a upper() una sola vez sobre la cadena entera.
Luego está el asunto de construir la cadena digits de forma incremental. La construcción incremental de cadenas es horriblemente ineficiente; de forma interna, el intérprete de Python necesita crear una cadena nueva cada vez dentro del bucle, y descartar la anterior.
Python es bueno con las listas, sin embargo. Podemos tratar automáticamente una cadena como una lista de caracteres. Y es fácil combinar de nuevo una lista en una cadena, usando el método de cadena join().
Éste es soundex/stage2/soundex2a.py, que convierte las letras en dígitos usando ↦ y lambda:
def soundex(source): # ... source = source.upper() digits = source[0] + "".join(map(lambda c: charToSoundex[c], source[1:]))
Sorprendentemente, soundex2a.py no es más rápida:
C:\samples\soundex\stage2>python soundex2a.py Woo W000 15.0097526362 Pilgrim P426 19.254806407 Flingjingwaller F452 29.3790847719
La sobrecarga de la función anónima lambda destruye cualquier rendimiento que ganemos al tratar la cadena como una lista de caracteres.
soundex/stage2/soundex2b.py usa una lista por comprensión en lugar de ↦ y lambda:
source = source.upper()
digits = source[0] + "".join([charToSoundex[c] for c in source[1:]])
Al usar una lista de comprensión soundex2b.py es más rápida que soundex2a.py usando ↦ y lambda, pero sigue sin ser más rápida que el código original (construir una cadena de forma incremental en soundex1c.py):
C:\samples\soundex\stage2>python soundex2b.py Woo W000 13.4221324219 Pilgrim P426 16.4901234654 Flingjingwaller F452 25.8186157738
Es hora de dar un enfoque radicalmente diferente. Las búsquedas en diccionarios son herramientas de propósito general. Las claves de los diccionarios pueden ser cadenas de cualquier longitud (o muchos otros tipos de datos), pero en este caso estamos tratando con claves de un único carácter y valores de un único carácter. Resulta que Python tiene una función especializada exactamente para tratar esta situación: la función string.maketrans.
Esto es soundex/stage2/soundex2c.py:
allChar = string.uppercase + string.lowercase charToSoundex = string.maketrans(allChar, "91239129922455912623919292" * 2) def soundex(source): # ... digits = source[0].upper() + source[1:].translate(charToSoundex)
¿Qué demonios está pasando aquí? string.maketrans crea una matriz de traducción entre dos cadenas: el primer argumento y el segundo. En este caso, el primer argumento es la cadena ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz, y el segundo la cadena 9123912992245591262391929291239129922455912623919292. ¿Ve el patrón? Es el mismo patrón de conversión que estamos haciendo caligráficamente con un diccionario. A se corresponde con 9, B con 1, C con 2, etc.. Pero no es un diccionario; es una estructura de datos especializada a la que podemos acceder usando el método de cadenas translate, que traduce cada carácter en el dígito correspondiente, de acuerdo a la matriz definida por string.maketrans.
timeit muestra que soundex2c.py es significativamente más rápida que si definiéramos un diccionario e iterásemos sobre la cadena para construir la salida de modo incremental:
C:\samples\soundex\stage2>python soundex2c.py Woo W000 11.437645008 Pilgrim P426 13.2825062962 Flingjingwaller F452 18.5570110168
No va a conseguir nada mucho mejor que esto. Python tiene una función especializada que hace exactamente lo que quiere; úsela y no se rompa la cabeza.
import string, re allChar = string.uppercase + string.lowercase charToSoundex = string.maketrans(allChar, "91239129922455912623919292" * 2) isOnlyChars = re.compile('^[A-Za-z]+$').search def soundex(source): if not isOnlyChars(source): return "0000" digits = source[0].upper() + source[1:].translate(charToSoundex) 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())
<< Optimización de expresiones regulares |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | |
Optimización de operaciones con listas >> |