Cap. 12 - Módulos

Un módulo es un archivo que contiene definiciones y sentencias Python que se quieren usar en otros programas Python. Hay varios módulos de Python que vienen con el propio lenguaje como parte de la librería estándar (standard library). Ya vimos dos de estos módulos hasta ahora: string y turtle.

También hemos visto cómo acceder a la ayuda (help). El sistema de ayuda contiene una lista de todos los módulos estándar que están disponibles en Python. Vale la pena consultarlo!

12.1) Números aleatorios (random)

Es frecuente que necesitemos números aleatorios en programas, aquí hay una lista de casos típicos:

Python provee un módulo random que ayuda con tareas como éstas. Se puede aprender más sobre él leyendo la ayuda, pero lo que vamos a utilizar de este módulo se muestra aquí:

#Crear una caja negra que genere números aleatorios
rng = random.Random()

lanzamiento_de_dado = rng.randrange(1, 7)    #Devuelve un entero de 1 a 6
espera_en_segundos = rng.random() * 5;

El método randrange genera un entero entre su primer y su segundo argumento, aplicando la misma semántica que range (por lo cual el extremo inferior es incluido, pero el superior es excluido). Todos los valores tienen la misma probabilidad de ocurrir (es decir, los resultados están uniformemente distribuidos). Como range, randrange también puede tomar un argumento step opcional. Por ejemplo, supongamos que necesitamos generar al azar un número impar menor que 100:

		r_impar = rng.randrange(1, 100, 2)

Otros métodos permiten generar otras distribuciones, por ejemplo la distribución normal (la de forma de campana) que puede ser más apropiada para estimar lluvias estacionales o la concentración de un compuesto en el cuerpo después de tomar una dosis medicinal. (El módulo random de Python tiene incorporadas estas distribuciones como opciones vía parámetros? O hay que implementarlas/simularlas?)

El método random retorna un número de tipo float en el intervalo [0.0, 1.0), donde el corchete y el paréntesis indican que el intervalo es cerrado por la izquierda y abierto por la derecha, es decir, 0.0 es posible como resultado, pero 1.0 nunca va a ocurrir. Es habitual escalar el resultado después del llamado a este método, para obtener números al azar en cualquier intervalo deseado. En el caso mostrado en el código anterior multiplicamos por cinco para obtener una espera_en_segundos que fuera un número al azar en el intervalo [0.0, 5.0). Como en el caso anterior, estos son números uniformemente distribuidos (los números cercanos a 0.0 son tan probables como los cercanos a 0.5 o a 1.0).

El siguiente ejemplo muestra cómo mezclar una lista de 52 números (shuffle no puede trabajar directamente con una lazy promise, por lo cual tuvimos que convertir el objeto range usando el convertidor de tipos list primero). El efecto obtenido es: list(range(52)) es una lista completa con los 52 números desde 0 a 51.

cartas = list(range(52))        # Genera enteros [0 .. 51]
rng.shuffle(cartas)             # Mezcla el paquete de 52 "cartas"

12.1.1) Repetibilidad y Testeo

Los generadores de números aleatorios se implementan en realidad mediante un algoritmo determinístico (repetible y predecible). Por lo tanto se los llama generadores pseudo-aleatorios (pseudo-random), pues no son auténticamente aleatorios. Comienzan con un valor semilla. Cada vez que se pide un nuevo número aleatorio, se obtiene uno basado en el actual atributo semilla, y el estado de la misma (que es uno de los atributos del generador) es actualizado.

Para hacer debugging y testing de unidades de testeo, conviene tener repetibilidad (la capacidad de repetir exactamente los mismos casos en cada repetición del testeo). Podemos forzar este comportamiento haciendo que el generador aleatorio sea siempre inicializado con la misma semilla. Con frecuencia esto sólo es deseable durante el testeo - jugar un juego de cartas en que el mazo es mezclado siempre de la misma forma se volvería rápidamente muy aburrido!

drng = random.Random(123)  # Crear un generador con un estado inicial conocido

Esta forma alternativa de crear un generador de números aleatorios da una semilla explícita al objeto. Si no se pasa este argumento, el sistema muy probablemente usará algo basado en la hora actual. Así que si se toman algunos números aleatorios de drng en sucesivos testeos se obtendrán una y otra vez los mismos resultados!

12.1.2) Tomando bolas de bolsas, tirando dados, mezclando un mazo de cartas

