17.5. plural.py, fase 4

Eliminemos la duplicación del código para que sea más sencillo definir nuevas reglas.

Ejemplo 17.9. plural4.py


import re

def buildMatchAndApplyFunctions((pattern, search, replace)):  
    matchFunction = lambda word: re.search(pattern, word)      1
    applyFunction = lambda word: re.sub(search, replace, word) 2
    return (matchFunction, applyFunction)                      3
1 buildMatchAndApplyFunctions es una función que construye otras de forma dinámica. Toma pattern, search y replace (en realidad toma una tupla, pero hablaremos sobre eso en un momento), y podemos construir la función de comparación usando la sintaxis lambda para que sea una función que toma un parámetro (word) e invoca a re.search con el pattern que se pasó a la función buildMatchAndApplyFunctions, y la word que se pasó a la función de comparación que estamos construyendo. ¡Guau!
2 La construcción la función de transformación se realiza igual. La función de transformación toma un parámetros y llama a re.sub con los parámetros search y replace que se pasaron a la función buildMatchAndApplyFunctions, y la word que se pasó a la función de transformación que estamos creando. Este técnica de usar los valores de parámetros externos dentro de una función dinámica se denomina closure[21]. Esencialmente estamos definiendo constantes dentro de la función de transformación que estamos creando: toma un parámetro (word), pero luego actúa sobre otros dos valores más (search y replace) cuyos valores se fijaron cuando definimos la función de transformación.
3 Por último, la función buildMatchAndApplyFunctions devuelve una tupla con los dos valores: las dos funciones que acabamos de crear. Las constantes que definimos dentro de esas funciones (pattern dentro de matchFunction, y search y replace dentro de applyFunction) se quedan en esas funciones incluso tras volver de buildMatchAndApplyFunctions. ¡Esto es de locura!

Si esto es increíblemente confuso (y debería serlo, es materia muy absurda), puede que quede más claro cuando vea cómo usarlo.

Ejemplo 17.10. continuación de plural4.py

patterns = \
  (
    ('[sxz]$', '$', 'es'),
    ('[^aeioudgkprt]h$', '$', 'es'),
    ('(qu|[^aeiou])y$', 'y$', 'ies'),
    ('$', '$', 's')
  )                                                 1
rules = map(buildMatchAndApplyFunctions, patterns)  2
1 Nuestras reglas de pluralización están definidas ahora como una serie de cadenas (y no funciones). La primera cadena es la expresión regular que usaremos en re.search para ver si la regla coincide; la segunda y la tercera son las expresiones de búsqueda y sustitución que usaríamos en re.sub para aplicar realmente la regla que convierte un sustantivo en su plural.
2 Esta línea es magia. Toma la lista de cadenas de patterns y las convierte en una lista de funciones. ¿Cómo? Relacionando las cadenas con la función buildMatchAndApplyFunctions, que resulta de tomar tres cadenas como parámetros y devolver una tupla con dos funciones. Esto significa que rules acaba siendo exactamente lo mismo que en el ejemplo anterior: una lista de tuplas donde cada tupla es un par de funciones, en que la primera función es la de comparación que llama a re.search, y la segunda función es la de transformación que llama a re.sub.

Le juro que no me estoy inventando esto: rules acaba siendo exactamente la misma lista de funciones del ejemplo anterior. Desenrolle la definición de rules y obtendrá esto:

Ejemplo 17.11. Desarrollo de la definición de reglas

rules = \
  (
    (
     lambda word: re.search('[sxz]$', word),
     lambda word: re.sub('$', 'es', word)
    ),
    (
     lambda word: re.search('[^aeioudgkprt]h$', word),
     lambda word: re.sub('$', 'es', word)
    ),
    (
     lambda word: re.search('[^aeiou]y$', word),
     lambda word: re.sub('y$', 'ies', word)
    ),
    (
     lambda word: re.search('$', word),
     lambda word: re.sub('$', 's', word)
    )
   )                                          

Ejemplo 17.12. final de plural4.py


def plural(noun):                                  
    for matchesRule, applyRule in rules:            1
        if matchesRule(noun):                      
            return applyRule(noun)                 
1 Ya que la lista rules es la misma del ejemplo anterior, no debería sorprenderle que no haya cambiado la función plural. Recuerde: es completamente genérica; toma una lista de funciones de reglas y las llama por orden. No le importa cómo se hayan definido. En la fase 2 se definieron como funciones con nombres diferentes. En la fase 3 se definieron como funciones lambda anónimas. Ahora en la fase 4 las estamos construyendo de forma dinámica relacionando la función buildMatchAndApplyFunctions sobre una lista de cadenas sin procesar. No importa; la función plural sigue funcionando de la misma manera.

Sólo por si acaso su mente no está ya suficientemente machacada, debo confesarle que había una sutileza en la definición de buildMatchAndApplyFunctions que no he explicado. Volvamos atrás a echarle otro vistazo.

Ejemplo 17.13. Otra mirada sobre buildMatchAndApplyFunctions


def buildMatchAndApplyFunctions((pattern, search, replace)):   1
1 ¿Advierte los dobles paréntesis? Esta función no toma tres parámetros en realidad; sólo toma uno, una tupla de tres elementos. Pero la tupla se expande al llamar a la función y los tres elementos de la tupla se asignan cada uno a una variable diferente. pattern, search y replace. ¿Sigue confuso? Veámoslo en acción.

Ejemplo 17.14. Expansión de tuplas al invocar funciones

>>> def foo((a, b, c)):
...     print c
...     print b
...     print a
>>> parameters = ('apple', 'bear', 'catnap')
>>> foo(parameters) 1
catnap
bear
apple
1 La manera correcta de invocar las función foo es con una tupla de tres elementos. Cuando se llama a la función se asignan los elementos a diferentes variables locales dentro de foo.

Ahora vamos a ver por qué era necesario este truco de autoexpansión de tupla. patterns es una lista de tuplas y cada tupla tiene tres elementos. Cuando se invoca map(buildMatchAndApplyFunctions, patterns), eso significa que no estamos llamando a buildMatchAndApplyFunctions con tres parámetros. Si usamos map para relacionar una única lista a una función siempre invocamos la función con un único parámetros: cada elemento de la lista. En el caso de patterns, cada elemento de la lista es una tupla, así que a buildMatchAndApplyFunctions siempre se le llama con la tupla, y usamos el truco de autoexpansión de tuplas en la definición de buildMatchAndApplyFunctions para asignar los elementos de la tupla a variables con las que podemos trabajar.

Footnotes

[21] N. del T.: o cierre