18.4. Optimización de búsquedas en diccionarios

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.

Ejemplo 18.4. El mejor resultado hasta ahora: soundex/stage2/soundex2c.py


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