He aquí un ejemplo que genera una lista que contiene n enteros al azar entre un límite inferior y un límite superior:

def lista_enteros_aleatorios(num, borde_inferior, borde_superior):
   """
     Genera una lista que contiene num enteros aleatorios entre borde_inferior
     y borde_superior, siendo este último un borde abierto.
   """
   rng = random.Random()  # Crear un generador de números aleatorios
   resultado = []
   for i in range(num):
      resultado.append(rng.randrange(borde_inferior, borde_superior))
   return resultado

El output que se obtiene por un llamado típico es:

>>> lista_enteros_aleatorios(20, 5, 25)
[9, 7, 6, 17, 8, 7, 17, 18, 9, 24, 14, 12, 12, 6, 17, 6, 17, 22, 18, 11]

Observar que tuvimos duplicados en el resultado. Con frecuencia esto es aceptable (si tiramos un dado varias veces, esperamos que ocurran repeticiones).

Pero ¿qué pasa si no queremos duplicados? Si quisiéramos que todos los resultados fueran distintos, podemos hacer esto: generar la lista de posibilidades, mezclarla y luego tomar de allí la cantidad de elementos que se quieran. Por ejemplo, el siguiente programa devuelve 5 meses al azar, sin duplicados (representando los meses por números del 1 al 12)

def cinco_meses_sin_duplicados():
    xs = list(range(1,13))  # Crear una lista con los valores 1..12 (No hay duplicados aquí)
    rng = random.Random()   # Crear un generador de números aleatorios
    rng.shuffle(xs)         # Mezclar la lista
    resultado = xs[:5]      # Tomar los primeros 5 elementos
    return resultado

En cursos de estadística, el primer caso (permitir duplicados) se conoce como tomar bolas de una bolsa con reemplazo (se devuelve cada bola después de sacarla, por lo cual puede volver a salir). Mientras que el segundo caso, sin duplicados, se conoce como tomar bolas de una bolsa sin reemplazo (una vez que la bola es quitada de la bolsa, no es devuelta, y por lo tanto no puede volver a salir). Los juegos de lotería funcionan de esta manera.

El algoritmo que recién presentamos para generar números aleatorios sin repeticiones no sería buena idea si tuviéramos que elegir unos pocos elementos de un dominio muy grande. Supongamos que queremos 5 números elegidos al azar entre 1 y 10 millones, sin duplicados. Generar una lista de 10 millones de items, mezclarla, y luego cortar sus 5 primeros elementos sería un desperdicio de espacio en memoria y tiempo! Entonces podemos intentarlo de otra manera:

def lista_enteros_aleatorios_sin_duplicados(num, borde_inferior, borde_superior):
   """
     Genera una lista que contiene num enteros aleatorios entre
     borde_inferior y borde superior, en que borde_superior es abierto.
     La lista de resultados no puede contener duplicados.
   """
   resultado = []
   rng = random.Random()
   for i in range(num):
        while True:
            candidato = rng.randrange(borde_inferior, borde_superior)
            if candidato not in resultado:
                break
        resultado.append(candidato)
   return resultado

xs = lista_enteros_aleatorios_sin_duplicados(5, 1, 10000000)
print(xs)

Esto produce 5 números aleatorios, sin duplicados:

[2995530, 7460010, 8456245, 9815177, 6300155]

Pero incluso esta función tiene sus puntos débiles. ¿Puedes imaginar qué ocurrirá en el siguiente caso?

xs = lista_enteros_aleatorios_sin_duplicados(10, 1, 6)

12.2) El módulo TIME

A medida que comenzamos a trabajar con algoritmos más sofisticados y programas más grandes, una preocupación natural es "es nuestro código eficiente"? Una forma de saberlo es tomar el tiempo que lleva ejecutar cada operación. El módulo time tiene una función time que se recomienda para hacer esto. Donde sea que se llame a time, devuelve un número de tipo float que representa cuántos segundos pasaron desde una fecha fija convencional conocida como epoch. Esa fecha puede depender de la plataforma, pero suele ser el 1 de enero de 1970 a las 00:00:00 (UTC) en Windows y la mayor parte de las plataformas Unix.

El modo de usarlo es llamar a time y asignar su valor a una variable, por ejemplo t0, justo antes de comenzar a ejecutar la parte del código que se quiere medir. Concluida la ejecución de dicha parte, volver a llamar a time y guardar el resultado en una variable t1. La diferencia t1 - t0 es el tiempo que transcurrió, y nos puede servir para medir qué tan rápido está corriendo nuestro pgrograma.

Veamos un pequeño ejemplo. Python tiene una función built-in sum que puede sumar los elementos de una lista. Podemos nosotros también crear nuestra propia función para hacer lo mismo. ¿Cuál de las dos será más eficiente? Trataremos de sumar los valores de una lista [0, 1, 2, ...] con ambas funciones, y veremos cuál es más eficiente.

def mi_suma_de_lista(xs):
   suma = 0
   for v in xs:
    suma += v
   return suma

sz = 10000000       # Vamos a testear con una lista con 10 millones de elementos
testdata = range(sz)

t0 = time.time()
mi_resultado = mi_suma_de_lista(testdata)
t1 = time.time()
print("mi_resultado = {0} (tiempo requerido = {1:.4f} segundos".format(mi_resultado, t1 - t0))

t2 = time.time()
su_resultado = sum(testdata)
t3 = time.time()
print("su_resultado = {0} (tiempo requerido = {1:.4f} segundos".format(su_resultado, t3 - t2))

En una laptop común y corriente, obtuve estos resultados:

mi_resultado = 49999995000000 (tiempo requerido = 1.1871 segundos
su_resultado = 49999995000000 (tiempo requerido = 0.6406 segundos

Por lo cual nuestra función es un 85% más lenta que la que viene con Python. Generar y sumar 10 millones de elementos en menos de un segundo no es poca cosa!

12.3) El módulo MATH

El módulo math contiene el tipo de funciones matemáticas que típicamente se encuentran en una calculadora: sin, cos, sqrt, asin, log, log10, así como algunas constantes matemáticas como pi y e.

>>> import math
>>> math.pi                 # Constante pi
3.141592653589793
>>> math.e                  # Constante e, base del logaritmo natural
2.718281828459045
>>> math.sqrt(2.0)          # Función raíz cuadrada (sqrt)
1.4142135623730951
>>> math.radians(90)        # Convertir 90 grados a radianes
1.5707963267948966
>>> math.sin(math.radians(90))  # Encontrar el seno de 90 grados
1.0
>>> math.asin(1.0) * 2      # Duplicar el arcoseno de 1.0 para obtener pi
3.141592653589793

Como en casi todos los lenguajes de programación, los ángulos se expresan en radianes y no en grados. Hay dos funciones radians y degrees que permiten convertir entre estas dos formas habituales de medir ángulos.

Observar una diferencia entre nuestro uso de este módulo y nuestro uso de random y turtle. En éstos creábamos objetos y llamábamos a métodos de estos objetos. Esto es debido a que los objetos tenían estado (una tortuga tiene color, posición, dirección hacia la que apunta, etc., y todo generador de números aleatorios tiene una semilla generadora que determina su resultado siguiente).

Las funciones matemáticas son "puras" y no tienen ningún estado (el cálculo de la raíz cuadrada de 2.0 no depende de ninguna clase de estado o de historia sobre qué es lo que ocurrió en el pasado). Por lo tanto las funciones no son métodos de un objeto - son simplemente funciones que están agrupadas en un módulo llamado math.

12.4) Creando tus propios módulos

Todo lo que necesitas para crear tus propios módulos es guardar tu script con la extensión .py. Supongamos, por ejemplo, que guardamos el siguiente script en el archivo herramientas_secuencias.py

def eliminar_de_posicion(pos, secuencia):
    return secuencia[:pos] + secuencia[pos + 1:]

Ahora podemos usar nuestro módulo, tanto en scripts que escribamos más adelante, o en el intérprete interactivo de Python. Para hacerlo, debemos comenzar por importar el módulo:

>>> import herramientas_secuencias
>>> s = "Una frase!"
>>> herramientas_secuencias.eliminar_de_posicion(5, s)
'Una fase!'

No incluimos la extensión .py del nombre del archivo al importar. Python espera que los archivos de los módulos terminen con .py, por lo cual no es necesario incluir la extensión en la sentencia import.

El uso de módulos permite descomponer programas muy grandes en partes de tamaño manejable, y mantener juntas partes relacionadas.

12.5) Namespaces (espacios de nombres)

Un namespace (espacio de nombres) es una colección de identificadores que pertenecen a un módulo, o a una función (y como pronto veremos, también a una clase). Generalmente, buscamos que el espacio de nombres guarde cosas "relacionadas", por ejemplo, todas las funciones matemáticas, o todas las cosas que típicamente haríamos con números aleatorios.

Cada módulo tiene su propio espacio de nombres, así que podemos utilizar el mismo nombre (para algún identificador) en distintos módulos sin provocar un problema de identificación.

Consideremos por ejemplo estos dos módulos:

# Modulo1.py

pregunta = "Cuál es la fuente de toda sabiduría?"
respuesta = 373
   Y:
# Modulo2.py

pregunta = "Qué estás buscando?"
respuesta = "Es lo que quisiera saber."
   Ahora podemos importar ambos módulos y acceder a la pregunta y la respuesta de cada uno:
import modulo1
import modulo2

print(modulo1.pregunta)
print(modulo2.pregunta)
print(modulo1.respuesta)
print(modulo2.respuesta)

El output de este programa será:

Cuál es la fuente de toda sabiduría?
Qué estás buscando?
373
Es lo que quisiera saber.

Las funciones también tienen su propio espacio de nombres:

def f():
    n = 7
    print("valor de n dentro de f:", n)

def g():
    n = 42
    print("valor de n dentro de g:", n)

n = 11
print("valor de n antes de llamar a f:", n)
f()
print("valor de n después de llamar a f:", n)
g()
print("valor de n después de llamar a g:", n)

La ejecución del programa produce el siguiente output:

valor de n antes de llamar a f: 11
valor de n dentro de f: 7
valor de n después de llamar a f: 11
valor de n dentro de g: 42
valor de n después de llamar a g: 42

Las tres n no entran en conflicto porque cada una pertenece a un espacio de nombres distinto - son 3 nombres para 3 variables diferentes, del mismo modo como uno se puede encontrar con 3 instancias de persona que se llaman "Bruno".

Los espacios de nombres permiten a varios programadores trabajar en el mismo proyecto sin tener colisiones de nombres.

Cómo se relacionan espacios de nombres, archivos y módulos?

Python tiene un mapeo uno-a-uno muy sencillo: un módulo por archivo, que da pie a un espacio de nombres. También, Python toma el nombre del módulo del nombre del archivo y esto se convierte en el nombre del espacio de nombres. math.py es un archivo, el módulo se llama math y su espacio de nombres es math. Así que en Python los conceptos son más o menos intercambiables.

Sin embargo, en otros lenguajes (ejemplo, C#) se puede definir un módulo en múltiples archivos, o que un archivo tenga múltiples espacios de nombres, o que varios archivos compartan el mismo espacio de nombres. Es decir que en esos lenguajes el nombre del archivo no tiene por qué coincidir con el del espacio de nombres.

Por lo tanto, una buena idea es tratar de mantener una distinción entre ambos conceptos en nuestra mente (una cosa es el archivo, otra cosa es el espacio de nombres).

Los archivos y directorios organizan dónde las cosas se guardan en nuestra computadora. Por el otro lado, los espacios de nombres y módulos son conceptos de programación: nos ayudan a organizar cómo queremos agrupar funciones y atributos que se relacionan entre sí. No tienen que ver tanto con "dónde" se guardan las cosas, y a priori no tendrían por qué coincidir con la estructura de archivos y directorios.

Todo lo anterior implica que en Python, si le cambiáramos el nombre al archivo math.py, esto causaría el cambio del nombre del módulo, y nuestros llamados a import math deberían modificarse para reflejarlo, así como todos nuestros llamados a funciones y atributos del módulo a través del espacio de nombre math, cuyo nombre también habrá cambiado.

En otros lenguajes esto no ocurre necesariamente así. Así que no debemos mezclar los conceptos sólo porque Python los mezcla!

12.6) Reglas de alcance y búsqueda (lookup)

El alcance de un identificador es la región del código del programa en que el identificador puede ser accedido, o usado.

Hay tres tipos importantes de alcance en Python:

Python (como casi todos los lenguajes de programación) usa reglas de precedencia: el mismo nombre puede ocurrir en varios alcances, pero el más interno, o alcance local, tendrá siempre precedencia sobre el alcance global, y el global siempre se usa en preferencia al built-in. Veamos un ejemplo sencillo:

def range(n):
    return (123*n)

print(range(10))

Qué es mostrado por la función print? Acabamos de definir una función de nombre range, lo que introduce cierta ambigüedad. Cuando usamos range, ¿nos referimos a la nuestra, o a la built-in? La aplicación de las reglas de alcance determina que será nuestra versión de range la llamada, y no la built-in, porque nuestra función range está en el espacio de nombres global, que tiene precedencia sobre los nombres built-in.

Por lo tanto, si bien nombres como range y min son built-in, pueden ser "ocultados" de tu uso si eliges definir tus propias variables o funciones utilizando dichos nombres. Es una práctica confusa y poco recomendable redefinir nombres built-in - pero parte de ser un programador requiere comprender las reglas de alcance y comprender también que puedes hacer cosas turbias que causarán confusión, y que es mejor evitarlas.

Ahora, un ejemplo un poco más complejo:

n = 10
m = 3
def f(n):
	 m = 7
	return 2*n+m
print(f(5), n, m) 

Esto imprime 17 10 3. La razón es que las dos variables m y n en las líneas 1 y 2 están fuera de la función en el espacio de nombres global. Dentro de la función, nuevas variables n y m son definidas y existen sólo durante la ejecución de la función f. Éstas son creadas en el espacio de nombres local de la función f. Dentro del cuerpo de f, las reglas de alcance determinan que usamos las variables locales n y m. Por el contrario, cuando retornamos de f, los argumentos n y m pasados a la función print se refieren a las variables originales de las líneas 1 y 2, y éstas no fueron alteradas por la ejecución de la función f.

Observar también que el def pone al nombre f en el espacio de nombres global aquí. Por lo cual puede ser llamado en la línea print.

¿Cuál es el alcance de la variable n en la línea 1? Su alcance (la región en la que es visible) es en todas las líneas externas a la definición de f, y no es visible en las 3 líneas en que se define a f, porque en ellas se utiliza la definición local de la variable n.

12.7) Atributos y el operador punto

Las variables definidas dentro de un módulo se llaman atributos del módulo. Hemos visto que los objetos también tienen atributos: por ejemplo, la mayor parte de los objetos tienen un atributo __doc__, algunas funciones tienen un atributo __annotations__. Los atributos se acceden mediante el operador punto (.). El atributo pregunta de modulo1 y modulo2 en el ejemplo de la sección 12.5 era accedido usando modulo1.pregunta y modulo2.pregunta.

Los módulos también contienen funciones, y el operador punto permite acceder a ellas del mismo modo. En la sección 12.4 vimos que herramientas_secuencias.eliminar_de_posicion se refiere a la función eliminar_de_posicion del módulo herramientas_secuencias.

Cuando usamos un nombre con punto, muchas veces no referimos a él como nombre completamente cualificado, porque estamos diciendo exactamente a qué atributo pregunta nos queremos referir. Eliminamos así toda posible ambigüedad o error de interpretación.

12.8) Tres variantes de la sentencia IMPORT

Veremos aquí 3 formas distintas de importar nombres al espacio de nombres actual, y usarlos:

import math
x = math.sqrt(10)

Aquí simplemente el identificador math es agregado al espacio de nombres. Si se quiere acceder a alguna de las funciones del módulo, es necesario utilizar la notación punto.

Aquí hay otra forma de hacerlo:

from math import cos, sin, sqrt
x = sqrt(10)

Los nombres se agregan directamente al espacio de nombres actual, y pueden ser utilizados sin cualificación. El nombre math en sí mismo no es importado, por lo cual el intento de usar el nombre cualificado math.sqrt devolverá un mensaje de error.

Por último tenemos una abreviación conveniente:

from math import *  	# Importa todos los identificadores de math,
                     			# agregándolos al espacio de nombres actual
x = sqrt(10)         		# Se pueden utilizar sin cualificación

De los 3, el primero suele ser el método preferido, incluso si requiere escribir un poco más cada vez que se llama a una función o atributo del módulo. De todas formas se puede abreviar esto, importando el módulo con un nombre más corto:

import math as m
m.pi
3.141592653589793

Pero tampoco es tan complicado escribir el nombre del módulo completo, con los editores actuales que hacen auto-completar y muestran menúes con listas emergentes de atributos y funciones.

Finalmente, observemos este caso:

def area(radio):
    import math
    return math.pi * radio * radio

x = math.sqrt(10)      # Esto da un error

Aquí hemos importado math, pero lo hicimos dentro de un espacio de nombres local (el de la función area). Así que el nombre se puede utilizar dentro del cuerpo de la función, pero no en el script que la incluye, porque no es visible en el espacio de nombres global.

12.9) Convierte tu tester de unidades en un módulo

Cerca del final del capítulo 6 (cuando vimos funciones fructíferas) introdujimos el concepto de testeo de unidades, e implementamos nuestra propia función test, y tuvimos que copiarla dentro de cada módulo para el cual escribimos tests. Ahora podemos poner esa definición dentro de un módulo propio, por ejemplo unit_tester.py, y simplemente usar una línea en cada nuevo script:

from unit_tester import test

12.10) Glosario

12.11) Ejercicios

1) Abre la ayuda sobre el módulo calendar.

import calendar
cal = calendar.TextCalendar()      # Crea una instancia
cal.pryear(2012)                   # ¿Qué pasa aquí?


d = calendar.LocaleTextCalendar(6, "SPANISH")
d.pryear(2012)

 

2) Abre la ayuda sobre el módulo math: (hacerlo)

 

3) Investigar el módulo copy. Qué hace deepcopy? En qué ejercicios del último capítulo podría haber ayudado deepcopy? (hacerlo)

 

4) Crear un módulo mimodulo1.py. Agregarle atributos miedad para guardar tu edad actual, y año para guardar el año actual. Crea otro módulo mimodulo2.py. Agrega atributos miedad con el valor 0 y año con el valor del año en que naciste. Ahora crea un archivo de nombre namespace_test.py, importa ambos módulo anteriores y escribe la siguiente sentencia: (hacerlo)

print( (mimodulo2.miedad - mimodulo1.miedad) == (mimodulo2.año - mimodulo1.año) )

 

5) Agrega la siguiente sentencia a mimodulo1.py, mimodulo2.py y namespace_test.py del ejercicio anterior: (hacerlo)

print("My name is", __name__)

if __name__ == "__main__":
    print("This won't run if I'm  imported.")

 

6) En una línea de comandos Python, intenta lo siguiente:

>>> import this

 

7) Describe la respuesta del intérprete Python a cada una de las siguientes sentencias en una sesión continuada en el intérprete: (hacerlo)

>>> s = "If we took the bones out, it wouldn't be crunchy, would it?"
>>> s.split()
>>> type(s.split())
>>> s.split("o")
>>> s.split("i")
>>> "0".join(s.split("o"))

def myreplace(old, new, s):
    """ Replace all occurrences of old with new in s. """
    ...

test(myreplace(",", ";", "this, that, and some other thing") ==
                         "this; that; and some other thing")
test(myreplace(" ", "**",
                 "Words will now      be  separated by stars.") ==
                 "Words**will**now**be**separated**by**stars.")

 

8) Crea un módulo llamado wordtools.py ya preparado para utilizar el módulo de testeo. Luego implementa funciones que pasen los tests siguientes: (hacerlo)

test(cleanword("what?") == "what")
test(cleanword("'now!'") == "now")
test(cleanword("?+='w-o-r-d!,@$()'") ==  "word")

test(has_dashdash("distance--but"))
test(not has_dashdash("several"))
test(has_dashdash("spoke--"))
test(has_dashdash("distance--but"))
test(not has_dashdash("-yo-yo-"))

test(extract_words("Now is the time!  'Now', is the time? Yes, now.") ==
      ['now','is','the','time','now','is','the','time','yes','now'])
test(extract_words("she tried to curtsey as she spoke--fancy") ==
      ['she','tried','to','curtsey','as','she','spoke','fancy'])

test(wordcount("now", ["now","is","time","is","now","is","is"]) == 2)
test(wordcount("is", ["now","is","time","is","now","the","is"]) == 3)
test(wordcount("time", ["now","is","time","is","now","is","is"]) == 1)
test(wordcount("frog", ["now","is","time","is","now","is","is"]) == 0)

test(wordset(["now", "is", "time", "is", "now", "is", "is"]) ==
      ["is", "now", "time"])
test(wordset(["I", "a", "a", "is", "a", "is", "I", "am"]) ==
      ["I", "a", "am", "is"])
test(wordset(["or", "a", "am", "is", "are", "be", "but", "am"]) ==
      ["a", "am", "are", "be", "but", "is", "or"])

test(longestword(["a", "apple", "pear", "grape"]) == 5)
test(longestword(["a", "am", "I", "be"]) == 2)
test(longestword(["this","supercalifragilisticexpialidocious"]) == 34)
test(longestword([ ]) == 0)