Inmersión en Python
Inmersión en Python
Inmersión en Python
25 de enero de 2005
Los programas de ejemplo de este libro son software libre; pueden ser
redistribuidos y modificados según los términos de la licencia de Python
publicada por la Python Software Foundation. En el Apéndice H, Python 2.1.1
license, se incluye una copia de la licencia.
Tabla de contenidos
• 1. Instalación de Python
o 1.1. ¿Qué Python es adecuado para usted?
o 1.2. Python en Windows
o 1.3. Python en Mac OS X
o 1.4. Python en Mac OS 9
o 1.5. Python en RedHat Linux
o 1.6. Python en Debian GNU/Linux
o 1.7. Instalación de Python desde el Código Fuente
o 1.8. El intérprete interactivo
o 1.9. Resumen
• 2. Su primer programa en Python
o 2.1. Inmersión
o 2.2. Declaración de funciones
2.2.1. Los tipos de Python frente a los de otros lenguajes de
programación
o 2.3. Documentación de funciones
o 2.4. Todo es un objeto
2.4.1. La ruta de búsqueda de import
2.4.2. ¿Qué es un objeto?
o 2.5. Sangrado (indentado) de código
o 2.6. Prueba de módulos
• 3. Tipos de dato nativos
o 3.1. Presentación de los diccionarios
3.1.1. Definir diccionarios
3.1.2. Modificar diccionarios
3.1.3. Borrar elementos de diccionarios
o 3.2. Presentación de las listas
3.2.1. Definir listas
3.2.2. Añadir de elementos a listas
3.2.3. Buscar en listas
3.2.4. Borrar elementos de listas
3.2.5. Uso de operadores de lista
o 3.3. Presentación de las tuplas
o 3.4. Declaración de variables
3.4.1. Referencia a variables
3.4.2. Asignar varios valores a la vez
o 3.5. Formato de cadenas
o 3.6. Inyección de listas (mapping)
o 3.7. Unir listas y dividir cadenas
3.7.1. Nota histórica sobre los métodos de cadena
o 3.8. Resumen
• 4. El poder de la introspección
o 4.1. Inmersión
o 4.2. Argumentos opcionales y con nombre
o 4.3. Uso de type, str, dir, y otras funciones incorporadas
4.3.1. La función type
4.3.2. La función str
4.3.3. Funciones incorporadas
o 4.4. Obtención de referencias a objetos con getattr
4.4.1. getattr con módulos
4.4.2. getattr como dispatcher
o 4.5. Filtrado de listas
o 4.6. La peculiar naturaleza de and y or
4.6.1. Uso del truco the and-or
o 4.7. Utilización de las funciones lambda
4.7.1. Funciones lambda en el mundo real
o 4.8. Todo junto
o 4.9. Resumen
• 5. Objetos y orientación a objetos
o 5.1. Inmersión
o 5.2. Importar módulos usando from módulo import
o 5.3. Definición de clases
5.3.1. Inicialización y programación de clases
5.3.2. Saber cuándo usar self e __init__
o 5.4. Instanciación de clases
5.4.1. Recolección de basura
o 5.5. Exploración de UserDict: Una clase cápsula
o 5.6. Métodos de clase especiales
5.6.1. Consultar y modificar elementos
o 5.7. Métodos especiales avanzados
o 5.8. Presentación de los atributos de clase
o 5.9. Funciones privadas
o 5.10. Resumen
• 6. Excepciones y gestión de ficheros
o 6.1. Gestión de excepciones
6.1.1. Uso de excepciones para otros propósitos
o 6.2. Trabajo con objetos de fichero
6.2.1. Lectura de un fichero
6.2.2. Cerrar ficheros
6.2.3. Gestión de errores de E/S
6.2.4. Escribir en ficheros
o 6.3. Iteración con bucles for
o 6.4. Uso de sys.modules
o 6.5. Trabajo con directorios
o 6.6. Todo junto
o 6.7. Resumen
• 7. Expresiones regulares
o 7.1. Inmersión
o 7.2. Caso de estudio: direcciones de calles
o 7.3. Caso de estudio: números romanos
7.3.1. Comprobar los millares
7.3.2. Comprobación de centenas
o 7.4. Uso de la sintaxis {n,m}
7.4.1. Comprobación de las decenas y unidades
o 7.5. Expresiones regulares prolijas
o 7.6. Caso de estudio: análisis de números de teléfono
o 7.7. Resumen
• 8. Procesamiento de HTML
o 8.1. Inmersión
o 8.2. Presentación de sgmllib.py
o 8.3. Extracción de datos de documentos HTML
o 8.4. Presentación de BaseHTMLProcessor.py
o 8.5. locals y globals
o 8.6. Cadenas de formato basadas en diccionarios
o 8.7. Poner comillas a los valores de los atributos
o 8.8. Presentación de dialect.py
o 8.9. Todo junto
o 8.10. Resumen
• 9. Procesamiento de XML
o 9.1. Inmersión
o 9.2. Paquetes
o 9.3. Análisis de XML
o 9.4. Unicode
o 9.5. Búsqueda de elementos
o 9.6. Acceso a atributos de elementos
o 9.7. Transición
• 10. Scripts y flujos
o 10.1. Abstracción de fuentes de datos
o 10.2. Entrada, salida y error estándar
o 10.3. Caché de búsqueda de nodos
o 10.4. Encontrar hijos directos de un nodo
o 10.5. Creación de manejadores diferentes por tipo de nodo
o 10.6. Tratamiento de los argumentos en línea de órdenes
o 10.7. Todo junto
o 10.8. Resumen
• 11. Servicios Web HTTP
o 11.1. Inmersión
o 11.2. Cómo no obtener datos mediante HTTP
o 11.3. Características de HTTP
11.3.1. User-Agent
11.3.2. Redirecciones
11.3.3. Last-Modified/If-Modified-Since
11.3.4. ETag/If-None-Match
11.3.5. Compresión
o 11.4. Depuración de servicios web HTTP
o 11.5. Establecer el User-Agent
o 11.6. Tratamiento de Last-Modified y ETag
o 11.7. Manejo de redirecciones
o 11.8. Tratamiento de datos comprimidos
o 11.9. Todo junto
o 11.10. Resumen
• 12. Servicios web SOAP
o 12.1. Inmersión
o 12.2. Instalación de las bibliotecas de SOAP
12.2.1. Instalación de PyXML
12.2.2. Instalación de fpconst
12.2.3. Instalación de SOAPpy
o 12.3. Primeros pasos con SOAP
o 12.4. Depuración de servicios web SOAP
o 12.5. Presentación de WSDL
o 12.6. Introspección de servicios web SOAP con WSDL
o 12.7. Búsqueda en Google
o 12.8. Solución de problemas en servicios web SOAP
o 12.9. Resumen
• 13. Pruebas unitarias (Unit Testing)
o 13.1. Introducción a los números romanos
o 13.2. Inmersión
o 13.3. Presentación de romantest.py
o 13.4. Prueba de éxito
o 13.5. Prueba de fallo
o 13.6. Pruebas de cordura
• 14. Programación Test-First
o 14.1. roman.py, fase 1
o 14.2. roman.py, fase 2
o 14.3. roman.py, fase 3
o 14.4. roman.py, fase 4
o 14.5. roman.py, fase 5
• 15. Refactorización
o 15.1. Gestión de fallos
o 15.2. Tratamiento del cambio de requisitos
o 15.3. Refactorización
o 15.4. Epílogo
o 15.5. Resumen
• 16. Programación Funcional
o 16.1. Inmersión
o 16.2. Encontrar la ruta
o 16.3. Revisión del filtrado de listas
o 16.4. Revisión de la relación de listas
o 16.5. Programación "datocéntrica"
o 16.6. Importación dinámica de módulos
o 16.7. Todo junto
o 16.8. Resumen
• 17. Funciones dinámicas
o 17.1. Inmersión
o 17.2. plural.py, fase 1
o 17.3. plural.py, fase 2
o 17.4. plural.py, fase 3
o 17.5. plural.py, fase 4
o 17.6. plural.py, fase 5
o 17.7. plural.py, fase 6
o 17.8. Resumen
• 18. Ajustes de rendimiento
o 18.1. Inmersión
o 18.2. Uso del módulo timeit
o 18.3. Optimización de expresiones regulares
o 18.4. Optimización de búsquedas en diccionarios
o 18.5. Optimización de operaciones con listas
o 18.6. Optimización de manipulación de cadenas
o 18.7. Resumen
• A. Lecturas complementarias
• B. Repaso en 5 minutos
• C. Trucos y consejos
• D. Lista de ejemplos
• E. Historial de revisiones
• F. Sobre este libro
• G. GNU Free Documentation License
o G.0. Preamble
o G.1. Applicability and definitions
o G.2. Verbatim copying
o G.3. Copying in quantity
o G.4. Modifications
o G.5. Combining documents
o G.6. Collections of documents
o G.7. Aggregation with independent works
o G.8. Translation
o G.9. Termination
o G.10. Future revisions of this license
o G.11. How to use this License for your documents
• H. Python 2.1.1 license
o H.A. History of the software
o H.B. Terms and conditions for accessing or otherwise using
Python
H.B.1. PSF license agreement
H.B.2. BeOpen Python open source license agreement
version 1
H.B.3. CNRI open source GPL-compatible license
agreement
Capítulo 1. Instalación de Python
Si está usando una cuenta en un servidor alquilado, puede que el ISP ya haya
instalado Python. Las distribuciones de Linux más populares incluyen Python
en la instalación predeterminada. Mac OS X 10.2 y posteriores incluyen una
versión de Python para la línea de órdenes, aunque probablemente quiera
instalar una versión que incluya una interfaz gráfica más acorde con Mac.
Como puede ver, Python funciona en una gran cantidad de sistemas operativos.
La lista completa incluye Windows, Mac OS, Mac OS X, y todas las variedades
de sistemas libres compatibles con UNIX, como Linux. También hay versiones
que funcionan en Sun Solaris, AS/400, Amiga, OS/2, BeOS, y una plétora de
otras plataformas de las que posiblemente no haya oído hablar siquiera.
Es más, los programas escritos para Python en una plataforma pueden
funcionar, con algo de cuidado, en cualquiera de las plataformas soportadas. Por
ejemplo, habitualmente desarrollo programas para Python en Windows que
luego funcionarán en Linux.
1. Descargue ActivePython de
http://www.activestate.com/Products/ActivePython/.
2. Si está usando Windows 95, Windows 98, o Windows ME, también
necesitará descargar e instalar Windows Installer 2.0 antes de instalar
ActivePython.
3. Haga doble clic sobre el instalador, ActivePython-2.2.2-224-win32-
ix86.msi.
Python 2.3.2 (#49, Oct 2 2003, 20:02:00) [MSC v.1200 32 bit (Intel)]
on win32
Type "copyright", "credits" or "license()" for more information.
****************************************************************
Personal firewall software may warn about the connection IDLE
makes to its subprocess using this computer's internal loopback
interface. This connection is not visible on any external
interface and no data is sent to or received from the Internet.
****************************************************************
IDLE 1.0
>>>
Pruebe:
Welcome to Darwin!
[localhost:~] usted% python
Python 2.2 (#1, 07/14/02, 23:25:09)
[GCC Apple cpp-precomp 6.14] on darwin
Type "help", "copyright", "credits", or "license" for more
information.
>>> [pulse Ctrl+D para volver a la línea de órdenes]
[localhost:~] usted%
Tenga en cuenta que una vez instale la última versión, la preinstalada seguirá
presente. Si ejecuta scripts desde la línea de órdenes, debe saber qué versión de
Python está usando.
localhost:~$ su -
Password: [introduzca la clave de root]
[root@localhost root]# wget
http://python.org/ftp/python/2.3/rpms/redhat-9/python2.3-2.3-
5pydotorg.i386.rpm
Resolving python.org... done.
Connecting to python.org[194.109.137.226]:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 7,495,111 [application/octet-stream]
...
[root@localhost root]# rpm -Uvh python2.3-2.3-5pydotorg.i386.rpm
Preparing...
########################################### [100%]
1:python2.3
########################################### [100%]
[root@localhost root]# python
Python 2.2.2 (#1, Feb 24 2003, 19:13:11)
[GCC 3.2.2 20030222 (Red Hat Linux 3.2.2-4)] on linux2
Type "help", "copyright", "credits", or "license" for more
information.
>>> [pulse Ctrl+D para salir]
[root@localhost root]# python2.3
Python 2.3 (#1, Sep 12 2003, 10:53:56)
[GCC 3.2.2 20030222 (Red Hat Linux 3.2.2-5)] on linux2
Type "help", "copyright", "credits", or "license" for more
information.
>>> [pulse Ctrl+D para salir]
[root@localhost root]# which python2.3
/usr/bin/python2.3
localhost:~$ su -
Password: [introduzca la clave de root]
localhost:~# apt-get install python
Reading Package Lists... Done
Building Dependency Tree... Done
The following extra packages will be installed:
python2.3
Suggested packages:
python-tk python2.3-doc
The following NEW packages will be installed:
python python2.3
0 upgraded, 2 newly installed, 0 to remove and 3 not upgraded.
Need to get 0B/2880kB of archives.
After unpacking 9351kB of additional disk space will be used.
Do you want to continue? [Y/n] Y
Selecting previously deselected package python2.3.
(Reading database ... 22848 files and directories currently
installed.)
Unpacking python2.3 (from .../python2.3_2.3.1-1_i386.deb) ...
Selecting previously deselected package python.
Unpacking python (from .../python_2.3.1-1_all.deb) ...
Setting up python (2.3.1-1) ...
Setting up python2.3 (2.3.1-1) ...
Compiling python modules in /usr/lib/python2.3 ...
Compiling optimized python modules in /usr/lib/python2.3 ...
localhost:~# exit
logout
localhost:~$ python
Python 2.3.1 (#2, Sep 24 2003, 11:39:14)
[GCC 3.3.2 20030908 (Debian prerelease)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> [pulse Ctrl+D para salir]
localhost:~$ su -
Password: [introduzca la clave de root]
localhost:~# wget http://www.python.org/ftp/python/2.3/Python-2.3.tgz
Resolving www.python.org... done.
Connecting to www.python.org[194.109.137.226]:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 8,436,880 [application/x-tar]
...
localhost:~# tar xfz Python-2.3.tgz
localhost:~# cd Python-2.3
localhost:~/Python-2.3# ./configure
checking MACHDEP... linux2
checking EXTRAPLATDIR...
checking for --without-gcc... no
...
localhost:~/Python-2.3# make
gcc -pthread -c -fno-strict-aliasing -DNDEBUG -g -O3 -Wall -Wstrict-
prototypes
-I. -I./Include -DPy_BUILD_CORE -o Modules/python.o Modules/python.c
gcc -pthread -c -fno-strict-aliasing -DNDEBUG -g -O3 -Wall -Wstrict-
prototypes
-I. -I./Include -DPy_BUILD_CORE -o Parser/acceler.o Parser/acceler.c
gcc -pthread -c -fno-strict-aliasing -DNDEBUG -g -O3 -Wall -Wstrict-
prototypes
-I. -I./Include -DPy_BUILD_CORE -o Parser/grammar1.o
Parser/grammar1.c
...
localhost:~/Python-2.3# make install
/usr/bin/install -c python /usr/local/bin/python2.3
...
localhost:~/Python-2.3# exit
logout
localhost:~$ which python
/usr/local/bin/python
localhost:~$ python
Python 2.3.1 (#2, Sep 24 2003, 11:39:14)
[GCC 3.3.2 20030908 (Debian prerelease)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> [pulse Ctrl+D para volver a la línea de órdenes]
localhost:~$
Ahora que ya ha instalado Python, ¿qué es este intérprete interactivo que está
ejecutando?
Es algo así: Python lleva una doble vida. Es un intérprete para scripts que puede
ejecutar desde la línea de órdenes o como aplicaciones si hace clic dos veces
sobre sus iconos. Pero también es un intérprete interactivo que puede evaluar
sentencias y expresiones arbitrarias. Esto es muy útil para la depuración,
programación rápida y pruebas. ¡Incluso conozco gente que usa el intérprete
interactivo de Python a modo de calculadora!
>>> 1 + 1
2
>>> print 'hola mundo'
hola mundo
>>> x = 1
>>> y = 2
>>> x + y
3
1.9. Resumen
• 2.1. Inmersión
• 2.2. Declaración de funciones
o 2.2.1. Los tipos de Python frente a los de otros lenguajes de
programación
• 2.3. Documentación de funciones
• 2.4. Todo es un objeto
o 2.4.1. La ruta de búsqueda de import
o 2.4.2. ¿Qué es un objeto?
• 2.5. Sangrado (indentado) de código
• 2.6. Prueba de módulos
2.1. Inmersión
def buildConnectionString(params):
"""Crea una cadena de conexión partiendo de un diccionario de
parámetros.
Devuelve una cadena."""
return ";".join(["%s=%s" % (k, v) for k, v in params.items()])
if __name__ == "__main__":
myParams = {"server":"mpilgrim", \
"database":"master", \
"uid":"sa", \
"pwd":"secret" \
}
print buildConnectionString(myParams)
server=mpilgrim;uid=sa;database=master;pwd=secret
def buildConnectionString(params):
def buildConnectionString(params):
"""Crea una cadena de conexión partiendo de un diccionario de
parámetros.
Las comillas triples implican una cadena multilínea. Todo lo que haya entre el
principio y el final de las comillas es parte de una sola cadena, incluyendo los
retornos de carro y otros comillas. Puede usarlas para definir cualquier cadena,
pero donde las verá más a menudo es haciendo de cadena de documentación.
Las comillas triples también son una manera sencilla de definir una cadena
que contenga comillas tanto simples como dobles, como qq/.../ en Perl.
(esto es, lo primero tras los dos puntos). Técnicamente, no necesita dotar a su
función de una cadena de documentación, pero debería hacerlo siempre. Sé que
habrá escuchado esto en toda clase de programación a la que haya asistido
alguna vez, pero Python le da un incentivo añadido: la cadena de
documentación está disponible en tiempo de ejecución como atributo de la
función.
Muchos IDE de Python utilizan la cadena de documentación para
proporcionar una ayuda sensible al contexto, de manera que cuando
escriba el nombre de una función aparezca su cadena de documentación
como ayuda. Esto puede ser increíblemente útil, pero lo será tanto como
buenas las cadenas de documentación que usted escriba.
En caso de que no se haya dado cuenta, acabo de decir que las funciones de
Python tienen atributos y que dispone de esos atributos en tiempo de ejecución.
import en Python es como require en Perl. Una vez que hace import sobre
búsqueda actual (la suya puede ser diferente, dependiendo del sistema
operativo, qué versión de Python esté ejecutando, y dónde la instaló
originalmente). Python buscará en estos directorios (y en ese orden) un
fichero .py que corresponda al nombre del módulo que intenta importar.
En realidad mentí; la verdad es más complicada que eso, porque no todos los
módulos se instalan como ficheros .py. Algunos, como el módulo sys, son
«módulos incorporados» ("built-in"); y están dentro del propio Python. Los
módulos built-in se comportan exactamente como los normales pero no
dispone de su código fuente en Python, ¡porque no se escriben usando
Python! (el módulo sys está escrito en C.)
Todo en Python es un objeto, y casi todo tiene atributos y métodos. Todas las
funciones tienen un atributo __doc__ que devuelve la cadena de documentación
definida en su código fuente. El módulo sys es un objeto que contiene (entre
otras cosas) un atributo llamado path. Y así con todo.
Aún así, la pregunta sigue sin contestar. ¿Qué es un objeto? Los diferentes
lenguajes de programación definen “objeto” de maneras diferentes. En algunos
significa que todos los objetos deben tener atributos y métodos; en otros esto
significa que todos los objetos pueden tener subclases. En Python la definición
es más amplia: algunos objetos no tienen ni atributos ni métodos (más sobre
esto en el Capítulo 3), y no todos los objetos pueden tener subclases (más al
respecto en el Capítulo 5). Pero todo es un objeto en el sentido de que se puede
asignar a una variable o ser pasado como argumento a una función (más sobre
en el Capítulo 4).
Esto es tan importante que voy a repetirlo en caso de que se lo haya perdido las
últimas veces: todo en Python es un objeto. Las cadenas son objetos. Las listas son
objetos. Las funciones son objetos. Incluso los módulos son objetos.
Las funciones de Python no tienen begin o end explícitos, ni llaves que marquen
dónde empieza o termina su código. El único delimitador son dos puntos (:) y
el sangrado del propio código.
def buildConnectionString(params):
"""Crea una cadena de conexión partiendo de un diccionario de
parámetros.
Los bloques de código van definidos por su sangrado. Con «bloque de código»
quiero decir funciones, sentencias if, bucles for, while, etc. El sangrado
comienza un bloque y su ausencia lo termina. No hay llaves, corchetes ni
palabras clave explícitas. Esto quiere decir que el espacio en blanco es
significativo y debe ser consistente. En este ejemplo el código de la función
(incluida la cadena de documentación) está sangrado a cuatro espacios. No
tienen por qué ser cuatro, el único requisito es que sea consistente. La primera
línea que no esté sangrada queda ya fuera de la función.
def fib(n):
print 'n =', n
if n > 1:
return n * fib(n - 1)
else:
print 'fin de la línea'
return 1
Por supuesto, los bloques if y else pueden contener varias líneas siempre
que mantengan un sangrado consistente. Este bloque else tiene dos líneas de
código dentro. No hay una sintaxis especial para bloques de código de varias
líneas. Simplemente indéntelas y siga con su vida.
Python utiliza retornos de carro para separar sentencias y los dos puntos y
el sangrado para reconocer bloques de código. C++ y Java usan puntos y
coma para separar sentencias, y llaves para indicar bloques de código.
Los módulos de Python son objetos y tienen varios atributos útiles. Puede usar
este hecho para probar sus módulos de forma sencilla a medida que los escribe.
Aquí tiene un ejemplo que usa el truco de if __name__.
if __name__ == "__main__":
Sabiendo esto, puede diseñar una batería de pruebas para su módulo dentro del
propio módulo situándola dentro de esta sentencia if. Cuando ejecuta el
módulo directamente, __name__ es __main__, de manera que se ejecutan las
pruebas. Cuando importa el módulo, __name__ es otra cosa, de manera que se
ignoran las pruebas. Esto hace más sencillo desarrollar y depurar nuevos
módulos antes de integrarlos en un programa mayor.
En MacPython, hay que dar un paso adicional para hacer que funcione el
truco if __name__. Muestre el menú de opciones pulsando el triángulo
negro en la esquina superior derecha de la ventana, y asegúrese de que está
marcado Run as __main__.
'mpilgrim'.
'database' es una clave, y su valor asociado, referenciado por
d["database"], es 'master'.
Puede obtener los valores por su clave pero no las claves por su valor. De
manera que d["server"] es 'mpilgrim', pero d["mpilgrim"] genera una
excepción, porque 'mpilgrim' no es una clave.
>>> d
{'server': 'mpilgrim', 'database': 'master'}
>>> d["database"] = "pubs"
>>> d
{'server': 'mpilgrim', 'database': 'pubs'}
>>> d["uid"] = "sa"
>>> d
{'server': 'mpilgrim', 'uid': 'sa', 'database': 'pubs'}
Advierta que el nuevo elemento (clave 'uid', valor 'sa') aparece en el medio.
En realidad es sólo coincidencia que los elementos apareciesen en orden en el
primer ejemplo; también es coincidencia que ahora aparezcan en desorden.
>>> d = {}
>>> d["clave"] = "valor"
>>> d["clave"] = "otro valor"
>>> d
{'clave': 'otro valor'}
>>> d["Clave"] = "tercer valor"
>>> d
{'Clave': 'tercer valor', 'clave': 'otro valor'}
Esto no asigna un valor a una clave existente, porque las cadenas en Python
distinguen las mayúsculas, de manera que 'clave' no es lo mismo que
'Clave'. Esto crea un nuevo par clave/valor en el diccionario; puede
>>> d
{'server': 'mpilgrim', 'uid': 'sa', 'database': 'pubs'}
>>> d["retrycount"] = 3
>>> d
{'server': 'mpilgrim', 'uid': 'sa', 'database': 'master',
'retrycount': 3}
>>> d[42] = "douglas"
>>> d
{'server': 'mpilgrim', 'uid': 'sa', 'database': 'master',
42: 'douglas', 'retrycount': 3}
Los diccionarios no son sólo para las cadenas. Los valores de los diccionarios
pueden ser cualquier tipo de dato, incluyendo cadenas, enteros, objetos o
incluso otros diccionarios. Y dentro de un mismo diccionario los valores no
tienen por qué ser del mismo tipo: puede mezclarlos según necesite.
Las claves de los diccionarios están más restringidas, pero pueden ser
cadenas, enteros y unos pocos tipos más. También puede mezclar los tipos de
claves dentro de un diccionario.
>>> d
{'server': 'mpilgrim', 'uid': 'sa', 'database': 'master',
42: 'douglas', 'retrycount': 3}
>>> del d[42]
>>> d
{'server': 'mpilgrim', 'uid': 'sa', 'database': 'master',
'retrycount': 3}
>>> d.clear()
>>> d
{}
clear elimina todos los elementos de un diccionario. Observe que unas llaves
Footnotes
[1] N. del T.: En inglés se dice que es case sensible, que en castellano se traduce
como sensible a la caja. "Caja", en el ámbito de la tipografía, se usa
tradicionalmente para definir si una letra es mayúscula (caja alta) o minúscula
(caja baja), en referencia al lugar en que se almacenaban los tipos móviles en las
imprentas antiguas. En el resto del libro diré simplemente que se "distinguen
las mayúsculas" (o no)
Las listas son el caballo de tiro de Python. Si su única experiencia con listas son
los array de Visual Basic o (dios no lo quiera) los datastore de Powerbuilder,
prepárese para las listas de Python.
Una lista de Python es como un array en Perl. En Perl, las variables que
almacenan arrays siempre empiezan con el carácter @; en Python, las
variables se pueden llamar de cualquier manera, y Python se ocupa de
saber el tipo que tienen.
Una lista en Python es mucho más que un array en Java (aunque puede
usarse como uno si es realmente eso todo lo que quiere en esta vida). Una
mejor analogía podría ser la clase ArrayList, que puede contener objetos
arbitrarios y expandirse de forma dinámica según se añaden otros nuevos.
Una lista se puede usar igual que un array basado en cero. El primer
elemento de cualquier lista que no esté vacía es siempre li[0].
El último elemento de esta lista de cinco elementos es li[4], porque las listas
siempre empiezan en cero.
>>> li
['a', 'b', 'mpilgrim', 'z', 'example']
>>> li[-1]
'example'
>>> li[-3]
'mpilgrim'
li[2].
Ejemplo 3.8. Slicing de una lista
>>> li
['a', 'b', 'mpilgrim', 'z', 'example']
>>> li[1:3]
['b', 'mpilgrim']
>>> li[1:-1]
['b', 'mpilgrim', 'z']
>>> li[0:3]
['a', 'b', 'mpilgrim']
Las listas empiezan en cero, así que li[0:3] devuelve los tres primeros
elementos de la lista, empezando en li[0], y hasta li[3], pero sin incluirlo.
>>> li
['a', 'b', 'mpilgrim', 'z', 'example']
>>> li[:3]
['a', 'b', 'mpilgrim']
>>> li[3:]
['z', 'example']
>>> li[:]
['a', 'b', 'mpilgrim', 'z', 'example']
>>> li
['a', 'b', 'mpilgrim', 'z', 'example']
>>> li.append("new")
>>> li
['a', 'b', 'mpilgrim', 'z', 'example', 'new']
>>> li.insert(2, "new")
>>> li
['a', 'b', 'new', 'mpilgrim', 'z', 'example', 'new']
>>> li.extend(["two", "elements"])
>>> li
['a', 'b', 'new', 'mpilgrim', 'z', 'example', 'new', 'two',
'elements']
índice del primer elemento que cambia de posición. Observe que los
elementos de la lista no tienen por qué ser únicos; ahora hay dos elementos
con el valor 'new', li[2] y li[6].
extend concatena listas. Verá que no se llama a extend con varios
argumentos; se le llama con uno, una lista. En este caso, esa lista tiene dos
elementos.
Las listas tienen dos métodos, extend y append, que parecen hacer lo mismo,
pero en realidad son completamente diferentes. extend toma un único
argumento, que es siempre una lista, y añade cada uno de los elementos de
esa lista a la original.
Por otro lado, append toma un argumento, que puede ser cualquier tipo de
dato, y simplemente lo añade al final de la lista. Aquí, estamos llamado al
método append con un único argumento, que es una lista de tres elementos.
Ahora la lista original, que empezó siendo una lista de tres elementos,
contiene cuatro. ¿Por qué cuatro? Porque el último elemento que acabamos
de añadir es una lista. Las listas contienen cualquier tipo de dato, incluyendo
otras listas. Puede que esto es lo que usted quiere, puede que no. No use
append si lo que quiere hacer es extend.
>>> li
['a', 'b', 'new', 'mpilgrim', 'z', 'example', 'new', 'two',
'elements']
>>> li.index("example")
5
>>> li.index("new")
2
>>> li.index("c")
Traceback (innermost last):
File "<interactive input>", line 1, in ?
ValueError: list.index(x): x not in list
>>> "c" in li
False
índice.
'new' aparece dos veces en la lista, en li[2] y li[6], pero index devolverá
Para probar si un valor está en la lista, utilice in, que devuelve True si el
valor existe o False si no.
>>> li
['a', 'b', 'new', 'mpilgrim', 'z', 'example', 'new', 'two',
'elements']
>>> li.remove("z")
>>> li
['a', 'b', 'new', 'mpilgrim', 'example', 'new', 'two', 'elements']
>>> li.remove("new")
>>> li
['a', 'b', 'mpilgrim', 'example', 'new', 'two', 'elements']
>>> li.remove("c")
Traceback (innermost last):
File "<interactive input>", line 1, in ?
ValueError: list.remove(x): x not in list
>>> li.pop()
'elements'
>>> li
['a', 'b', 'mpilgrim', 'example', 'new', 'two']
pop es una bestia interesante. Hace dos cosas: elimina el último elemento de
operador + devuelve una nueva lista (concatenada) como valor, mientras que
extend sólo altera una existente. Esto significa que extend es más rápido,
• How to Think Like a Computer Scientist le instruye sobre listas y señala algo
importante al respecto de pasar listas como argumentos a funciones.
• El Tutorial de Python muestra cómo usar listas como pilas y colas.
• La Python Knowledge Base contesta preguntas frecuentes sobre listas y
tiene un montón de código de ejemplo que usa listas.
• La Referencia de bibliotecas de Python enumera todos los métodos de las
listas.
Footnotes
Una tupla es una lista inmutable. Una tupla no puede cambiar de ninguna
manera una vez creada.
Una tupla se define de la misma manera que una lista, excepto que el
conjunto de elementos se encierra entre paréntesis en lugar de corchetes.
Los elementos de una tupla tienen un orden definido, como una lista. Los
índices de las tuplas comienzan en cero, igual que una lista, de manera que el
primer elemento de una tupla que no esté vacía siempre es t[0].
Los índices negativos cuentan desde el final de la tupla, just como en una
lista.
También funciona el slicing. Observe que cuando trocea una lista, obtiene una
nueva lista; cuando trocea una tupla, obtiene una nueva tupla.
>>> t
('a', 'b', 'mpilgrim', 'z', 'example')
>>> t.append("new")
Traceback (innermost last):
File "<interactive input>", line 1, in ?
AttributeError: 'tuple' object has no attribute 'append'
>>> t.remove("z")
Traceback (innermost last):
File "<interactive input>", line 1, in ?
AttributeError: 'tuple' object has no attribute 'remove'
>>> t.index("example")
Traceback (innermost last):
File "<interactive input>", line 1, in ?
AttributeError: 'tuple' object has no attribute 'index'
>>> "z" in t
True
No puede añadir métodos a una tupla. Las tuplas no tienen métodos append
o extend.
No puede buscar elementos en una tupla. Las tuplas no tienen método index.
• How to Think Like a Computer Scientist instruye sobre las tuplas y muestra
cómo concatenarlas.
• La Python Knowledge Base muestra cómo ordenar una tupla.
• El Tutorial de Python muestra cómo definir una tupla con un elemento.
Ahora que ya sabe algo sobre diccionarios, tuplas y listas (¡oh dios mío!),
volvamos al programa de ejemplo de Capítulo 2, odbchelper.py.
Python tiene variables locales y globales como casi todo el resto de lenguajes,
pero no tiene declaración explícitade variables. Las variables cobran existencia
al asignársele un valor, y se destruyen automáticamente al salir de su ámbito.
if __name__ == "__main__":
myParams = {"server":"mpilgrim", \
"database":"master", \
"uid":"sa", \
"pwd":"secret" \
}
>>> x
Traceback (innermost last):
File "<interactive input>", line 1, in ?
NameError: There is no variable named 'x'
>>> x = 1
>>> x
1
Uno de los atajos más molones de Python es el uso de secuencias para asignar
múltiples valores a la vez.
Asignar una a la otra provoca que cada uno de los valores de v se asigne a las
variables correspondientes, en orden.
Esto tiene todo tipo de usos. A menudo quiero asignar nombres a un rango de
valores. En C usaríamos enum y listaríamos de forma manual cada constante y
sus valores asociados, lo que parece especialmente tedioso cuando los valores
son consecutivos. En Python, podemos usar la función incorporada range con la
asignación a múltiples variables para asignar valores consecutivos rápidamente.
>>> range(7)
[0, 1, 2, 3, 4, 5, 6]
>>> (LUNES, MARTES, MIERCOLES, JUEVES, VIENRES, SABADO, DOMINGO) =
range(7)
>>> LUNES
0
>>> MARTES
1
>>> DOMINGO
6
variables que estamos definiendo. (Este ejemplo viene del módulo calendar
un módulo pequeño y simpático que imprime calendarios, como el programa
cal de UNIX. El módulo de calendario define constntes enteras para los días
de la semana).
Python admite dar formato a valores dentro de cadenas. Aunque esto puede
incluir expresiones muy complicadas, el uso más básico es insertar valores
dentro de una cadena con el sustituto %s.
>>> k = "uid"
>>> v = "sa"
>>> "%s=%s" % (k, v)
'uid=sa'
Observe que (k, v) es una tupla. Ya le dije que servían para algo.
Puede que esté pensando que esto es mucho trabajo sólo para hacer una simple
concatenación, y estaría en lo correcto, excepto porque el formato de cadenas no
es sólo concatenación. Ni siquiera es sólo formato. También trata sobre
conversión de tipos.
extraña, pero hay una buena razón para ella: es una tupla sin ambigüedades.
De hecho, siempre puede incluir una coma tras el último elemento cuando
define una lista, tupla o diccionario, pero se precisa la coma cuando se define
una tupla con un solo elemento. Si no se precisase la coma, Python no podría
saber si (userCount) es una tupla de un elemento, o sólo el valor de
userCount.
>>> li = [1, 9, 8, 4]
>>> [elem*2 for elem in li]
[2, 18, 16, 8]
>>> li
[1, 9, 8, 4]
>>> li = [elem*2 for elem in li]
>>> li
[2, 18, 16, 8]
Para que esto tenga sentido, léalo de derecha a izquierda. li es la lista que
está inyectando. Python itera sobre li elemento a elemento, asignando
temporalmente el valor de cada elemento a la variable elem. Python aplica
entonces la función elem*2 y añade el resultado a la lista que ha de devolver.
diccionario.
El método values devuelve una lista de todos los valores. La lista está en el
mismo orden que la devuelta por keys, de manera que params.values()[n]
== params[params.keys()[n]] para todos los valores de n.
Observe que estamos usando dos variables para iterar sobre la lista
params.items(). Éste es otro uso de la asignación multivariable. El primer
Aquí hacemos lo mismo, pero ignoramos el valor de k, así que la lista acaba
siendo equivalente a params.values().
Combinar los dos ejemplos anteriores con formato de cadenas sencillo, nos
da una lista de cadenas que incluye tanto la clave como el valor de cada
elemento del diccionario. Esto se parece sospechosamente a la salida del
programa. Todo lo que nos queda es juntar los elementos de esta lista en una
sola cadena.
Una nota interesante antes de continuar. He repetido que las funciones son
objetos, las cadenas son objetos... todo es un objeto. Podría estar pensando que
quiero decir que las variables de cadena son objetos. Pero no, mire atentamente a
este ejemplo y verá que la cadena ";" en sí es un objeto, y está llamando a su
método join.
El método join une los elementos de la lista en una única cadena, separado
cada uno por un punto y coma. El delimitador no tiene por qué ser un punto y
coma; no tiene siquiera por qué ser un único carácter. Puede ser cualquier
cadena.
conversión de tipos. Juntar una lista que tenga uno o más elementos que no
sean cadenas provocará una excepción.
split deshace el trabajo de join dividiendo una cadena en una lista de varios
subcadena dentro de una cadena, y trabajar después con todo lo que hay
antes de esa subcadena (que es el primer elemento de la lista devuelta) y
todo lo que hay detrás (el segundo elemento).
Cuando aprendí Python, esperaba que join fuese un método de una lista, que
tomaría delimitadores como argumento. Mucha gente piensa lo mismo, pero
hay toda una historia tras el método join. Antes de Python 1.6, las cadenas no
tenían todos estos métodos tan útiles. Había un módulo string aparte que
contenía todas las funciones de cadenas; cada función tomaba una cadena como
primer argumento. Se consideró que estas funciones eran suficientemente
importantes como para ponerlas en las propias cadenas, lo cual tenía sentido en
funciones como lower, upper, y split. Pero muchos programadores del ala dura
de Python objetaron ante el nuevo método join, argumentando que debería ser
un método de las listas, o que no debería moverse de lugar siquiera, sino seguir
siendo parte exclusiva del módulo string (que aún sigue conteniendo muchas
cosas útiles). Yo uso el nuevo método join de forma exclusiva, pero verá mucho
código escrito de la otra manera, y si realmente le molesta, puede limitarse a
usar la antigua función string.join en su lugar.
3.8. Resumen
def buildConnectionString(params):
"""Crea una cadena de conexión partiendo de un diccionario de
parámetros.
if __name__ == "__main__":
myParams = {"server":"mpilgrim", \
"database":"master", \
"uid":"sa", \
"pwd":"secret" \
}
print buildConnectionString(myParams)
server=mpilgrim;uid=sa;database=master;pwd=secret
Antes de sumergirnos en el siguiente capítulo, asegúrese de que hace las
siguientes cosas con comodidad:
• 4.1. Inmersión
• 4.2. Argumentos opcionales y con nombre
• 4.3. Uso de type, str, dir, y otras funciones incorporadas
o 4.3.1. La función type
o 4.3.2. La función str
o 4.3.3. Funciones incorporadas
• 4.4. Obtención de referencias a objetos con getattr
o 4.4.1. getattr con módulos
o 4.4.2. getattr como dispatcher
• 4.5. Filtrado de listas
• 4.6. La peculiar naturaleza de and y or
o 4.6.1. Uso del truco the and-or
• 4.7. Utilización de las funciones lambda
o 4.7.1. Funciones lambda en el mundo real
• 4.8. Todo junto
• 4.9. Resumen
Este capítulo trata uno de los puntos fuertes de Python: la introspección. Como
usted sabe, todo en Python es un objeto, y la introspección es código que ve
otros módulos y funciones en memoria como objetos, obtiene información sobre
ellos y los manipula. De paso, definiremos funciones sin nombre, llamaremos a
funciones con argumentos sin el orden establecido, y haremos referencia a
funciones cuyos nombres incluso desconocemos.
4.1. Inmersión
if __name__ == "__main__":
print info.__doc__
Este módulo tiene una funcion, info. Según su declaración, admite tres
parámetros: object, spacing y collapse. Los dos últimos son en realidad
parámetros opcionales, como veremos en seguida.
Por omisión, la salida se formatea para que sea de fácil lectura. Las cadenas de
documentación de varias líneas se unen en una sola línea larga, pero esta opción
Returns string.
Python permite que los argumentos de las funciones tengan valores por
omisión; si se llama a la función sin el argumento, éste toma su valor por
omisión. Además, los argumentos pueden especificarse en cualquier orden
indicando su nombre. Los procedimientos almacenados en SQL Server
Transact/SQL pueden hacer esto; si es usted un gurú de los scripts en SQL
Server, puede saltarse esta parte.
Aquí tiene un ejemplo de info, una función con dos argumentos opcionales
Supongamos que desea usted especificar un valor para collapse, pero acepta el
valor por omisión de spacing. En la mayoría de los lenguajes estaría
abandonado a su suerte, pues tendría que invocar a la función con los tres
argumentos. Pero en Python, los argumentos pueden indicarse por su nombre
en cualquier orden.
info(odbchelper)
info(odbchelper, 12)
info(odbchelper, collapse=0)
info(spacing=15, object=odbchelper)
Incluso los argumentos obligatorios (como object, que no tiene valor por
omisión) pueden indicarse por su nombre, y los argumentos así indicados
pueden aparecer en cualquier orden.
Esto sorprende hasta que se advierte que los argumentos simplemente forman
un diccionario. El método “normal” de invocar a funciones sin nombres de
argumentos es realmente un atajo por el que Python empareja los valores con
sus nombres en el orden en que fueron especificados en la declaración de la
función. La mayor parte de las veces llamará usted a las funciones de la forma
“normal”, pero siempre dispone de esta flexibilidad adicional si la necesita.
La función type devuelve el tipo de dato de cualquier objeto. Los tipos posibles
se enumeran en el módulo types. Esto es útil para funciones auxiliares que
pueden manejar distintos tipos de datos.
>>> type(1)
<type 'int'>
>>> li = []
>>> type(li)
<type 'list'>
>>> import odbchelper
>>> type(odbchelper)
<type 'module'>
>>> import types
>>> type(odbchelper) == types.ModuleType
True
type toma cualquier cosa y devuelve su tipo. Y quiero decir cualquier cosa:
Pueden utilizarse las constantes del módulo types para comparar tipos de
objetos. Esto es lo que hace la función info, como veremos en seguida.
>>> str(1)
'1'
>>> horsemen = ['war', 'pestilence', 'famine']
>>> horsemen
['war', 'pestilence', 'famine']
>>> horsemen.append('Powerbuilder')
>>> str(horsemen)
"['war', 'pestilence', 'famine', 'Powerbuilder']"
>>> str(odbchelper)
"<module 'odbchelper' from 'c:\\docbook\\dip\\py\\odbchelper.py'>"
>>> str(None)
'None'
Sin embargo, str funciona con cualquier objeto de cualquier tipo. Aquí
funciona sobre una lista que hemos construido por partes.
str funciona también con módulos. Fíjese que la representación como cadena
del módulo incluye su ruta en el disco, por lo que lo que usted obtenga será
diferente.
En el corazón de nuestra función info está la potente función dir. dir devuelve
una lista de los atributos y métodos de cualquier objeto: módulos, funciones,
cadenas, listas, diccionarios... prácticamente todo.
>>> li = []
>>> dir(li)
['append', 'count', 'extend', 'index', 'insert',
'pop', 'remove', 'reverse', 'sort']
>>> d = {}
>>> dir(d)
['clear', 'copy', 'get', 'has_key', 'items', 'keys', 'setdefault',
'update', 'values']
>>> import odbchelper
>>> dir(odbchelper)
['__builtins__', '__doc__', '__file__', '__name__',
'buildConnectionString']
li es una lista, luego dir(li) devuelve una lista de todos los métodos de una
lista. Advierta que la lista devuelta contiene los nombres de los métodos en
forma de cadenas, no los propios métodos.
d es un diccionario, luego dir(d) devuelve una lista con los nombres de los
Las funciones del módulo string están desaconsejadas (aunque mucha gente
utiliza la función join), pero el módulo contiene muchas constantes útiles
como string.puctuation, que contiene todos los signos habituales de
puntuación.
métodos a los que invocar, pero no podemos hacerlo con la propia cadena).
string.join puede ser invocada; es una función que toma dos argumentos.
[...snip...]
Ya sabe usted que las funciones de Python son objetos. Lo que no sabe es que se
puede obtener una referencia a una función sin necesidad de saber su nombre
hasta el momento de la ejecución, utilizando la función getattr.
Aquí también se devuelve una referencia al método pop, pero esta vez el
nombre del método se especifica como una cadena, argumento de la función
getattr. getattr es una función incorporada increíblemente útil que
En teoría, getattr debería funcionar con tuplas, pero como las tuplas no
tienen métodos, así que getattr lanzará una excepción sea cual sea el
nombre de atributo que se le pida.
getattr no sirve sólo para tipos de datos incorporados. También funciona con
módulos.
cualquier cosa definida en el módulo: una función, una clase o una variable
global.
import statsout
lugar de una función válida, y la siguiente línea que intente llamar a esa función
provocará una excepción. Esto es malo.
Por suerte, getattr toma un tercer argumento opcional, un valor por omisión.
import statsout
Está garantizado que esta llamada a función tendrá éxito, porque hemos
añadido un tercer argumento a la llamada a getattr. El tercer argumento es
un valor por omisión que será devuelto si no se encontrase el atributo o
método especificado por el segundo argumento.
Como ya sabe, Python tiene potentes capacidades para convertir una lista en
otra por medio de las listas por comprensión (Sección 3.6, “Inyección de listas
(mapping)”). Esto puede combinarse con un mecanismo de filtrado en el que se
van a tomar algunos elementos de la lista mientras otros se pasarán por alto.
Esto es una extensión de las listas por comprensión que usted conoce y que
tanto le gustan. Las dos primeras partes son como antes; la última parte, la que
comienza con if, es la expresión de filtrado. Una expresión de filtrado puede
ser cualquier expresión que se evalúe como verdadera o falsa (lo cual, en
Python, puede ser casi cualquier resultado). Cualquier elemento para el cual la
expresión resulte verdadera, será incluido en el proceso de relación. Todos los
demás elementos se pasan por alto, de modo que no entran en el proceso de
relación y no se incluyen en la lista de salida.
Aquí filtramos un valor concreto, b. Observe que esto filtra todas las
apariciones de b, ya que cada vez que aparezca este valor, la expresión de
filtrado será falsa.
valor en una lista. Se podría pensar que este filtro elimina los valores
duplicados en una lista, devolviendo otra que contiene una única copia de
cada valor de la lista original. Pero no es así, porque los valores que aparecen
dos veces en la lista original (en este caso, b y d) son completamente
excluidos. Hay modos de eliminar valores duplicados en una lista, pero el
filtrado no es la solución.
La expresión de filtrado parece que asusta, pero no. Usted ya conoce callable,
getattr, e in. Como se vio en la sección anterior, la expresión getattr(object,
Por tanto esta expresión toma un objeto (llamado object). Obtiene la lista de
sus atributos, métodos, funciones y algunas cosas más. A continuación filtra
esta lista para eliminar todo lo que no nos interesa. Esto lo hacemos tomando el
nombre de cada atributo/método/función y obteniendo una referencia al
objeto real por medio de la función getattr. Después comprobamos si ese
objeto puede ser invocado, que serán los métodos y funciones, tanto
incorporados (como el método pop de una lista) como definidos por el usuario
(como la función buildConnectionString del módulo odbchelper). No nos
interesan otros atributos, como el atributo __name__ que está incorporado en
todos los módulos.
Todos los valores son verdaderos, luego and devuelve el último valor, 'c'.
or evalúa '', que es falsa, después 'b', que es verdadera, y devuelve 'b'.
Si todos los valores son falsos, or devuelve el último. or evalúa '', que es
falsa, después [], que es falsa, después {}, que es falsa, y devuelve {}.
Advierta que or sólo evalúa valores hasta que encuentra uno verdadero en
contexto booleano, y entonces omite el resto. Esta distinción es importante si
algunos valores tienen efectos laterales. Aquí no se invoca nunca a la función
sidefx, porque or evalúa u'a', que es verdadera, y devuelve 'a'
inmediatamente.
>>> a = "first"
>>> b = "second"
>>> 1 and a or b
'first'
>>> 0 and a or b
'second'
resultado 'second'.
>>> a = ""
>>> b = "second"
>>> 1 and a or b
'second'
>>> a = ""
>>> b = "second"
>>> (1 and [a] or [b])[0]
''
Dado que [a] es una lista no vacía, nunca es falsa. Incluso si a es 0 o '' o
cualquier otro valor falso, la lista [a] es verdadera porque tiene un elemento.
Hasta aquí, puede parecer que este truco tiene más inconvenientes que ventajas.
Después de todo, se podría conseguir el mismo efecto con una sentencia if, así
que ¿por qué meterse en este follón? Bien, en muchos casos, se elige entre dos
valores constantes, luego se puede utilizar la sintaxis más simple sin
preocuparse, porque se sabe que el valor de a será siempre verdadero. E incluso
si hay que usar la forma más compleja, hay buenas razones para ello; en
algunos casos no se permite la sentencia if, por ejemplo en las funciones
lambda.
Python admite una interesante sintaxis que permite definir funciones mínimas,
de una línea, sobre la marcha. Tomada de Lisp, se trata de las denominadas
funciones lambda, que pueden utilizarse en cualquier lugar donde se necesite
una función.
Ejemplo 4.20. Presentación de las funciones lambda
Ésta es una función lambda que consigue el mismo efecto que la función
anterior. Advierta aquí la sintaxis abreviada: la lista de argumentos no está
entre paréntesis, y falta la palabra reservada return (está implícita, ya que la
función entera debe ser una única expresión). Igualmente, la función no tiene
nombre, pero puede ser llamada mediante la variable a que se ha asignado.
Se puede utilizar una función lambda incluso sin asignarla a una variable. No
es lo más útil del mundo, pero sirve para mostrar que una lambda es sólo una
función en línea.
En general, una función lambda es una función que toma cualquier número de
argumentos (incluso argumentos opcionales) y devuelve el valor de una
expresión simple. Las funciones lambda no pueden contener órdenes, y no
pueden contener tampoco más de una expresión. No intente exprimir
demasiado una función lambda; si necesita algo más complejo, defina en su
lugar una función normal y hágala tan grande como quiera.
Las funciones lambda son una cuestión de estilo. Su uso nunca es necesario.
En cualquier sitio en que puedan utilizarse, se puede definir una función
normal separada y utilizarla en su lugar. Yo las utilizo en lugares donde
deseo encapsulación, código no reutilizable que no ensucie mi propio
código con un montón de pequeñas funciones de una sola línea.
4.7.1. Funciones lambda en el mundo real
Observe que aquí usamos la forma simple del truco and-or, lo cual está bien,
porque una función lambda siempre es verdadera en un contexto booleano (esto
no significa que una función lambda no pueda devolver un valor falso; la
función es siempre verdadera; su valor de retorno puede ser cualquier cosa).
split sin argumentos divide en los espacios en blanco. De modo que tres
processFunc es ahora una función, pero qué función sea depende del valor de la
Para hacer esto con un lenguaje menos robusto, como Visual Basic,
probablemente crearía usted una función que tomara una cadena y un
argumento collapse utilizando una sentencia if para decidir si unificar o no el
espacio en blanco, para devolver después el valor apropiado. Esto sería ineficaz,
porque la función tendría que considerar todos los casos posibles; cada vez que
se le llamara, tendría que decidir si unificar o no el espacio en blanco antes de
devolver el valor deseado. En Python, puede tomarse esta decisión fuera de la
función y definir una función lambda adaptada para devolver exactamente (y
únicamente) lo que se busca. Esto es más eficaz, más elegante, y menos
propenso a esos errores del tipo “Uy, creía que esos argumentos estaban al
revés”.
El meollo de apihelper.py:
Advierta que esto es una sola orden, dividida en varias líneas, pero sin utilizar
el carácter de continuación de línea (“\”). ¿Recuerda cuando dije que algunas
expresiones pueden dividirse en varias líneas sin usar la barra inversa? Una
lista por comprensión es una de estas expresiones, ya que la expresión completa
está entre corchetes.
nos muestra que esto es una lista por comprensión. Como ya sabe, methodList
es una lista de todos los métodos que nos interesan de object. De modo que
estamos recorriendo esta lista con la variable method.
Ejemplo 4.23. ¿Por qué usar str con una cadena de documentación?
en blanco. Ahora se ve por qué es importante utilizar str para convertir el valor
None en su representación como cadena. processFunc asume que su argumento
Yendo más atrás, verá que estamos utilizando las cadenas de formato de nuevo
para concatenar el valor de retorno de processFunc con el valor de retorno del
método ljust de method. Éste es un nuevo método de cadena que no hemos
visto antes.
>>> s = 'buildConnectionString'
>>> s.ljust(30)
'buildConnectionString '
>>> s.ljust(20)
'buildConnectionString'
ljust rellena la cadena con espacios hasta la longitud indicada. Esto lo usa la
función info para hacer dos columnas y alinear todas las cadenas de
documentación en la segunda columna.
Casi hemos terminado. Dados el nombre del método relleno con espacios con el
método ljust y la cadena de documentación (posiblemente con el espacio
blanco unificado), que resultó de la llamada a processFunc, concatenamos las
dos y obtenemos una única cadena. Como estamos recorriendo methodList,
terminamos con una lista de cadenas. Utilizando el método join de la cadena
"\n", unimos esta lista en una única cadena, con cada elemento de la lista en
Ésta es la última pieza del puzzle. Este código debería ahora entenderse
perfectamente.
4.9. Resumen
if __name__ == "__main__":
print info.__doc__
• 5.1. Inmersión
• 5.2. Importar módulos usando from módulo import
• 5.3. Definición de clases
o 5.3.1. Inicialización y programación de clases
o 5.3.2. Saber cuándo usar self e __init__
• 5.4. Instanciación de clases
o 5.4.1. Recolección de basura
• 5.5. Exploración de UserDict: Una clase cápsula
• 5.6. Métodos de clase especiales
o 5.6.1. Consultar y modificar elementos
• 5.7. Métodos especiales avanzados
• 5.8. Presentación de los atributos de clase
• 5.9. Funciones privadas
• 5.10. Resumen
Este capítulo, y básicamente todos los que le siguen trabajan con programación
en Python orientada a objetos.
5.1. Inmersión
def stripnulls(data):
"strip whitespace and nulls"
return data.replace("\00", "").strip()
class FileInfo(UserDict):
"store file metadata"
def __init__(self, filename=None):
UserDict.__init__(self)
self["name"] = filename
class MP3FileInfo(FileInfo):
"store ID3v1.0 MP3 tags"
tagDataMap = {"title" : ( 3, 33, stripnulls),
"artist" : ( 33, 63, stripnulls),
"album" : ( 63, 93, stripnulls),
"year" : ( 93, 97, stripnulls),
"comment" : ( 97, 126, stripnulls),
"genre" : (127, 128, ord)}
if __name__ == "__main__":
for info in listDirectory("/music/_singles/", [".mp3"]):
print "\n".join(["%s=%s" % (k, v) for k, v in info.items()])
print
album=
artist=Ghost in the Machine
title=A Time Long Forgotten (Concept
genre=31
name=/music/_singles/a_time_long_forgotten_con.mp3
year=1999
comment=http://mp3.com/ghostmachine
album=Rave Mix
artist=***DJ MARY-JANE***
title=HELLRAISER****Trance from Hell
genre=31
name=/music/_singles/hellraiser.mp3
year=2000
comment=http://mp3.com/DJMARYJANE
album=Rave Mix
artist=***DJ MARY-JANE***
title=KAIRO****THE BEST GOA
genre=31
name=/music/_singles/kairo.mp3
year=2000
comment=http://mp3.com/DJMARYJANE
album=Journeys
artist=Masters of Balance
title=Long Way Home
genre=31
name=/music/_singles/long_way_home1.mp3
year=2000
comment=http://mp3.com/MastersofBalan
album=
artist=The Cynic Project
title=Sidewinder
genre=18
name=/music/_singles/sidewinder.mp3
year=2000
comment=http://mp3.com/cynicproject
album=Digitosis@128k
artist=VXpanded
title=Spinning
genre=255
name=/music/_singles/spinning.mp3
year=2000
comment=http://mp3.com/artists/95/vxp
Python tiene dos maneras de importar módulos. Ambas son útiles, y debe saber
cuándo usar cada cual. Una de las maneras, import módulo, ya la hemos visto
en Sección 2.4, “Todo es un objeto”. La otra hace lo mismo, pero tiene
diferencias sutiles e importantes.
Esto es similar a la sintaxis de import módulo que conoce y adora, pero con una
diferencia importante: los atributos y métodos de lo módulo importado types
se sitúan directamente en el espacio de nombres local, de manera que están
disponibles directamente, sin necesidad de acceder a ellos mediante el nombre
del módulo. Puede importar elementos individuales o usar from módulo import
* para importarlo todo.
El módulo types no contiene métodos; sino sólo atributos para cada tipo de
objeto en Python. Observe que el atributo, FunctionType, debe cualificarse
usando el nombre del módulo, types.
el contexto de types.
• eff-bot tiene más cosas que decir sobre import módulo frente a from
módulo import.
Definir una clase en Python es simple. Como con las funciones, no hay una
definición interfaz separada. Simplemente defina la clase y empiece a escribir
código. Una clase de Python empieza con la palabra reservada class, seguida
de su nombre. Técnicamente, esto es todo lo que necesita, ya que la clase no
tiene por qué heredar de ninguna otra.
class Loaf:
pass
Esta clase no define métodos ni atributos, pero sintácticamente, hace falta que
exista alguna cosa en la definición, de manera que usamos pass. Ésta es una
palabra reservada de Python que simplemente significa “múevanse, nada
que ver aquí”. Es una sentencia que no hace nada, y es un buen sustituto de
funciones o clases modelo, que no hacen nada.
class FileInfo(UserDict):
poco de magia negra tras todo esto, que demistificaré más adelante en este
capítulo cuando exploremos la clase UserDict en más profundidad.
class FileInfo(UserDict):
"store file metadata"
def __init__(self, filename=None):
class FileInfo(UserDict):
"store file metadata"
def __init__(self, filename=None):
UserDict.__init__(self)
self["name"] = filename
Cuando defina los métodos de su clase, debe enumerar self de forma explícita
como el primer argumento de cada método, incluido __init__. Cuando llame a
un método de una clase ancestra desde dentro de la clase, debe incluir el
argumento self. Pero cuando invoque al método de su clase desde fuera, no
debe especificar nada para el argumento self; evítelo completamente, y Python
añadirá automáticamente la referencia a la instancia. Soy consciente de que esto
es confuso al principio; no es realmente inconsistente, pero puede parecer
inconsistente debido a que se basa en una distinción (entre métodos bound y
unbound) que aún no conoce.
¡Vaya! Me doy cuenta de que es mucho por absorber, pero le acabará pillando el
truco. Todas las clases de Python funcionan de la misma manera, de manera
que cuando aprenda una, las habrá aprendido todas. Si se olvida de algo,
recuerde esto, porque prometo que se lo tropezará:
Los métodos __init__ son opcionales, pero cuando define uno, debe
recordar llamar explícitamente al método __init__ del ancestro (si define
uno). Suele ocurrir que siempre que un descendiente quiera extender el
comportamiento de un ancestro, el método descendiente deba llamar al del
ancestro en el momento adecuado, con los argumentos adecuados.
Cada vez que se invoca la función leakmem, se crea una instancia de FileInfo,
asignándola a la variable f, que es local a la función. Entonces la función
termina sin liberar f, de manera que esperaríamos tener aquí una fuga de
memoria (memory leak), pero estaríamos equivocados. Cuando termina la
función, se dice que la variable f sale de ámbito. En este momento, ya no hay
referencias a la instancia recién creada de FileInfo (ya que no la hemos
asignado a ninguna otra cosa aparte de f), así que Python la destruye por
nosotros.
Como ha podido ver, FileInfo es una clase que actúa como un diccionario.
Para explorar esto un poco más, veamos la clase UserDict del módulo UserDict,
que es el ancestro de la clase FileInfo . No es nada especial; la clase está escrita
en Python y almacenada en un fichero .py, igual que cualquier otro código
Python. En particular, está almacenada en el directorio lib de su instalación de
Python.
class UserDict:
def __init__(self, dict=None):
self.data = {}
if dict is not None: self.update(dict)
Observe que UserDict es una clase base, que no hereda de ninguna otra.
Ésta es una sintaxis que puede no haber visto antes (no la he usado en los
ejemplos de este libro). Hay una sentencia if, pero en lugar de un bloque
sangrado en la siguiente líena, la sentencia está en una sola línea, tras los dos
puntos. Esta sintaxis es perfectamente válida, y es sólo un atajo que puede
usar cuando el bloque conste de sólo una sentencia (es como especificar una
sola sentencia sin llaves en C++). Puede usar esta sintaxis, o sangrar el código
en las siguientes líneas, pero no puede hacer ambas cosas en el mismo
bloque.
fileinfo_fromdict.py.
class FileInfo(dict):
"store file metadata"
def __init__(self, filename=None):
self["name"] = filename
inicialización explícita.
Como pudo ver en la sección anterior, los métodos normales van mucho más
allá de simplemente actuar como cápsula de un diccionario en una clase. Pero
los métodos normales por sí solos no son suficientes, porque hay muchas cosas
que puede hacer con diccionarios aparte de llamar a sus métodos. Para
empezar, en lugar de usar get y set para trabajar con los elmentos, puede
hacerlo con una sintaxis que no incluye una invocación explícita a métodos.
Aquí es donde entran los métodos especiales de clase: proporcionan una
manera de convertir la sintaxis-que-no-llama-a-métodos en llamadas a
métodos.
>>> f = fileinfo.FileInfo("/music/_singles/kairo.mp3")
>>> f
{'name':'/music/_singles/kairo.mp3'}
>>> f.__getitem__("name")
'/music/_singles/kairo.mp3'
>>> f["name"]
'/music/_singles/kairo.mp3'
puede invocarlo usted, sino que puede hacer que Python lo invoque usando
la sintaxis adecuada.
>>> f
{'name':'/music/_singles/kairo.mp3'}
>>> f
{'name':'/music/_singles/kairo.mp3', 'genre':31}
>>> f["genre"] = 32
>>> f
{'name':'/music/_singles/kairo.mp3', 'genre':32}
Igual que con el método __getitem__, __setitem__ simplemente delega en el
diccionario real self.data para hacer su trabajo. E igual que __getitem__,
normalmente no lo invocará de forma ordinaria de esta manera; Python
llama a __setitem__ cuando usa la sintaxis correcta.
pero sigue siendo un método de clase. Con la misma facilidad con que definió
el método __setitem__ en UserDict, puede redefinirlo en la clase descendiente
para reemplezar el método del ancestro. Esto le permite definir clases que
actúan como diccionarios de cierta manera pero con su propio comportamiento
más allá del de un diccionario estándar.
claves.
A estas alturas, puede estar pensando, “Todo este trabajo sólo para hacer algo
en una clase que podría hacer con un tipo de datos incorporado”. Y es cierto
que la vida sería más fácil (e innecesaria toda la clase UserDict) si pudiéramos
heredar de tipos de datos incorporados como un diccionario. Pero incluso
aunque pudiera, los métodos especiales seguirían siendo útiles, porque se
pueden usar en cualquier clase, no sólo en las que envuelven a otras como
UserDict.
Los métodos especiales implican que cualquier clase puede almacenar pares
clave/valor como un diccionario. Cualquier clase puede actuar como una
secuencia, simplemente definiendo el método __getitem__. Cualquier clase que
defina el método __cmp__ puede compararse con ==. Y si su clase representa
algo que tenga una longitud, no defina un método GetLength; defina el método
__len__ y use len(instancia).
Mientras que otros lenguajes orientados a objeto sólo le permitirán definir
el modelo físico de un objeto (“este objeto tiene un método GetLength”), los
métodos especiales de Python como __len__ le permiten definir el modelo
lógico de un objeto (“este objeto tiene una longitud”).
Python tiene muchos otros métodos especiales. Hay todo un conjunto de ellos
para hacer que las clases actúen como números, permitiéndole sumar, sustraer
y otras operaciones aritméticas sobre instancias de clases (el ejemplo canónico
es la clase que representa a los números complejos, con componentes real e
imaginario). El método __call__ permite que una clase actúe como una
función, permitiéndole invocar directamente a una instancia de la clase. Y hay
otros métodos especiales que permiten a las clases tener atributos de datos de
sólo lectura o sólo escritura; hablaremos más sobre ellos en otros capítulos.
Ya conoce los atributos de datos, que son variables que pertenecen a una
instancia específica de una clase. Python también admite atributos de clase, que
son variables que pertenecen a la clase en sí.
class MP3FileInfo(FileInfo):
"store ID3v1.0 MP3 tags"
tagDataMap = {"title" : ( 3, 33, stripnulls),
"artist" : ( 33, 63, stripnulls),
"album" : ( 63, 93, stripnulls),
"year" : ( 93, 97, stripnulls),
"comment" : ( 97, 126, stripnulls),
"genre" : (127, 128, ord)}
>>> import fileinfo
>>> fileinfo.MP3FileInfo
<class fileinfo.MP3FileInfo at 01257FDC>
>>> fileinfo.MP3FileInfo.tagDataMap
{'title': (3, 33, <function stripnulls at 0260C8D4>),
'genre': (127, 128, <built-in function ord>),
'artist': (33, 63, <function stripnulls at 0260C8D4>),
'year': (93, 97, <function stripnulls at 0260C8D4>),
'comment': (97, 126, <function stripnulls at 0260C8D4>),
'album': (63, 93, <function stripnulls at 0260C8D4>)}
>>> m = fileinfo.MP3FileInfo()
>>> m.tagDataMap
{'title': (3, 33, <function stripnulls at 0260C8D4>),
'genre': (127, 128, <built-in function ord>),
'artist': (33, 63, <function stripnulls at 0260C8D4>),
'year': (93, 97, <function stripnulls at 0260C8D4>),
'comment': (97, 126, <function stripnulls at 0260C8D4>),
'album': (63, 93, <function stripnulls at 0260C8D4>)}
Los atributos de clase se pueden usar como constantes de la clase (que es para
lo que las usamos en MP3FileInfo), pero no son constantes realmente. También
puede cambiarlas.
No hay constantes en Python. Todo puede cambiar si lo intenta con ahínco.
Esto se ajusta a uno de los principios básicos de Python: los
comportamientos inadecuados sólo deben desaconsejarse, no prohibirse. Si
en realidad quiere cambiar el valor de None, puede hacerlo, pero no venga
luego llorando si es imposible depurar su código.
clase). Es una referencia a la clase de la que es instancia self (en este caso, la
clase counter).
5.10. Resumen
Esto es todo lo en cuanto a trucos místicos. Verá una aplicación en el mundo
real de métodos especiales de clase en Capítulo 12, que usa getattr para crear
un proxy a un servicio remoto por web.
Una excepción no tiene por qué resultar en un completo desastre, sin embargo.
Las excepciones, tras ser lanzadas, pueden ser controladas. Algunas veces
ocurren excepciones debido a fallos en el programa (como acceder a una
variable que no existe), pero a menudo, sucede por algo que podemos anticipar.
Si abrimos un fichero, puede ser que no exista. Si conectamos a una base de
datos, puede que no esté disponible, o quizá no introdujimos las credenciales de
seguridad correctas al acceder. Si sabe qué línea de código lanzará la excepción,
podría gestionarla utilizando un bloque try...except.
Hay muchos otros usos para las excepciones aparte de controlar verdaderas
condiciones de error. Un uso común en la biblioteca estándar de Python es
intentar importar un módulo, y comprobar si funcionó. Importar un módulo
que no existe lanzará una excepción ImportError. Puede usar esto para definir
varios niveles de funcionalidad basándose en qué módulos están disponibles en
tiempo de ejecución, o para dar soporte a varias plataformas (donde esté
separado el código específico de cada plataforma en varios módulos).
También puede definir sus propias excepciones creando una clase que herede
de la clase incorporada Exception, para luego lanzarlas con la orden raise. Vea
la sección de lecturas complementarias si le interesa hacer esto.
hace de manera diferente en las plataformas UNIX, Windows y Mac OS, pero el
código encapsula todas esas diferencias.
OK, no tenemos termios, así que probemos con msvcrt, que es un módulo
específico de Windows que propociona un API a muchas funciones útiles en
los servicios de ejecución de Visual C++. Si falla esta importación, Python
lanzará una ImportError, que capturaremos.
para mostrar ventanas de diálogo de varios tipos. Una vez más, si falla esta
operación, Python lanzará una ImportError, que capturaremos.
cláusula else. En este caso, eso significa que funcionó la importación from
EasyDialogs import AskPassword, de manera que deberíamos asociar
El atributo name de un objeto de fichero nos dice el nombre del fichero que el
objeto ha abierto.
Tras abrir un fichero, lo más importante que querrá hacer es leerlo, como se
muestra en el siguiente ejemplo.
>>> f
<open file '/music/_singles/kairo.mp3', mode 'rb' at 010E3988>
>>> f.tell()
0
>>> f.seek(-128, 2)
>>> f.tell()
7542909
>>> tagData = f.read(128)
>>> tagData
'TAGKAIRO****THE BEST GOA ***DJ MARY-JANE***
Rave Mix 2000http://mp3.com/DJMARYJANE \037'
>>> f.tell()
7543037
que no hemos hecho nada con este fichero aún, la posición actual es 0, que es
el comienzo del fichero.
Los ficheros abiertos consumen recursos del sistema, y dependiendo del modo
del fichero, puede que otros programas no puedan acceder a ellos. Es
importante cerrar los ficheros tan pronto como haya terminado con ellos.
>>> f
<open file '/music/_singles/kairo.mp3', mode 'rb' at 010E3988>
>>> f.closed
False
>>> f.close()
>>> f
<closed file '/music/_singles/kairo.mp3', mode 'rb' at 010E3988>
>>> f.closed
True
>>> f.seek(0)
Traceback (innermost last):
File "<interactive input>", line 1, in ?
ValueError: I/O operation on closed file
>>> f.tell()
Traceback (innermost last):
File "<interactive input>", line 1, in ?
ValueError: I/O operation on closed file
>>> f.read()
Traceback (innermost last):
File "<interactive input>", line 1, in ?
ValueError: I/O operation on closed file
>>> f.close()
Para cerrar un fichero, llame al método close del objeto. Esto libera el
bloqueo (si lo hubiera) que estaba manteniendo sobre el fichero, activa la
escritura de búfers (si los hubiera) que el sistema aún no ha escrito realmente,
y libera los recursos del sistema.
try:
fsock = open(filename, "rb", 0)
try:
fsock.seek(-128, 2)
tagdata = fsock.read(128)
finally:
fsock.close()
.
.
.
except IOError:
pass
Como abrir y leer ficheros tiene su riesgo y puede provocar una excepción,
todo el código está encapsulado en un bloque try...except. (¡Eh!, ¿no es
maravilloso el sangrado estándarizado? Aquí es donde empezará a
apreciarlo).
La función open puede lanzar una IOError (puede que el fichero no exista).
El método seek puede lanzar una IOError (puede que el fichero sea más
pequeño que 128 bytes).
El método read puede lanzar una IOError (puede que el disco tenga sectores
defectuosos, o esté en una unidad de red y ésta acabe de caerse).
Por fin, gestionamos la excepción IOError. Podría ser la IOError lanzada por
la llamada a open, seek o read. En este caso no nos importa, porque todo lo
que vamos a hacer es ignorarlo en silencio y continuar (recuerde, pass es una
sentencia de Python que no hace nada). Eso es perfectamente válido;
“gestionar” una excepción puede querer decir no hacer nada, pero
explícitamente. Sigue contando como gestión, y el proceso continuará de
forma normal en la siguiente línea de código tras el bloque try...except.
Puede añadir datos al fichero recién abierto con el método write del objeto
de fichero devuelto por open.
lo imprime.
Usted sabe que test.log existe (ya que acaba de escribir en él), de manera
que podemos abrirlo y añadir datos al final (el parámetro "a" implica abrir el
fichero para adición). En realidad podría hacer esto incluso si el fichero no
existiese, porque abrir el fichero para añadir lo creará en caso necesario. Pero
este modo nunca dañará el contenido ya existente en el fichero.
Como puede ver, tanto la línea original que escribió como la segunda que
acaba de añadir están en el fichero test.log. Observe también que no se
incluye el retorno de carro. Como no lo ha escrito explícitamente ninguna de
las veces, el fichero no lo incluye. Puede escribir un retorno de carro con el
carácter "\n". Como no lo ha hecho, todo lo que escribió en el fichero ha
acabado pegadas en la misma línea.
Como la mayoría de los otros lenguajes, Python cuenta con bucles for. La única
razón por la que no los ha visto antes es que Python es bueno en tantas otras
cosas que no los ha necesitado hasta ahora con tanta frecuencia.
Como una sentencia if o cualquier otro bloque sangrado, un bucle for puede
constar de cualquier número de líneas.
Ésta es la razón por la que aún no ha visto aún el bucle for: todavía no lo
habíamos necesitado. Es impresionante lo a menudo que se usan los bucles
for en otros lenguajes cuando todo lo que desea realmente es un join o una
Nunca haga esto. Esto es pensar estilo Visual Basic. Despréndase de eso.
Simplemente, itere sobre la lista, como se mostró en el ejemplo anterior.
Los bucles for no son sólo simples contadores. Puede interar sobre todo tipo de
cosas. Aquí tiene un ejemplo de uso de un bucle for para iterar sobre un
diccionario.
Ejemplo 6.10. Iteración sobre un diccionario
>>> import os
>>> for k, v in os.environ.items():
... print "%s=%s" % (k, v)
USERPROFILE=C:\Documents and Settings\mpilgrim
OS=Windows_NT
COMPUTERNAME=MPILGRIM
USERNAME=mpilgrim
[...snip...]
>>> print "\n".join(["%s=%s" % (k, v)
... for k, v in os.environ.items()])
USERPROFILE=C:\Documents and Settings\mpilgrim
OS=Windows_NT
COMPUTERNAME=MPILGRIM
USERNAME=mpilgrim
[...snip...]
(clave2, valor2), ...]. El bucle for itera sobre esta lista. En la primera
.
.
.
if tagdata[:3] == "TAG":
for tag, (start, end, parseFunc) in
self.tagDataMap.items():
self[tag] = parseFunc(tagdata[start:end])
tagDataMap es un atributo de clase que define las etiquetas que está buscando
Ahora que hemos extraído todos los parámetros de una etiqueta de MP3,
guardar sus datos es sencillo. Haremos un slice de tagdata desde start hasta
end para obtener el dato de esa etiqueta, llamaremos a parseFunc para
eso.
Los módulos, como todo lo demás en Python son objetos. Una vez importados,
siempre puede obtener una referencia a un módulo mediante el diccionario
global sys.modules.
def getFileInfoClass(filename,
module=sys.modules[FileInfo.__module__]):
"get file info class from filename extension"
subclass = "%sFileInfo" %
os.path.splitext(filename)[1].upper()[1:]
return hasattr(module, subclass) and getattr(module, subclass)
or FileInfo
Ésta es una función con dos argumentos; filename es obligatoria, pero module
es opcional y por omisión es el módulo que contiene la clase FileInfo. Esto
no parece eficiente, porque es de esperar que Python evalúe la expresión
sys.modules cada vez que se llama a la función. En realidad, Python evalúa
las expresiones por omisión sólo una vez, la primera en que se importa el
módulo. Como verá más adelante, nunca se llama a esta función con un
argumento module, así que module sirve como constante en la función.
Volverá a esta línea más adelante, tras sumergirse en el módulo os. Por
ahora, creáse que subclass acaba siendo el nombre de una clase, como
MP3FileInfo.
>>> import os
>>> os.path.join("c:\\music\\ap\\", "mahadeva.mp3")
'c:\\music\\ap\\mahadeva.mp3'
>>> os.path.join("c:\\music\\ap", "mahadeva.mp3")
'c:\\music\\ap\\mahadeva.mp3'
>>> os.path.expanduser("~")
'c:\\Documents and Settings\\mpilgrim\\My Documents'
>>> os.path.join(os.path.expanduser("~"), "Python")
'c:\\Documents and Settings\\mpilgrim\\My Documents\\Python'
En este caso ligeramente menos trivial, join añadirá una barra inversa extra a
la ruta antes de unirla al nombre de fichero. Quedé encantadísimo cuando
descubrí esto, ya que addSlashIfNecessary es una de las pequeñas funciones
estúpidas que siempre tengo que escribir cuando escribo mis propias
herramientas en un nuevo lenguaje. No escriba esta pequeña estúpida
función en Python hay gente inteligente que ya lo ha hecho por usted.
>>> os.path.split("c:\\music\\ap\\mahadeva.mp3")
('c:\\music\\ap', 'mahadeva.mp3')
>>> (filepath, filename) =
os.path.split("c:\\music\\ap\\mahadeva.mp3")
>>> filepath
'c:\\music\\ap'
>>> filename
'mahadeva.mp3'
>>> (shortname, extension) = os.path.splitext(filename)
>>> shortname
'mahadeva'
>>> extension
'.mp3'
La función split divide una ruta completa y devuelve una tupla que
contiene la ruta y el nombre del fichero. ¿Recuerda cuando dije que podría
usar la asignación de múltiples variables para devolver varios valores de una
función? Bien, split es una de esas funciones.
>>> os.listdir("c:\\music\\_singles\\")
['a_time_long_forgotten_con.mp3', 'hellraiser.mp3',
'kairo.mp3', 'long_way_home1.mp3', 'sidewinder.mp3',
'spinning.mp3']
>>> dirname = "c:\\"
>>> os.listdir(dirname)
['AUTOEXEC.BAT', 'boot.ini', 'CONFIG.SYS', 'cygwin',
'docbook', 'Documents and Settings', 'Incoming', 'Inetpub', 'IO.SYS',
'MSDOS.SYS', 'Music', 'NTDETECT.COM', 'ntldr', 'pagefile.sys',
'Program Files', 'Python20', 'RECYCLER',
'System Volume Information', 'TEMP', 'WINNT']
>>> [f for f in os.listdir(dirname)
... if os.path.isfile(os.path.join(dirname, f))]
['AUTOEXEC.BAT', 'boot.ini', 'CONFIG.SYS', 'IO.SYS', 'MSDOS.SYS',
'NTDETECT.COM', 'ntldr', 'pagefile.sys']
>>> [f for f in os.listdir(dirname)
... if os.path.isdir(os.path.join(dirname, f))]
['cygwin', 'docbook', 'Documents and Settings', 'Incoming',
'Inetpub', 'Music', 'Program Files', 'Python20', 'RECYCLER',
'System Volume Information', 'TEMP', 'WINNT']
La función listdir toma una ruta y devuelve una lista con el contenido de
ese directorio.
listdir devuelve tanto ficheros como carpetas, sin indicar cual es cual.
Puede usar el filtrado de listas y la función isfile del módulo os.path para
separar los ficheros de las carpetas. isfile toma una ruta y devuelve 1 si
representa un fichero, y un 0 si no. Aquí estamos usando os.path.join para
asegurarnos de tener una ruta completa, pero isfile también funciona con
rutas parciales, relativas al directorio actual de trabajo. Puede usar
os.getcwd() para obtener el directorio de trabajo.
os.path también tiene una función isdir que devuelve 1 si la ruta representa
directory.
Iterando sobre la lista con f, usamos os.path.normcase(f) para normalizar el
caso de acuerdo a los valores por omisión del sistema operativo. normcase es
una pequeña y útil función que compensa en los sistemas operativos
insensibles al caso que piensan que mahadeva.mp3 y mahadeva.MP3 son el
mismo fichero. Por ejemplo, en Windows y Mac OS, normcase convertirá
todo el nombre del fichero a minúsculas; en los sistemas compatibles con
UNIX, devolverá el nombre sin cambios.
extensión.
Siempre que sea posible, debería usar las funciones de os y os.path para
manipulaciones sobre ficheros, directorios y rutas. Estos módulos
encapsulan los específicos de cada plataforma, de manera que funciones
como os.path.split funcionen en UNIX, Windows, Mac OS y cualquier
otra plataforma en que funcione Python.
>>> os.listdir("c:\\music\\_singles\\")
['a_time_long_forgotten_con.mp3', 'hellraiser.mp3',
'kairo.mp3', 'long_way_home1.mp3', 'sidewinder.mp3',
'spinning.mp3']
>>> import glob
>>> glob.glob('c:\\music\\_singles\\*.mp3')
['c:\\music\\_singles\\a_time_long_forgotten_con.mp3',
'c:\\music\\_singles\\hellraiser.mp3',
'c:\\music\\_singles\\kairo.mp3',
'c:\\music\\_singles\\long_way_home1.mp3',
'c:\\music\\_singles\\sidewinder.mp3',
'c:\\music\\_singles\\spinning.mp3']
>>> glob.glob('c:\\music\\_singles\\s*.mp3')
['c:\\music\\_singles\\sidewinder.mp3',
'c:\\music\\_singles\\spinning.mp3']
>>> glob.glob('c:\\music\\*\\*.mp3')
El módulo glob, por otro lado, toma un comodín y devuelve la ruta absoluta
hasta todos los ficheros y directorios que se ajusten al comodín. Aquí estamos
usando una ruta de directorio más "*.mp3", que coincidirá con todos los
ficheros .mp3. Observe que cada elemento de la lista devuelta incluye la ruta
completa hasta el fichero.
def getFileInfoClass(filename,
module=sys.modules[FileInfo.__module__]):
"get file info class from filename extension"
subclass = "%sFileInfo" %
os.path.splitext(filename)[1].upper()[1:]
return hasattr(module, subclass) and getattr(module, subclass)
or FileInfo
return [getFileInfoClass(f)(f) for f in fileList]
Como pudo ver en la sección anterior, esta línea de código devuelve una lista
de las rutas absolutas de todos los ficheros de directory que tienen una
extensión interesante (especificada por fileExtList).
Ahora que ha visto el módulo os, esta línea debería tener más sentido. Toma
la extensión del fichero (os.path.splitext(filename)[1]), fuerza a
convertirla en mayúsculas (.upper()), le quita el punto ([1:]), y construye el
nombre de una clase usando cadenas de formato. Así que
c:\music\ap\mahadeva.mp3 se convierte en .mp3, y esto a su vez en .MP3 que
es, pero no nos importa. Entonces creamos una instancia de esta clase (la que
sea) y pasamos el nombre del fichero (f de nuevo) al método __init__. Como
vio anteriormente en este capítulo, el método __init__ de FileInfo asigna
self["name"], lo que dispara __setitem__, que está reempleazada en la clase
6.7. Resumen
def stripnulls(data):
"strip whitespace and nulls"
return data.replace("\00", "").strip()
class FileInfo(UserDict):
"store file metadata"
def __init__(self, filename=None):
UserDict.__init__(self)
self["name"] = filename
class MP3FileInfo(FileInfo):
"store ID3v1.0 MP3 tags"
tagDataMap = {"title" : ( 3, 33, stripnulls),
"artist" : ( 33, 63, stripnulls),
"album" : ( 63, 93, stripnulls),
"year" : ( 93, 97, stripnulls),
"comment" : ( 97, 126, stripnulls),
"genre" : (127, 128, ord)}
if __name__ == "__main__":
for info in listDirectory("/music/_singles/", [".mp3"]):
print "\n".join(["%s=%s" % (k, v) for k, v in info.items()])
print
• 7.1. Inmersión
• 7.2. Caso de estudio: direcciones de calles
• 7.3. Caso de estudio: números romanos
o 7.3.1. Comprobar los millares
o 7.3.2. Comprobación de centenas
• 7.4. Uso de la sintaxis {n,m}
o 7.4.1. Comprobación de las decenas y unidades
• 7.5. Expresiones regulares prolijas
• 7.6. Caso de estudio: análisis de números de teléfono
• 7.7. Resumen
7.1. Inmersión
Las cadenas tienen métodos para la búsqueda (index, find, y count), reemplazo
(replace), y análisis (split), pero están limitadas a los casos más sencillos. Los
métodos de búsqueda encuentran cadenas sencillas, fijas, y siempre
distinguiendo las mayúsculas. Para hacer búsquedas de una cadena s sin que
importen las mayúsculas, debe invocar s.lower() o s.upper() y asegurarse de
que las cadenas que busca están en el caso adecuado. Los métodos replace y
split tienen las mismas limitaciones.
listas por comprensión de formas oscuras e ilegibles, puede que deba pasarse a
las expresiones regulares.
Lo que yo quería de verdad era una coincidencia con 'ROAD' cuando estuviera
al final de la cadena y fuera una palabra en sí misma, no parte de una mayor.
Para expresar esto en una expresión regular, usamos \b, que significa “aquí
debería estar el límite de una palabra”. En Python, esto se complica debido al
hecho de que el carácter '\' ha de escaparse si está dentro de una cadena. A
veces a esto se le llama la plaga de la barra inversa, y es una de las razones
por las que las expresiones regulares son más sencillas en Perl que en Python.
Por otro lado, Perl mezcla las expresiones regulares con el resto de la sintaxis,
de manera que si tiene un fallo, será difícil decidir si es a causa de la sintaxis
o de la expresión regular.
es lo que deseaba.
Para resolver este problema, eliminé el carácter $ y añadí otro \b. Ahora la
expresión regular dice “busca una 'ROAD' que sea una palabra por sí misma
en cualquier parte de la cadena”, ya sea al final, al principio, o en alguna
parte por en medio.
Footnotes
Seguramente ha visto números romanos, incluso si no los conoce. Puede que los
haya visto en copyrights de viejas películas y programas de televisión
(“Copyright MCMXLVI” en lugar de “Copyright 1946”), o en las placas
conmemorativas en bibliotecas o universidades (“inaugurada en
MDCCCLXXXVIII” en lugar de “inaugurada en 1888”). Puede que incluso los haya
• V=5
• X = 10
• L = 50
• C = 100
• D = 500
• M = 1000
>>> import re
>>> pattern = '^M?M?M?$'
>>> re.search(pattern, 'M')
<SRE_Match object at 0106FB58>
>>> re.search(pattern, 'MM')
<SRE_Match object at 0106C290>
>>> re.search(pattern, 'MMM')
<SRE_Match object at 0106AA38>
>>> re.search(pattern, 'MMMM')
>>> re.search(pattern, '')
<SRE_Match object at 0106F4A8>
repite tres veces, estamos buscando cualquier cosa entre cero y tres
caracteres M seguidos.
• $ para que lo anterior preceda sólo al fin de la cadena. Cuando se
preocuparnos por ahora es si el patrón coincide, cosa que podemos saber con
sólo mirar el valor devuelto por search. 'M' coincide con esta expresión
regular, porque se ajusta a la primera M opcional, mientras que se ignoran la
segunda y tercera M opcionales.
Interesante: una cadena vacía también coincide con esta expresión regular, ya
que todos los caracteres M son opcionales.
Las centenas son más complejas que los millares, porque hay varias maneras
mutuamente exclusivas de expresarlas, dependiendo de su valor.
• 100 = C
• 200 = CC
• 300 = CCC
• 400 = CD
• 500 = D
• 600 = DC
• 700 = DCC
• 800 = DCCC
• 900 = CM
Así que hay cuatro patrones posibles:
• CM
• CD
• De cero a tres caracteres C (cero si hay un 0 en el lugar de las centenas)
• D, seguido opcionalmente de hasta tres caracteres C
>>> import re
>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)$'
>>> re.search(pattern, 'MCM')
<SRE_Match object at 01070390>
>>> re.search(pattern, 'MD')
<SRE_Match object at 01073A50>
>>> re.search(pattern, 'MMMCCC')
<SRE_Match object at 010748A8>
>>> re.search(pattern, 'MCMC')
>>> re.search(pattern, '')
<SRE_Match object at 01071D98>
D?C?C?C? coincide con la D (cada uno de los tres caracteres C son opcionales
'MMMCCC' coincide con los tres primeros caracteres M, y con el patrón D?C?C?C?
también coincide CM, pero $ no lo hace, ya que aún no hemos llegado al final
de la cadena de caracteres (todavía nos queda un carácter C sin pareja). La C
no coincide como parte del patrón D?C?C?C?, ya que se encontró antes el
patrón CM, mutuamente exclusivo.
Es interesante ver que la cadena vacía sigue coincidiendo con este patrón,
porque todas las M son opcionales y se ignoran, y porque la cadena vacía
coincide con el patrón D?C?C?C?, en el que todos los caracteres son
opcionales, y por tanto también se ignoran.
¡Vaya! ¿Ve lo rápido que las expresiones regulares se vuelven feas? Y sólo
hemos cubierto los millares y las centenas de los números romanos. Pero si ha
seguido el razonamiento, las decenas y las unidades son sencillas, porque
siguen exactamente el mismo patrón de las centenas. Pero veamos otra manera
de expresarlo.
>>> import re
>>> pattern = '^M?M?M?$'
>>> re.search(pattern, 'M')
<_sre.SRE_Match object at 0x008EE090>
>>> pattern = '^M?M?M?$'
>>> re.search(pattern, 'MM')
<_sre.SRE_Match object at 0x008EEB48>
>>> pattern = '^M?M?M?$'
>>> re.search(pattern, 'MMM')
<_sre.SRE_Match object at 0x008EE090>
>>> re.search(pattern, 'MMMM')
>>>
Esto coincide con el principio de la cadena, y luego con las tres M opcionales,
antes del fin de la cadena.
Esto coincide con el principio de la cadena, y luego las tres M opcionales, pero
no encontramos el fin de la cadena (porque aún hay una M sin emparejar), así
que el patrón no coincide y devuelve None.
Esto coincide con el principio de la cadena, luego tres de tres posibles M, pero
entonces no encuentra el final de la cadena. La expresión regular nos permitía
sólo hasta tres caracteres M antes de encontrar el fin de la cadena, pero
tenemos cuatro, así que el patrón no coincide y se devuelve None.
>>> pattern =
'^M?M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$'
¿Cómo se vería esto usando su sintaxis alternativa {n,m}? El ejemplo nos
muestra la nueva sintaxis.
>>> pattern =
'^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$'
>>> re.search(pattern, 'MDLV')
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMDCLXVI')
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMMMDCCCLXXXVIII')
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'I')
<_sre.SRE_Match object at 0x008EEB48>
Esto coincide con el principio de la cadena, después con uno de las cuatro M
posibles, luego con D?C{0,3}. De estos, coincide con la D opcional y ninguna
de las tres posibles C. Seguimos, y coincide con L?X{0,3} con la L opcional y
ninguna de las tres posibles X. Entonces coincide con V?I{0,3} al encontrar la
V opcional y ninguna de las tres posibles I, y por último el fin de la cadena.
Esto coincide con el principio de la cadena, luego dos de las cuatro posibles M,
entonces con D?C{0,3} por una D y una C de tres posibles; luego con L?X{0,3}
por la L y una de tres X posibles; después con V?I{0,3} por una V y una de
tres posibles I; y por último, con el fin de la cadena. MMDCLXVI es la
representación en números romanos de 2666.
Hasta ahora sólo hemos tratado con lo que llamaremos expresiones regulares
“compactas”. Como verá, son difíciles de leer, e incluso si uno sabe lo que
hacen, eso no garantiza que seamos capaces de comprenderlas dentro de seis
meses. Lo que estamos necesitando es documentación en línea.
Python le permite hacer esto con algo llamado expresiones regulares prolijas. Una
expresión regular prolija es diferente de una compacta de dos formas:
patrón debe ser tratado como una expresión prolija. Como puede ver, este
patrón tiene bastante espacio en blanco (que se ignora por completo), y
varios comentarios (que también se ignoran). Una vez ignorado el espacio en
blanco y los comentarios, es exactamente la misma expresión regular que
vimos en la sección anterior, pero es mucho más legible.
• 800-555-1212
• 800 555 1212
• 800.555.1212
• (800) 555-1212
• 1-800-555-1212
• 800-555-1212-1234
• 800-555-1212x1234
• 800-555-1212 ext. 1234
• work 1-(800) 555.1212 #1234
¡Qué gran variedad! En cada uno de estos casos, necesitaba saber que el código
de área era 800, la troncal 555, y el resto del número de teléfono era 1212. Para
aquellos con extensión, necesitaba saber que ésta era 1234.
Lea siempre una expresión regular de izquierda a derecha. Ésta coincide con
el comienzo de la cadena, y luego (\d{3}). ¿Qué es \d{3}? Bien, el {3}
significa “coincidir con exactamente tres caracteres”; es una variante de la
sintaxis {n,m} que vimos antes. \d significa “un dígito numérico” (de 0 a 9).
regular. En este caso, hemos definido tres grupos, un con tres dígitos, otro
con tres dígitos, y otro más con cuatro dígitos.
Esta expresión regular es casi idéntica a la anterior. Igual que antes buscamos
el comienzo de la cadena, luego recordamos un grupo de tres dígitos, luego
un guión, un grupo de tres dígitos a recordar, un guión, un grupo de cuatro
dígitos a recordar. Lo nuevo es que ahora buscamos otro guión, y un grupo a
recordar de uno o más dígitos, y por último el final de la cadena.
¡Vaya! No sólo no hace todo lo que queremos esta expresión, sino que incluso
es un paso atrás, porque ahora no podemos reconocer números sin una
extensión. Eso no es lo que queríamos; si la extensión está ahí, queremos
saberla, pero si no, aún queremos saber cuales son las diferentes partes del
número principal.
>>> phonePattern =
re.compile(r'^(\d{3})\D+(\d{3})\D+(\d{4})\D+(\d+)$')
>>> phonePattern.search('800 555 1212 1234').groups()
>>>
>>> phonePattern.search('800-555-1212')
>>>
>>>
Por último, hemos resuelto el otro problema que nos ocupaba: las
extensiones vuelven a ser opcionales. Si no se encuentra una extensión, el
método groups() sigue devolviendo una tupla de 4 elementos, pero el cuarto
es simplemente una cadena vacía.
Odio ser portador de malas noticias, pero aún no hemos terminado. ¿Cual es
el problema aquí? Hay un carácter adicional antes del código de área, pero la
expresión regular asume que el código de área es lo primero que hay al
empezar la cadena. No hay problema, podemos usar la misma técnica de
“cero o más caracteres no numéricos” para eliminar los caracteres del que
hay antes del código de área.
El siguiente ejemplo muestra cómo manejar los caracteres antes del número de
teléfono.
>>> phonePattern =
re.compile(r'^\D*(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')
>>> phonePattern.search('(800)5551212 ext. 1234').groups()
>>>
Aquí es cuando las expresiones regulares me hacen desear sacarme los ojos
con algún objeto romo. ¿Por qué no funciona con este número de teléfono?
Porque hay un 1 antes del código de área, pero asumimos que todos los
caracteres antes de ese código son no numéricos (\D*). Aggghhh.
Ahora ya podemos analizar con éxito un número teléfono que contenga otros
caracteres y dígitos antes, además de cualquier cantidad de cualquier tipo de
separadores entre cada parte del número de teléfono.
7.7. Resumen
Ésta es sólo la minúscula punta del iceberg de lo que pueden hacer las
expresiones regulares. En otras palabras, incluso aunque esté completamente
saturado con ellas ahora mismo, créame, todavía no ha visto nada.
Las expresiones regulares son muy potentes, pero no son la solución adecuada
para todos los problema. Debería aprender de ellas lo suficiente como para
saber cuándo es apropiado usarlas, cuándo resolverán sus problemas, y cuándo
causarán más problemas de los que resuelven.
• 8.1. Inmersión
• 8.2. Presentación de sgmllib.py
• 8.3. Extracción de datos de documentos HTML
• 8.4. Presentación de BaseHTMLProcessor.py
• 8.5. locals y globals
• 8.6. Cadenas de formato basadas en diccionarios
• 8.7. Poner comillas a los valores de los atributos
• 8.8. Presentación de dialect.py
• 8.9. Todo junto
• 8.10. Resumen
8.1. Inmersión
class BaseHTMLProcessor(SGMLParser):
def reset(self):
# extend (called by SGMLParser.__init__)
self.pieces = []
SGMLParser.reset(self)
def output(self):
"""Return processed HTML as a single string"""
return "".join(self.pieces)
import re
from BaseHTMLProcessor import BaseHTMLProcessor
class Dialectizer(BaseHTMLProcessor):
subs = ()
def reset(self):
# extend (called from __init__ in ancestor)
# Reset all data attributes
self.verbatim = 0
BaseHTMLProcessor.reset(self)
def end_pre(self):
# called for every </pre> tag in HTML source
# Decrement verbatim mode count
self.unknown_endtag("pre")
self.verbatim -= 1
class ChefDialectizer(Dialectizer):
"""convert HTML to Swedish Chef-speak
class FuddDialectizer(Dialectizer):
"""convert HTML to Elmer Fudd-speak"""
subs = ((r'[rl]', r'w'),
(r'qu', r'qw'),
(r'th\b', r'f'),
(r'th', r'd'),
(r'n[.]', r'n, uh-hah-hah-hah.'))
class OldeDialectizer(Dialectizer):
"""convert HTML to mock Middle English"""
subs = ((r'i([bcdfghjklmnpqrstvwxyz])e\b', r'y\1'),
(r'i([bcdfghjklmnpqrstvwxyz])e', r'y\1\1e'),
(r'ick\b', r'yk'),
(r'ia([bcdfghjklmnpqrstvwxyz])', r'e\1e'),
(r'e[ea]([bcdfghjklmnpqrstvwxyz])', r'e\1e'),
(r'([bcdfghjklmnpqrstvwxyz])y', r'\1ee'),
(r'([bcdfghjklmnpqrstvwxyz])er', r'\1re'),
(r'([aeiou])re\b', r'\1r'),
(r'ia([bcdfghjklmnpqrstvwxyz])', r'i\1e'),
(r'tion\b', r'cioun'),
(r'ion\b', r'ioun'),
(r'aid', r'ayde'),
(r'ai', r'ey'),
(r'ay\b', r'y'),
(r'ay', r'ey'),
(r'ant', r'aunt'),
(r'ea', r'ee'),
(r'oa', r'oo'),
(r'ue', r'e'),
(r'oe', r'o'),
(r'ou', r'ow'),
(r'ow', r'ou'),
(r'\bhe', r'hi'),
(r've\b', r'veth'),
(r'se\b', r'e'),
(r"'s\b", r'es'),
(r'ic\b', r'ick'),
(r'ics\b', r'icc'),
(r'ical\b', r'ick'),
(r'tle\b', r'til'),
(r'll\b', r'l'),
(r'ould\b', r'olde'),
(r'own\b', r'oune'),
(r'un\b', r'onne'),
(r'rry\b', r'rye'),
(r'est\b', r'este'),
(r'pt\b', r'pte'),
(r'th\b', r'the'),
(r'ch\b', r'che'),
(r'ss\b', r'sse'),
(r'([wybdp])\b', r'\1e'),
(r'([rnt])\b', r'\1\1e'),
(r'from', r'fro'),
(r'when', r'whan'))
def test(url):
"""test all dialects against URL"""
for dialect in ("chef", "fudd", "olde"):
outfile = "%s.html" % dialect
fsock = open(outfile, "wb")
fsock.write(translate(url, dialect))
fsock.close()
import webbrowser
webbrowser.open_new(outfile)
if __name__ == "__main__":
test("http://diveintopython.org/odbchelper_list.html")
<div class="abstract">
<p>Lists awe <span class="application">Pydon</span>'s wowkhowse
datatype.
If youw onwy expewience wif wists is awways in
<span class="application">Visuaw Basic</span> ow (God fowbid) de
datastowe
in <span class="application">Powewbuiwdew</span>, bwace youwsewf fow
<span class="application">Pydon</span> wists.</p>
</div>
La clave para comprender este capítulo es darse cuenta de que HTML no es sólo
texto, sino texto estructurado. La estructura se deriva de una secuencia más o
menos jerárquica de etiquetas de inicio y de final. Normalmente no trabajará
con HTML de esta manera; trabajará con él de forma textual en un editor de
texto, o visualmente en un navegador o herramienta de autoedición. sgmllib.py
presenta HTML de forma estructural.
sgmllib.py contiene una clase importante: SGMLParser. SGMLParser disgrega
HTML en partes útiles, como etiquetas de inicio y fin. Tan pronto como
consigue obtener algunos datos, llama a un método de sí misma basándose en
lo que encontró. Para utilizar este analizador, derivará la clase SGMLParser para
luego reemplazar estos métodos. A esto me refería cuando dije que presenta
HTML de forma estructural: la estructura de HTML determina la secuencia de
llamadas a métodos y los argumentos que se pasarán a cada uno.
Etiqueta de inicio
Una etiqueta HTML que inicia un bloque, como <html>, <head>, <body>, o
<pre>, o una etiqueta individual como <br> o <img>. Cuando encuentra
atributos.
Etiqueta de fin
Una etiqueta de HTML que termina un bloque, como </html>, </head>,
</body>, o </pre>. Cuando encuentra una etiqueta de fin, SGMLParser
instrucción.
Declaración
Una declaración de HTML, como DOCTYPE, limitada por <! ... >.
Cuando SGMLParser la encuentra, invoca a handle_decl pasándole el
cuerpo de la declaración.
Datos textuales
Un bloque de texto. Cualquier cosa que no caiga en ninguna de las otras
7 categorías. Cuando SGMLParser encuentra esto, invoca a handle_data
pasándole el texto.
y esto imprimirá las etiquetas y otros elementos a medida que los reconozca.
Esto lo hace derivando la clase SGMLParser y definiendo unknown_starttag,
unknown_endtag, handle_data y otros métodos para que se limiten a imprimir
sus argumentos.
c:\python23\lib> type
"c:\downloads\diveintopython\html\toc\index.html"
<!DOCTYPE html
PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-
8859-1">
Hacer pasar esto por la batería de pruebas de sgmllib.py resulta en esta salida:
Por el camino aprenderá también que existen locals, globals y cómo dar
formato a cadenas usando un diccionario.
Footnotes
[...cortamos...]
El uso más simple de urllib es la descarga del texto completo de una página
web usando la función urlopen. Abrir una URL es similar a abrir un fichero.
El valor de retorno de urlopen es objeto parecido al de un fichero, que tiene
algunos de sus mismos métodos.
La operación más sencilla que puede realizar con el objeto devuelto por
urlopen es read, que lee el HTML completo de la página web y lo almacena
class URLLister(SGMLParser):
def reset(self):
SGMLParser.reset(self)
self.urls = []
válida (aunque inútil), en cuyo caso attrs será una lista vacía.
Puede averiguar si esta etiqueta <a> tiene un atributo href con una simple
lista por comprensión multivariable.
Debería cerrar (close) también el objeto analizador, pero por una razón
diferente. Ha leído todos los datos y se los ha dado al analizador, pero el
método feed no garantiza que se haya procesado todo el HTML que le pasó;
puede haberlo almacenado en un búfer, y estar esperando por más.
Asegúrese de invocar close para forzar el vaciado del búfer y que se analice
todo por completo.
Footnotes
métodos por cada cosa interesante que encuentra, pero los métodos no hacen
nada. SGMLParser es un consumidor de HTML: toma un HTML y lo divide en
trozos pequeños y estructurados. Como vio en la sección anterior, puede
derivar SGMLParser para definir clases que capturen etiquetas específicas y
produzcan cosas útiles, como una lista de todos los enlaces en una página web.
Ahora llevará esto un paso más allá, definiendo una clase que capture todo lo
que SGMLParser le lance, reconstruyendo el documento HTML por completo. En
términos técnicos, esta clase será un productor de HTML.
class BaseHTMLProcessor(SGMLParser):
def reset(self):
self.pieces = []
SGMLParser.reset(self)
def output(self):
"""Return processed HTML as a single string"""
return "".join(self.pieces)
Si lo prefiere puede utilizar el método join del módulo string en lugar de:
string.join(self.pieces, "")
Lecturas complementarias
Footnotes
[6] La razón de que Python sea mejor con las listas que con las cadenas es que las
listas son mutables pero las cadenas son inmutables. Esto significa que añadir a
una lista simplemente agrega el elemento y actualiza el índice. Como las
cadenas no se pueden modificar tras haber sido creadas, un código como s = s
+ nuevaparte creará una cadena complemente nueva partiendo de la
No, espere, todavía no puede aprender cosas sobre locals. Antes debe
aprender sobre espacios de nombres. Es un material árido pero es importante,
así que preste atención.
Cuando una línea de código solicita el valor de una variable x, Python busca esa
variable en todos los espacios de nombres disponibles, por orden:
Python 2.2 introdujo un cambio sutil pero importante que afecta al orden
de búsqueda en los espacios de nombre: los ámbitos anidados. En las
versiones de Python anteriores a la 2.2, cuando hace referencia a una
variable dentro de una función anidda o función lambda, Python buscará
esa variable en el espacio de la función actual (anidada o lambda) y después
en el espacio de nombres del módulo. Python 2.2 buscará la variable en el
espacio de nombres de la función actual (anidada o lambda), después en el
espacio de su función madre, y luego en el espacio del módulo. Python 2.1
puede funcionar de ambas maneras; por omisión lo hace como Python 2.0,
pero puede añadir la siguiente línea al código al principio de su módulo
para hacer que éste funcione como Python 2.2:
La función foo tiene dos variables en su espacio de nombres local: arg, cuyo
valor se pasa a la función y x, que se define dentro de la función.
diccionario son los nombres de las variables como cadenas; los valores del
diccionario son los auténticos valores de las variables. De manera que
invocar a foo con un 7 imprime el diccionario que contiene las dos variables
locales de la función: arg (7) y x (1).
Recuerde, Python es de tipado dinámico, así que podría pasar una cadena en
arg con la misma facilidad; la función (y la llamada a locals) funcionará
igual de bien. locals funciona con todas las variables de todos los tipos.
Lo que hace locals con los espacios de nombres locales (función), lo hace
globals para el espacio de nombres global (módulo). globals es más
¿Recuerda la diferencia entre from módulo import e import módulo? Con import
módulo se importa el módulo en sí, pero retiene su espacio de nombres, y esta es
la razón por la que necesita usar el nombre del módulo para acceder a
cualquiera de sus funciones o atributos: módulo.función. Pero con from módulo
import, en realidad está importando funciones y atributos específicos de otro
módulo al espacio de nombres del suyo propio, que es la razón por la que
puede acceder a ellos directamente sin hacer referencia al módulo del que
vinieron originalmente. Con la función globals puede ver esto en
funcionamiento.
Ejemplo 8.11. Presentación de globals
if __name__ == "__main__":
for k, v in globals().items():
print k, "=", v
Para que no se sienta intimidado, recuerde que ya ha visto todo esto antes. La
función globals devuelve un diccionario y estamos iterando sobre él usando
el método items y una asignación multivariable. La única cosa nueva es la
función globals.
Ahora ejecutar el script desde la línea de órdenes nos da esta salida (tenga en
cuenta que la suya puede ser ligeramente diferente, dependiendo de la
plataforma en que haya instalado Python):
SGMLParser = sgmllib.SGMLParser
BaseHTMLProcessor = __main__.BaseHTMLProcessor
__name__ = __main__
Hay otra diferencia importante entre las funciones locals y globals que deberá
aprender antes de que se pille los dedos con ella. Y se los pillará de todas
maneras, pero al menos recordará haberlo aprendido.
def foo(arg):
x = 1
print locals()
locals()["x"] = 2
print "x=",x
z = 7
print "z=",z
foo(3)
globals()["z"] = 8
print "z=",z
Dado que llamamos a foo con 3, esto imprimirá {'arg': 3, 'x': 1}. No
debería sorprenderle.
locals es una función que devuelve un diccionario, y aquí estamos
asignando un valor a ese diccionario. Puede que piense que esto cambiará el
valor de la variable local x a 2, pero no es así. locals no devuelve el
verdadero espacio de nombres local, sino una copia. Así que cambiarlo no
hace nada con los valores de las variables del espacio local.
Esto imprime x= 1, no x= 2.
Tras habernos quemado con locals podríamos pensar que esto no cambiaría
el valor de z, pero sí lo hace. Debido a las diferencias internas en la manera
que está implementado Python (en las que no voy a entrar, ya que no las
comprendo completamente), globals devuelve el verdadero espacio de
nombres global, no una copia: justo lo opuesto que locals. Así que los
cambios que haga en el diccionario devuelto por globals afectan
directamente a las variables globales.
Esto imprime z= 8, no z= 7.
Footnotes
[7] namespaces
¿Por qué le he enseñado que existen locals y globals? Para que pueda
aprender a dar formato a cadenas usando diccionarios. Como recordará, la
cadena de formato normal proporciona una manera sencilla de insertar valores
en cadenas. Los valores se listan en una tupla y se insertan por orden en la
cadena en lugar de cada marcador de formato. Aunque esto es eficiente, no
siempre produce el código más fácil de leer, especialmente cuando ha de
insertar múltiples valores. No se puede simplemente echar un vistazo a la
cadena y comprender cual será el resultado; alternará constantemente entre leer
la cadena y la tupla de valores.
Hay una forma alternativa de dar formato a cadenas que usa diccionarios en
lugar de tuplas de valores.
Incluso puede especificar la misma clave dos veces; cada una será sustituida
por el mismo valor.
Así que, ¿por qué usar una cadena de formato con diccionario? Bien, parece
matar moscas a cañonazos construir un diccionario de claves y valores sólo para
dar formato a una cadena en la siguiente línea; es mucho más útil cuando
resulta que ya tiene un diccionario lleno de claves significativas con sus
respectivos valores. Como locals.
Ejemplo 8.14. Formato basado en diccionarios en
BaseHTMLProcessor.py
Cuando se invoca este método, attrs es una lista de tuplas clave/valor igual
que los items de un diccionario, lo que significa que puede usar la asignación
multivariable para iterar sobre ella. Este patrón debería resultarle familiar ya,
pero se está haciendo mucho aquí, así que vamos a hacer una disección:
Observe que los valores de los atributos href de las etiquetas <a> no tienen
las comillas adecuadas. (Advierta también que está usando comillas triples
para algo diferente a una cadena de documentación. Y directamente en el
IDE, nada menos. Son muy útiles).
Alimentar al analizador.
Usando la función output definida en BaseHTMLProcessor obtenemos la
salida como una única cadena, completa con comillas en los valores sus
atributos. Aunque esto pueda parecer anticlimático, piense sobre todo lo que
ha sucedido aquí: SGMLParser analizó el documento HTML entero,
reduciéndolo a etiquetas, referencias, datos, etc.; BaseHTMLProcessor usó esos
elementos para reconstruir las partes de HTML (que aún están almacenadas
en parser.pieces, por si quiere verlas); y por último, ha llamado a
parser.output, que juntó todas las partes del HTML en una única cadena.
Footnotes
[9] Vale, no es una pregunta tan frecuente. No se acerca a “¿Qué editor debería
usar para escribir código de Python?” (respuesta: Emacs - el traductor disiente)
o “¿Python es mejor o peor que Perl?” (respuesta: “Perl es peor que Python
porque la gente quiere que sea peor.” -Larry Wall, 10/14/1998) Pero aparecen
preguntas sobre procesamiento de HTML de una u otra forma una vez al mes, y
entre estas preguntas, esta es bastante popular.
def end_pre(self):
self.unknown_endtag("pre")
self.verbatim -= 1
Se invoca start_pre cada vez que SGMLParser encuentra una etiqueta <pre>
en la fuente HTML (en breve, veremos exactamente cómo sucede esto). El
método toma un único parámetro, attrs que contiene los atributos de la
etiqueta (si los hay). attrs es una lista de tuplas clave/valor, igual que la que
toma unknown_starttag.
Esto es el único procesamiento especial que hacemos para las etiquetas <pre>.
Ahora pasamos la lista de atributos a unknown_starttag de manera que
pueda hacer el procesamiento por omisión.
Primero querrá hacer el procesamiento por omisión, igual que con cualquier
otra etiqueta de final.
Recuerde, los bloques try...except pueden tener una cláusula else, que se
invoca si no se lanza ninguna excepción dentro del bloque try...except.
Lógicamente, esto significa que encontramos un método do_xxx para esta
etiqueta, así que vamos a invocarla.
Es hora de darle buen uso a todo lo que ha aprendido hasta ahora. Espero que
haya prestado atención.
¡Eh!, espere un momento, ¡hay una sentencia import en esta función! Esto es
perfectamente válido en Python. Está acostumbrado a ver sentencias import
al comienzo de un programa, lo que significa que lo importado está
disponible para todo el programa. Pero también puede importar módulos
dentro de una función, lo que quiere decir que el módulo importado sólo está
disponible dentro de la función. Si tiene un módulo que sólo se usa una vez
en una función, ésta es una manera sencilla de hacer el código más modular
(cuando se dé cuenta de que su hack del fin de semana se ha convertido en
una obra de arte de 800 líneas y decida convertirlo en una docena de
módulos reutilizables, agradecerá esto).
pone en mayúscula la primera letra de una cadena y obliga a que el resto esté
en minúscula. Combinándola con algo de formato de cadenas, hemos
tomado el nombre de un dialecto y lo transformamos en el nombre de la clase
Dialectizer correspondiente. Si dialectName es la cadena 'chef', parserName
será la cadena 'ChefDialectizer'.
ChefDialectizer.
Por último, tiene el objeto de una clase (parserClass), y quiere una instancia
de esa clase. Bien, ya sabe cómo hacerlo: invoque a la clase como si fuera una
función. El hecho de que la clase esté almacenada en una variable local no
supone diferencia alguna; simplemente invoque la variable como si fuera una
función, y lo que obtendrá es una instancia de la clase. Si parserClass es la
clase ChefDialectizer, parser tendrá una instancia de la clase
ChefDialectizer.
¿Por qué molestarse? Al fin y al cabo, sólo hay tres clases Dialectizer; ¿por qué
no usar simplemente una sentencia case? (bien, no hay sentencia case en
Python pero, ¿no es lo mismo usar una serie de sentencias if?) Una razón:
extensibilidad. La función translate no tiene la menor idea de cuántas clases
Dialectizer ha definido. Imagine que mañana define una nueva FooDialectizer;
translate funcionará correctamente pasando 'foo' como dialectName.
adecuado, sin darle nada excepto el nombre del dialecto (no ha visto el
importado dinámico aún, pero prometo que lo veremos en el siguiente
capítulo). Para añadir un nuevo dialecto, se limitaría a añadir un fichero con el
nombre apropiado en el directorio de plug-ins (como foodialect.py qque
contendría la clase FooDialectizer). Llamar a la función translate con el
nombre de dialecto 'foo' encontraría el módulo foodialect.py, importaría la
clase FooDialectizer, y ahí continuaría.
parser.feed(htmlSource)
parser.close()
return parser.output()
una única cadena, así que sólo tiene que llamar a feed una vez. Sin embargo,
podemos llamar a feed con la frecuencia que queramos, y el analizador
seguirá analizando. Así que si está preocupado por el uso de la memoria (o
sabe que va a tratar con un número bien grande de páginas HTML), podría
hacer esto dentro de un bucle, en el que leería unos pocos bytes de HTML
para dárselo al analizador. El resultado sería el mismo.
Como feed mantiene un búfer interno, deberíamos llamar siempre al método
close del analizador cuando hayamos acabado (incluso si alimentó todo de
una vez, como hemos hecho). De otra manera, puede encontrarse conque a la
salida le faltan los últimos bytes.
Y de la misma manera, ha “traducido” una página web, dando sólo una URL y
el nombre de un dialdecto.
Lecturas complementarias
8.10. Resumen
Según estos ejemplos, debería sentirse cómodo haciendo las siguientes cosas:
• 9.1. Inmersión
• 9.2. Paquetes
• 9.3. Análisis de XML
• 9.4. Unicode
• 9.5. Búsqueda de elementos
• 9.6. Acceso a atributos de elementos
• 9.7. Transición
9.1. Inmersión
Hay dos maneras básicas de trabajar con XML. Una se denomina SAX (“Simple
API for XML”), y funciona leyendo un poco de XML cada vez, invocando un
método por cada elemento que encuentra (si leyó Capítulo 8, Procesamiento de
HTML, esto debería serle familiar, porque es la manera en que trabaja el
módulo sgmllib). La otra se llama DOM (“Document Object Model”), y
funciona leyendo el documento XML completo para crear una representación
interna utilizando clases nativas de Python enlazadas en una estructura de
árbol. Python tiene módulos estándar para ambos tipos de análisis, pero en este
capítulo sólo trataremos el uso de DOM.
Options:
-g ..., --grammar=... use specified grammar file or URL
-h, --help show this help
-d show debugging information while parsing
Examples:
kgp.py generates several paragraphs of Kantian
philosophy
kgp.py -g husserl.xml generates several paragraphs of Husserl
kpg.py "<xref id='paragraph'/>" generates a paragraph of Kant
kgp.py template.xml reads from template.xml to decide what to
generate
"""
from xml.dom import minidom
import random
import toolbox
import sys
import getopt
_debug = 0
class KantGenerator:
"""generates mock philosophy based on a context-free grammar"""
def getDefaultSource(self):
"""guess default source of the current grammar
The default source will be one of the <ref>s that is not
cross-referenced. This sounds complicated but it's not.
Example: The default source for kant.xml is
"<xref id='section'/>", because 'section' is the one <ref>
that is not <xref>'d anywhere in the grammar.
In most grammars, the default source will produce the
longest (and most interesting) output.
"""
xrefs = {}
for xref in self.grammar.getElementsByTagName("xref"):
xrefs[xref.attributes["id"].value] = 1
xrefs = xrefs.keys()
standaloneXrefs = [e for e in self.refs.keys() if e not in
xrefs]
if not standaloneXrefs:
raise NoSourceError, "can't guess source, and no source
specified"
return '<xref id="%s"/>' % random.choice(standaloneXrefs)
def reset(self):
"""reset parser"""
self.pieces = []
self.capitalizeNextWord = 0
def refresh(self):
"""reset output buffer, re-parse entire source file, and
return output
def output(self):
"""output generated text"""
return "".join(self.pieces)
The <p> tag is the core of the grammar. It can contain almost
anything: freeform text, <choice> tags, <xref> tags, even
other
<p> tags. If a "class='sentence'" attribute is found, a flag
is set and the next word will be capitalized. If a
"chance='X'"
attribute is found, there is an X% chance that the tag will be
evaluated (and therefore a (100-X)% chance that it will be
completely ignored)
"""
keys = node.attributes.keys()
if "class" in keys:
if node.attributes["class"].value == "sentence":
self.capitalizeNextWord = 1
if "chance" in keys:
chance = int(node.attributes["chance"].value)
doit = (chance > random.randrange(100))
else:
doit = 1
if doit:
for child in node.childNodes: self.parse(child)
A <choice> tag contains one or more <p> tags. One <p> tag
is chosen at random and evaluated; the rest are ignored.
"""
self.parse(self.randomChildElement(node))
def usage():
print __doc__
def main(argv):
grammar = "kant.xml"
try:
opts, args = getopt.getopt(argv, "hg:d", ["help", "grammar="])
except getopt.GetoptError:
usage()
sys.exit(2)
for opt, arg in opts:
if opt in ("-h", "--help"):
usage()
sys.exit()
elif opt == '-d':
global _debug
_debug = 1
elif opt in ("-g", "--grammar"):
grammar = arg
source = "".join(args)
k = KantGenerator(grammar, source)
print k.output()
if __name__ == "__main__":
main(sys.argv[1:])
def openAnything(source):
"""URI, filename, or string --> stream
This function lets you define parsers that take any input source
(URL, pathname to local or network file, or actual data as a
string)
and deal with it in a uniform manner. Returned object is
guaranteed
to have all the basic stdio read methods (read, readline,
readlines).
Just .close() the object when you're done with it.
Examples:
>>> from xml.dom import minidom
>>> sock = openAnything("http://localhost/kant.xml")
>>> doc = minidom.parse(sock)
>>> sock.close()
>>> sock = openAnything("c:\\inetpub\\wwwroot\\kant.xml")
>>> doc = minidom.parse(sock)
>>> sock.close()
>>> sock = openAnything("<ref
id='conjunction'><text>and</text><text>or</text></ref>")
>>> doc = minidom.parse(sock)
>>> sock.close()
"""
if hasattr(source, "read"):
return source
if source == '-':
import sys
return sys.stdin
# try to open with urllib (if source is http, ftp, or file URL)
import urllib
try:
return urllib.urlopen(source)
except (IOError, OSError):
pass
[...corte...]
Let me repeat that this is much, much funnier if you are now or have ever been
a philosophy major.
The interesting thing about this program is that there is nothing Kant-specific
about it. All the content in the previous example was derived from the
grammar file, kant.xml. If you tell the program to use a different grammar file
(which you can specify on the command line), the output will be completely
different.
9.2. Paquetes
Analizar un documentl XML es algo muy sencillo: una línea de código. Sin
embargo, antes de llegar a esa línea de código hará falta dar un pequeño rodeo
para hablar sobre los paquetes.
Así que cuando decimos from xml.dom import minidom, Python interpreta eso
como “busca el directorio dom en xml, y luego busca el módulo minidom ahí, e
impórtalo como minidom”. Pero Python es incluso más inteligente; no sólo
puede importar módulos enteros contenidos en un paquete; puede importar
deforma selectiva clases o funciones específicas de un módulo contenido en un
paquete. También puede importar el paquete en sí como un módulo. La sintaxis
es la misma; Python averigua lo que usted quiere basándose en la estructura de
ficheros del paquete, y hace lo correcto de forma automática.
Ejemplo 9.7. Los paquetes también son módulos
Aquí estamos importando el paquete dom (un paquete anidado en xml) como
si fuera un módulo. A un paquete de cualquier nivel se le puede tratar como
un módulo, como verá enseguida. Incluso puede tener sus propios atributos
y métodos, igual que los módulos que ya hemos visto.
Entonces, ¿cómo puede ser que importemos y tratemos un paquete (que es sólo
un directorio en el disco) como un módulo (que siempre es un fichero)? La
palabra es el fichero mágico __init__.py. Como ve, los paquetes no son simples
directorios; son directorios con un fichero especial dentro, __init__.py. Este
fichero define los atributos y métodos del paquete. Por ejemplo, xml.dom
contiene una clase Node que está definida en xml/dom/__init__.py. Cuando
importa un paquete como módulo (como dom de xml), en realidad está
importando su fichero __init__.py.
Así que, ¿por qué usar paquetes? Bueno, proporcionan una manera lógica de
agrupar módulos relacionados. En lugar de tener un paquete xml que contenga
los paquetes sax y dom, los autores podrían haber escogido poner toda la
funcionalidad de sax en xmlsax.py y toda la funcionalidad de dom en xmldom.py,
o incluso ponerlo todo en un único módulo. Pero eso hubiera sido horrible
(mientras escribo esto, el paquete XML contiene más de 3000 líneas de código) y
difícil de mantener (los ficheros fuente separados implican que varias personas
pueden trabajar simultáneamente en aspectos diferentes).
Como iba diciendo, analizar un documento XML es muy sencillo: una línea de
código. A dónde ir partiendo de eso es cosa suya.
>>> xmldoc =
minidom.parse('~/diveintopython/common/py/kgp/binary.xml')
>>> xmldoc
Como vio en la sección anterior, esto importa el módulo minidom del paquete
xml.dom.
>>> xmldoc.childNodes
[<DOM Element: grammar at 17538908>]
>>> xmldoc.childNodes[0]
<DOM Element: grammar at 17538908>
>>> xmldoc.firstChild
<DOM Element: grammar at 17538908>
Cada Node tiene un atributo childNodes, que es una lista de objetos Node. Un
Document siempre tiene un único nodo hijo, el elemento raíz del documento
Para obtener el primer nodo hijo (en este caso, el único), use la sintaxis
normal de las listas. Recuerde, no hay nada especial aquí; es sólo una lista
normal de Python que contiene objetos normales.
Como obtener el primer nodo hijo de un nodo es una actividad útil y común,
la clase Node cuenta con el atributo firstChild, que es sinónimo de
childNodes[0] (también tiene un atributo lastChild, que es sinónimo de
childNodes[-1]).
>>> grammarNode.childNodes
[<DOM Text node "\n">, <DOM Element: ref at 17533332>, \
<DOM Text node "\n">, <DOM Element: ref at 17549660>, <DOM Text node
"\n">]
>>> print grammarNode.firstChild.toxml()
Viendo el XML de binary.xml, podría pensar que el nodo grammar sólo tiene
dos hijos, los dos elementos ref. Pero está olvidándose de algo: ¡los retornos
de carro! Tras '<grammar>' y antes del primer '<ref>' hay un retorno de
carro, y este texto cuenta como nodo hijo del elemento grammar. De forma
similar, hay un retorno de carro tras cada '</ref>'; que también cuentan
como nodos hijos. Así que grammar.childNodes en realidad es una lista de 5
objetos: 3 objetos Text y 2 Element.
El último hijo es un objeto Text que representa el retorno de carro que hay
antes de la etiqueta de fin '</ref>' antes de la etiqueta '</grammar>'.
>>> grammarNode
<DOM Element: grammar at 19167148>
>>> refNode = grammarNode.childNodes[1]
>>> refNode
<DOM Element: ref at 17987740>
>>> refNode.childNodes
[<DOM Text node "\n">, <DOM Text node " ">, <DOM Element: p at
19315844>, \
<DOM Text node "\n">, <DOM Text node " ">, \
<DOM Element: p at 19462036>, <DOM Text node "\n">]
>>> pNode = refNode.childNodes[2]
>>> pNode
<DOM Element: p at 19315844>
>>> print pNode.toxml()
<p>0</p>
>>> pNode.firstChild
<DOM Text node "0">
>>> pNode.firstChild.data
u'0'
retorno de carro.
El elemento ref tiene su propio juego de nodos hijos, uno por el retorno de
carro, otro para los espacios, otro más para el elemento p, etc..
El elemento p tiene un solo nodo hijo (no podría adivinarlo por este ejemplo,
pero mire pNode.childNodes si no me cree), que es un nodo Text para el
carácter '0'.
El atributo .data de un nodo Text le da la cadena que representa el texto del
nodo. Pero, ¿qué es esa 'u' delante de la cadena? La respuesta para eso
merece su propia sección.
9.4. Unicode
Python trabaja con unicode desde la versión 2.0 del lenguaje. El paquete XML
utiliza unicode para almacenar todos los datos XML, pero puede usar unicode
en cualquier parte.
Para crear una cadena unicode en lugar de una ASCII normal, añada la letra
“u” antes de la cadena. Observe que esta cadena en particular no tiene
ningún carácter que no sea ASCII. Esto no es problema; unicode es un
superconjunto de ASCII (un superconjunto muy grande, por cierto), así que
también se puede almacenar una cadena ASCII normal como unicode.
¿Recuerda que dije que la función print intenta convertir una cadena
unicode en ASCII para poder imprimirla? Bien, eso no funcionará aquí,
porque la cadena unicode contiene caracteres que no son de ASCII, así que
Python produce un error UnicodeError.
# sitecustomize.py
# this file can be anywhere in your Python path,
# but it usually goes in ${pythondir}/lib/site-packages/
import sys
sys.setdefaultencoding('iso-8859-1')
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
Ahora, ¿qué pasa con XML? Bueno, cada documento XML está en una
codificación específica. ISO-8859-1 es una codificación popular para los datos en
idiomas europeos occidentales. KOI8-R es habitual en textos rusos. Si se
especifica, la codificación estará en la cabecera del documento XML.
Este ejemplo está extraído de un documento XML ruso real; es parte de una
traducción al ruso de este mismo libro. Observe la codificación especificada
en la cabecera, koi8-r.
Éstos son caracteres cirílicos que, hasta donde sé, constituyen la palabra rusa
para “Prefacio” . Si abre este fichero en un editor de textos normal los
caracteres probablemente parezcan basura, puesto que están codificados
usando el esquema koi8-r, pero se mostrarán en iso-8859-1.
Lecturas complementarias
Footnotes
[10] Tristemente, esto no es más que una simplificación extrema. Unicode ha sido
extendido para poder tratar textos clásicos chinos, coreanos y japoneses, que
tienen tantos caracteres diferentes que el sistema unicode de 2 bytes no podía
representarlos todos. Pero Python no soporta eso de serie, y no sé si hay algún
proyecto en marcha para añadirlo. Hemos llegado a los límites de mi
conocimiento, lo siento.
[11] N. del T.: Dado que está leyendo este texto en español, es bastante probable
que también cuente con una ñ en el teclado y pueda escribirla sin recurrir al
hexadecimal, pero aún así he decidido mantener el ejemplo tal cual está en el
original, ya que ilustra el concepto.
Recorrer un documento XML saltando por cada nodo puede ser tedioso. Si está
buscando algo en particular, escondido dentro del documento XML, puede usar
un atajo para encontrarlo rápidamente: getElementsByTagName.
<?xml version="1.0"?>
<!DOCTYPE grammar PUBLIC "-//diveintopython.org//DTD Kant Generator
Pro v1.0//EN" "kgp.dtd">
<grammar>
<ref id="bit">
<p>0</p>
<p>1</p>
</ref>
<ref id="byte">
<p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>\
<xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p>
</ref>
</grammar>
Tiene dos ref, 'bit' y 'byte'. Un bit puede ser '0' o '1', y un byte es 8 bits.
Ejemplo 9.21. Presentación de getElementsByTagName
Los dos primeros elementos p están dentro del primer ref (el ref 'bit').
El último elemento p es el que está dentro del segundo ref (el ref 'byte').
>>> a = bitref.attributes["id"]
>>> a
<xml.dom.minidom.Attr instance at 0x81d5044>
>>> a.name
u'id'
>>> a.value
u'bit'
9.7. Transición
Bien, esto era el material más duro sobre XML. El siguiente capítulo continuará
usando estos mismos programas de ejemplo, pero centrándose en otros
aspectos que hacen al programa más flexible: uso de flujos[12] para proceso de
entrada, uso de getattr para despachar métodos, y uso de opciones en la línea
de órdenes para permitir a los usuarios reconfigurar el programa sin cambiar el
código.
Footnotes
[12] streams
Capítulo 10. Scripts y flujos
Uno de los puntos más fuertes de Python es su enlace dinámico, y un uso muy
potente del enlace dinámico es el objeto tipo fichero.
En su caso más sencillo un objeto tipo fichero es cualquier objeto con un método
read con un parámetro size opcional, que devuelve una cadena. Cuando se le
invoca sin el parámetro size, lee cualquier cosa que quede dentro de la fuente
de datos y devuelve todos esos datos en una sola cadena. Cuando se la invoca
con el parámetro size, lee y de vuelve sólo esa cantidad de datos desde la
fuente; cuando la invocan de nuevo continúa donde lo dejó y devuelve el
siguiente paquete de datos.
Así es como funciona la lectura desde ficheros reales; la diferencia es que no nos
estamos limitando a ficheros. La fuente de entrada puede ser cualquier cosa: un
fichero en el disco, una página web e incluso una cadena de contenido fijo.
Mientras pase a la función un objeto que parezca un fichero, y la función sólo
llame al método read del objeto, la función puede manejar cualquier tipo de
fuente de datos sin necesidad de código específico para cada tipo.
En caso de que se esté preguntando qué tiene esto que ver con el procesamiento
de XML, minidom.parse es una de esas funciones que puede tomar objetos de
fichero.
Bien, todo eso parece una colosal pérdida de tiempo. Después de todo, ya
hemos visto que minidom.parse puede tomar el nombre del fichero y hacer
automáticamente esas tonterías de abrirlo y cerrarlo. Y es cierto que si sabes que
sólo vas a analizar un fichero local, se le puede pasar el nombre y
minidom.parse es lo suficientemente inteligente para Hacer Lo Correcto™. Pero
<channel>
<title>Slashdot</title>
<link>http://slashdot.org/</link>
<description>News for nerds, stuff that matters</description>
</channel>
<image>
<title>Slashdot</title>
<url>http://images.slashdot.org/topics/topicslashdot.gif</url>
<link>http://slashdot.org/</link>
</image>
<item>
<title>To HDTV or Not to HDTV?</title>
<link>http://slashdot.org/article.pl?sid=01/12/28/0421241</link>
</item>
[...corte...]
Como vio en un capítulo anterior, urlopen toma la URL de una página web y
devuelve un objeto de tipo fichero. Aún más importante, este objeto tiene un
método read que devuelve el HTML fuente de la página web.
Tan pronto como haya acabado, asegúrese de cerrar el objeto de fichero que
le da urlopen.
Bien, así que podemos usar la función minidom.parse para analizar tanto
ficheros locales como URLs remotas, pero para analizar cadenas usamos... una
función diferente. Eso significa que si quiere poder tomar la entrada desde un
fichero, una URL o una cadena, necsita una lógica especial para comprobar si es
una cadena, y llamar a parseString en su lugar. Qué insatisfactorio.
El módulo StringIO contiene una única clase que también se llama StringIO,
que le permite convertir una cadena en un objeto de tipo fichero. La clase
StringIO toma la cadena como parámetro al crear una instancia.
Invocar de nuevo a read devuelve una cadena vacía. Así es como funciona
también un fichero real; una vez leído el fichero entero, no se puede leer más
allá sin volver explícitamente al principio del fichero. El objeto StringIO
funciona de la misma manera.
felizmente, sin llegar nunca a saber que la entrada provino de una cadena de
contenido fijo.
Así que ya sabemos cómo usar una única función, minidom.parse, para analizar
un documento XML almacenado en una página web, en un fichero local o en
una cadena fija. Para una página web usaremos urlopen para obtener un objeto
de tipo fichero; para un fichero local usaremos open; y para una cadena,
usaremos StringIO. Ahora llevémoslo un paso más adelante y generalicemos
también éstas diferencias.
def openAnything(source):
# try to open with urllib (if source is http, ftp, or file URL)
import urllib
try:
return urllib.urlopen(source)
except (IOError, OSError):
pass
Por otro lado, si urllib le grita diciéndole que source no es una URL válida,
asumirá que es la ruta a un fichero en el disco e intentará abrirlo. De nuevo,
no usaremos ninguna sofisticación para comprobar si source es un nombre
de fichero válido o no (de todas maneras, las reglas para la validez de los
nombres de fichero varían mucho entre plataformas, así que probablemente
lo haríamos mal). En su lugar, intentará abrir el fichero a ciegas y
capturaremos cualquier error en silencio.
A estas alturas debemos asumir que source es una cadena que contiene los
datos (ya que lo demás no ha funcionado), así que usaremos StringIO para
crear un objeto de tipo fichero partiendo de ella y devolveremos eso. (En
realidad, dado que usamos la función str, source ni siquiera tiene por qué
ser una cadena; puede ser cualquier objeto y usaremos su representación
como cadena, tal como define su método especial __str__).
class KantGenerator:
def _load(self, source):
sock = toolbox.openAnything(source)
xmldoc = minidom.parse(sock).documentElement
sock.close()
return xmldoc
imprime algo, sale por la tubería stdout; cuando el programa aborta e imprime
información de depuración (como el traceback de Python), sale por la tubería
stderr. Ambas se suelen conectar a la ventana terminal donde está trabajando
usted para que cuando el programa imprima, usted vea la salida, y cuando un
programa aborte, pueda ver la información de depuración. (Si está trabajando
en un sistema con un IDE de Python basado en ventanas, stdout y stderr
descargan por omisión en la “Ventana interactiva”).
invoca a sys.stdout.write.
stdout y stderr son ambos objetos de tipo fichero, como aquellos de los que
(On Windows, you can use type instead of cat to display the contents of a file.)
Deja stdout tal como estaba antes de que jugase con él.
Lance una excepción. Observe por la salida de pantalla que esto no imprime
nada. Toda la información normal del volcado de pila se ha escrito en
error.log.
Dado que es tan común escribir los mensajes de error a la salida de errores, hay
una sintaxis abreviada que puede usar en lugar de tomarse las molestias de
hacer la redirección explícita.
Como vio en Sección 9.1, “Inmersión”, esto imprimirá unacadenade ocho bits
al azar, 0 o 1.
Esto imprime simplemente el contenido entero de binary.xml. (Los usuarios
de Windows deberían usar type en lugar de cat).
So how does the script “know” to read from standard input when the grammar
file is “-”? It's not magic; it's just code.
def openAnything(source):
if source == "-":
import sys
return sys.stdin
# try to open with urllib (if source is http, ftp, or file URL)
import urllib
try:
Footnotes
[13]
Un fichero de gramática define una serie de elementos ref. Cada ref contiene
uno o más elementos p, que pueden contener un montón de cosas diferentes,
incluyendo xrefs. Cada vez que encuentras un xref, se busca el elemento ref
correspondiente con el mismo atributo id, se escoge uno de los hijos del
elemento ref y se analiza (veremos cómo se hace esta elección al azar en la
siguiente sección).
Así es como se construye la gramática: definimos elementos ref para las partes
más pequeñas, luego elementos ref que "incluyen" el primer elemento ref
usando xref, etc.. Entonces se analiza la referencia "más grande" y se sigue cada
xref hasta sacar texto real. El texto de la salida depende de las decisiones (al
azar) que se tomen cada vez que seguimos un xref, así que la salida es diferente
cada vez.
Los valores del diccionario self.refs serán los elemento ref en sí. Como vio
en Sección 9.3, “Análisis de XML”, cada elemento, cada nodo, cada
comentario, cada porción de texto de un documento XML analizado es un
objeto.
Una vez construya esta caché, cada vez que encuentre un xref y necesite buscar
el elemento xref con el mismo atributo id, simplemente puede buscarlo en
self.refs.
Otra técnica útil cuando se analizan documentos XML es encontrar todos los
elementos que sean hijos directos de un elemento en particular. Por ejemplo, en
los ficheros de gramáticas un elemento ref puede tener varios elementos p,
cada uno de los cuales puede contener muchas cosas, incluyendo otros
elementos p. Queremos encontrar sólo los elementos p que son hijos de ref, no
elementos p que son hijos de otros elementos p.
Sin embargo, como vio en Ejemplo 9.11, “Los nodos hijos pueden ser un
texto”, la lista que devuelve childNodes contiene todos los tipos diferentes de
nodos, incluidos nodos de texto. Esto no es lo que queremos. Sólo queremos
los hijos que sean elementos.
valores está en el fichero __init__.py del paquete xml.dom (en Sección 9.2,
“Paquetes” hablamos de los paquetes). Pero sólo nos interesan los nodos que
son elementos, así que podemos filtrar la lista para incluir sólo los nodos
cuyo nodeType es ELEMENT_NODE.
Una vez tenemos la lista de los elementos, escoger uno al azar es sencillo.
Python viene con un módulo llamado random que incluye varias funciones
útiles. La función random.choice toma una lista de cualquier número de
elementos y devuelve uno al azar. Por ejemplo, si los elementos ref
contienen varios elementos p entonces choices podría ser una lista de
elementos p y chosen terminaría siendo uno de ellos, escogido al azar.
sencillo escribir algo que separe la lógica por cada tipo de nodo.
Bien, ahora podemos obtener el nombre de la clase de cualquier nodo XML (ya
que cada nodo se representa con un objeto de Python). ¿Cómo puede
aprovechar esto para separar la lógica al analizar cada tipo de nodo? La
respuesta es getattr, que ya vio en Sección 4.4, “Obtención de referencias a
objetos con getattr”.
Antes de nada, advierta que está creando una cadena más grande basándose
en el nombre de la clase del nodo que le pasaron (en el argumento node). Así
que si le pasan un nodo Document estará creando la cadena 'parse_Document',
etc..
Ahora puede tratar la cadena como el nombre de una función y obtener una
referencia a la función en sí usando getattr
parse_Document sólo se invoca una vez, ya que sólo hay un nodo Document en
que hay en los ficheros de gramáticas. Observe sin embargo que aún
debemos definir la función e indicar explícitamente que no haga nada. Si la
función no existiese la parse genérica fallaría tan pronto como se topase con
un comentario, porque intentaría encontrar la inexistente función
parse_Comment. Definir una función por cada tipo de nodo, incluso los que no
#argecho.py
import sys
La primera cosa que hay de saber sobre sys.argv es que contiene el nombre
del script que se ha ejecutado. Aprovecharemos este conocimiento más
adelante, en Capítulo 16, Programación Funcional. No se preocupe de eso por
ahora.
Para hacer las cosas incluso más interesantes, algunas opciones pueden
tomar argumentos propios. Por ejemplo, aquí hay una opción (-m) que toma
un argumento (kant.xml). Tanto la opción como su argumento son simples
elementos secuenciales en la lista sys.argv. No se hace ningún intento de
asociar uno con otro; todo lo que tenemos es una lista.
Así que como puede ver, ciertamente tenemos toda la información que se nos
pasó en la línea de órdenes, pero sin embargo no parece que vaya a ser tan fácil
hacer uso de ella. Para programas sencillos que sólo tomen un argumento y no
tengan opciones, podemos usar simplemente sys.argv[1] para acceder a él. No
hay que avergonzarse de esto; yo mismo lo hago a menudo. Para programas
más complejos necesitará el módulo getopt.
def main(argv):
grammar = "kant.xml"
try:
opts, args = getopt.getopt(argv, "hg:d", ["help", "grammar="])
except getopt.GetoptError:
usage()
sys.exit(2)
...
if __name__ == "__main__":
main(sys.argv[1:])
Antes de nada, mire al final del ejemplo y advierta que está llamando a la
función main con sys.argv[1:]. Recuerde, sys.argv[0] es el nombre del
script que está ejecutándose; no nos interesa eso a la hora de procesar la línea
de órdenes, así que lo eliminamos y pasamos el resto de la lista.
opciones que entedemos, así que probablemente esto significa que el usuario
pasó alguna opción que no entendemos.
Así que, ¿qué son todos esos parámetros que le pasamos a la función getopt?
Bien, el primero es simplemente la lista sin procesar de opciones y argumentos
de la línea de órdenes (sin incluir el primer elemento, el nombre del script, que
ya eliminamos antes de llamar a la función main). El segundo es la lista de
opciones cortas que acepta el script.
"hg:d"
-h
print usage summary
-g ...
use specified grammar file or URL
-d
show debugging information while parsing
Para complicar más las cosas, el script acepta tanto opciones cortas (igual que -
h) como opciones largas (igual que --help), y queremos que hagan lo mismo.
Para esto es el tercer parámetro de getopt, para especificar una lista de opciones
largas que corresponden a las cortas que especificamos en el segundo
parámetro.
["help", "grammar="]
--help
print usage summary
--grammar ...
use specified grammar file or URL
def main(argv):
grammar = "kant.xml"
try:
opts, args = getopt.getopt(argv, "hg:d", ["help", "grammar="])
except getopt.GetoptError:
usage()
sys.exit(2)
for opt, arg in opts:
if opt in ("-h", "--help"):
usage()
sys.exit()
elif opt == '-d':
global _debug
_debug = 1
elif opt in ("-g", "--grammar"):
grammar = arg
source = "".join(args)
k = KantGenerator(grammar, source)
print k.output()
getopt valida que las opciones de la línea de órdenes sean aceptables, pero
Esto es todo. Hemos iterado y tenido en cuenta todas las opciones de línea de
órdenes. Eso significa que si queda algo deben ser argumentos. Ese resto lo
devuelve la función getopt en la variable args. En este caso, estamos
tratándolo como material fuente para el analizador. Si no se especifican
argumentos en la línea de órdenes args será una lista vacía, y source una
cadena vacía.
Para empezar, este es un script que toma argumentos desde la línea de órdenes
usando el módulo getopt.
def main(argv):
...
try:
opts, args = getopt.getopt(argv, "hg:d", ["help", "grammar="])
except getopt.GetoptError:
...
for opt, arg in opts:
...
Creamos una nueva instancia de la clase KantGenerator y le pasamos el fichero
de gramática y una fuente que pueden haber sido especificados desde la línea
de órdenes, o no.
k = KantGenerator(grammar, source)
def getDefaultSource(self):
xrefs = {}
for xref in self.grammar.getElementsByTagName("xref"):
xrefs[xref.attributes["id"].value] = 1
xrefs = xrefs.keys()
standaloneXrefs = [e for e in self.refs.keys() if e not in
xrefs]
return '<xref id="%s"/>' % random.choice(standaloneXrefs)
Ahora exploramos el material fuente. Este material también es XML y lo
analizamos nodo por nodo. Para que el código sea desacoplado y de fácil
mantenimiento, usamos manejadores diferentes por cada tipo de nodo.
Nos movemos por la gramática analizando todos los hijos de cada elemento p,
y los elementos xref con un hijo al azar del elemento ref correspondiente, que
pusimos anteriormente en la caché.
que imprimiremos.
def main(argv):
...
k = KantGenerator(grammar, source)
print k.output()
10.8. Resumen
• 11.1. Inmersión
• 11.2. Cómo no obtener datos mediante HTTP
• 11.3. Características de HTTP
o 11.3.1. User-Agent
o 11.3.2. Redirecciones
o 11.3.3. Last-Modified/If-Modified-Since
o 11.3.4. ETag/If-None-Match
o 11.3.5. Compresión
• 11.4. Depuración de servicios web HTTP
• 11.5. Establecer el User-Agent
• 11.6. Tratamiento de Last-Modified y ETag
• 11.7. Manejo de redirecciones
• 11.8. Tratamiento de datos comprimidos
• 11.9. Todo junto
• 11.10. Resumen
11.1. Inmersión
En próximos capítulos explorará APIs que usan HTTP como transporte para
enviar y recibir datos, pero no relacionará la semántica de la aplicación a la
propia de HTTP (hacen todo usando HTTP POST). Pero este capítulo se
concentrará en usar HTTP GET para obtener datos de un servidor remoto, y
exploraremos varias características de HTTP que podemos usar para obtener el
máximo beneficio de los servicios web puramente HTTP.
Aquí tiene una versión más avanzada del módulo openanything que vio en el
capítulo anterior:
USER_AGENT = 'OpenAnything/1.0
+http://diveintopython.org/http_web_services/'
class SmartRedirectHandler(urllib2.HTTPRedirectHandler):
def http_error_301(self, req, fp, code, msg, headers):
result = urllib2.HTTPRedirectHandler.http_error_301(
self, req, fp, code, msg, headers)
result.status = code
return result
class DefaultErrorHandler(urllib2.HTTPDefaultErrorHandler):
def http_error_default(self, req, fp, code, msg, headers):
result = urllib2.HTTPError(
req.get_full_url(), code, msg, headers, fp)
result.status = code
return result
This function lets you define parsers that take any input source
(URL, pathname to local or network file, or actual data as a
string)
and deal with it in a uniform manner. Returned object is
guaranteed
to have all the basic stdio read methods (read, readline,
readlines).
Just .close() the object when you're done with it.
If the etag argument is supplied, it will be used as the value of
an
If-None-Match request header.
if hasattr(source, 'read'):
return source
if source == '-':
return sys.stdin
if urlparse.urlparse(source)[0] == 'http':
# open URL with urllib2
request = urllib2.Request(source)
request.add_header('User-Agent', agent)
if etag:
request.add_header('If-None-Match', etag)
if lastmodified:
request.add_header('If-Modified-Since', lastmodified)
request.add_header('Accept-encoding', 'gzip')
opener = urllib2.build_opener(SmartRedirectHandler(),
DefaultErrorHandler())
return opener.open(request)
Lecturas complementarias
• Paul Prescod cree que los servichos web HTTP puros son el futuro de
Internet.
Digamos que quiere descargar un recurso mediante HTTP, tal como un feed
sindicado Atom. Pero no sólo queremos descargarlo una vez; queremos
descargarlo una y otra vez, cada hora, para obtener las últimas noticias de un
sitio que nos ofrece noticias sindicadas. Hagámoslo primero a la manera sucia y
rápida, y luego veremos cómo hacerlo mejor.
objeto de tipo fichero del que podemos simplemente leer con read() para
obtener todo el contenido de la página. No puede ser más fácil.
¿Qué hay de malo en esto? Bien, para una prueba rápida o durante el desarrollo
no hay nada de malo. Lo hago a menudo. Quería el contenido de del feed y
obtuve el contenido del feed. La misma técnica funciona con cualquier página
web. Pero una vez empezamos a pensar en términos de servicios web a los que
queremos acceder con regularidad (y recuerde, dijimos que planeamos hacer
esto cada hora) estamos siendo ineficientes, y rudos.
• 11.3.1. User-Agent
• 11.3.2. Redirecciones
• 11.3.3. Last-Modified/If-Modified-Since
• 11.3.4. ETag/If-None-Match
• 11.3.5. Compresión
Hay cinco características importantes de HTTP que debería tener en cuenta.
11.3.1. User-Agent
11.3.2. Redirecciones
A veces se cambia de sitio un recurso. Los sitios web se reorganizan, las páginas
pasan a tener direcciones nuevas. Incluso se puede reorganizar un servicio web.
Una sindicación en http://example.com/index.xml puede convertirse en
http://example.com/xml/atom.xml. O puede que cambie un dominio completo,
1.example.com/index.xml.
Cada vez que solicita cualquier tipo de recurso a un servidor HTTP, el servidor
incluye un código de estado en su respuesta. El código 200 significa “todo
normal, aquí está la página que solicitó”. El código 404 significa “no encontré la
página” (probablemente haya visto errores 404 al navegar por la web).
11.3.3. Last-Modified/If-Modified-Since
Si pedimos los mismos datos una segunda vez (o tercera, o cuarta), podemos
decirle al servidor la fecha de última modificación que obtuvimos la vez
anterior: enviamos junto a la petición la cabecera If-Modified-Since, con la
fecha que nos dieron la última vez. Si los datos no han cambiado desde
entonces, el servidor envía un código de estado HTTP especial, 304, que
significa “estos datos no han cambiado desde la última vez que los pediste”.
¿Por qué supone esto una mejora? Porque cuando el servidor envía un 304, no
envía los datos. Todo lo que obtenemos es el código de estado. Así que no hace
falta descargar los mismos datos una y otra vez si no han cambiado; el servidor
asume que los tenemos en caché local.
11.3.4. ETag/If-None-Match
simplemente envía el 304; no envía los mismos datos una segunda vez. Al incluir
la suma de comprobación ETag en la segunda consulta, le estamos diciendo al
servidor que no necesita enviarlos los datos si aún coinciden con esta suma, ya
que aún conservamos los de la última consulta.
La biblioteca de URL de Python no incorpora la funcionalidad de las ETag, pero
veremos cómo añadirla más adelante en este capítulo.
11.3.5. Compresión
Advierta que nuestro pequeño script de una línea para descargar feeds
sindicados no admite ninguna de estas características de HTTP. Veamos cómo
mejorarlo.
El servidor nos dice cuándo ha sido modificado por última vez este feed
Atom (en este caso, hace unos 13 minutos). Puede enviar esta fecha de vuelta
al servidor la siguiente vez que pida el mismo feed, y el servidor puede hacer
una comprobación de última modificación.
El servidor también nos dice que este feed Atom tiene una suma de
comprobación ETag de valor "e8284-68e0-4de30f80". La suma no significa
nada en sí; no hay nada que podamos hacer con ella excepto enviársela al
servidor la siguiente vez que pidamos el mismo feed. Entonces el servidor
puede usarlo para decirle si los datos han cambiado o no.
El segundo paso es construir algo que abra la URL. Puede tomar cualquier
tipo de manejador, que controlará cómo se manipulan las respuestas. Pero
también podríamos crear uno de estos “abridores” sin manejadores a
medida, que es lo que estamos haciendo aquí. Veremos cómo definir y usar
estos manejadores más adelante en este capítulo, cuando exploremos las
redirecciones.
El último paso es decirle al abridor que abra la URL usando el objeto Request
que hemos creado. Como puede ver por toda la información de depuración
que se imprime, este paso sí que descarga el recurso, y almacena los datos
devueltos en feeddata.
>>> request
<urllib2.Request instance at 0x00250AA8>
>>> request.get_full_url()
http://diveintomark.org/xml/atom.xml
>>> request.add_header('User-Agent',
... 'OpenAnything/1.0 +http://diveintopython.org/')
>>> feeddata = opener.open(request).read()
connect: (diveintomark.org, 80)
send: '
GET /xml/atom.xml HTTP/1.0
Host: diveintomark.org
User-agent: OpenAnything/1.0 +http://diveintopython.org/
'
reply: 'HTTP/1.1 200 OK\r\n'
header: Date: Wed, 14 Apr 2004 23:45:17 GMT
header: Server: Apache/2.0.49 (Debian GNU/Linux)
header: Content-Type: application/atom+xml
header: Last-Modified: Wed, 14 Apr 2004 22:14:38 GMT
header: ETag: "e8284-68e0-4de30f80"
header: Accept-Ranges: bytes
header: Content-Length: 26848
header: Connection: close
si le apetece.
¿Recuerda todas aquellas cabeceras HTTP que vio impresas cuando activó la
depuración? Así es como puede obtener acceso de forma programática a
ellas: firstdatastream.headers es un objeto que actúa como un diccionario y
le permite obtener cualquiera de las cabeceras individuales devueltas por el
servidor HTTP.
urllib2 también lanza una excepción HTTPError por condiciones en las que sí
class DefaultErrorHandler(urllib2.HTTPDefaultErrorHandler):
def http_error_default(self, req, fp, code, msg, headers):
result = urllib2.HTTPError(
req.get_full_url(), code, msg, headers, fp)
result.status = code
return result
una clase que puede definir cualquier cantidad de métodos. Cuando sucede
algo (como un error de HTTP o incluso un código 304) urllib2 hace
introspección en la lista de manipuladores definidos a la busca de un método
que lo maneje. Usamos una introspección similar en Capítulo 9, Procesamiento
de XML para definir manipuladores para diferentes tipos de nodo, pero
urllib2 es más flexible e inspecciona todos los manipuladores que se hayan
definido para la consulta actual.
>>> request.headers
{'If-modified-since': 'Thu, 15 Apr 2004 19:45:21 GMT'}
>>> import openanything
>>> opener = urllib2.build_opener(
... openanything.DefaultErrorHandler())
>>> seconddatastream = opener.open(request)
>>> seconddatastream.status
304
>>> seconddatastream.read()
''
Continuamos con el ejemplo anterior, así que aún existe el objeto Request y
hemos añadido la cabecera If-Modified-Since.
buenas razones? Aquí tiene por qué la construcción del abridor de URL tiene
su propio paso: porque puede hacerlo con su propio manipulador de URL
personalizado que sustituya el comportamiento por omisión de urllib2.
Observe que cuando el servidor envía un código 304 no envía los datos. Ésa
es la idea: ahorrar ancho de banda al no descargar de nuevo los datos que no
hayan cambiado. Así que si realmente quiere los datos, necesitará guardarlos
en una caché local la primera vez que los obtiene.
La segunda llamada tiene un éxito sin aspavientos (no lanza una excepción),
y de nuevo podemos ver que el servidor envió un código de estado 304. Sabe
que los datos no han cambiado basándose en la ETag que enviamos la
segunda vez.
de un servicio web debería estar preparado para usar ambos, pero debe
programar de forma defensiva por si se da el caso de que el servidor sólo
trabaje con uno de ellos, o con ninguno.
Exacto, cuando intenta descargar los datos de esa dirección, el servidor envía
un código de estado 301 diciéndonos que se movió el recurso de forma
permanente.
El servidor también responde con una cabecera Location: que indica el lugar
de la nueva dirección de estos datos.
class SmartRedirectHandler(urllib2.HTTPRedirectHandler):
def http_error_301(self, req, fp, code, msg, headers):
result = urllib2.HTTPRedirectHandler.http_error_301(
self, req, fp, code, msg, headers)
result.status = code
return result
¿Qué hemos conseguido con esto? Ahora podemos crear un abridor de URL con
el manipulador personalizado de redirecciones, y seguirá accediendo
automáticamente a la nueva dirección, pero ahora también podemos averiguar
el código de estado de la redirección.
>>> f.status
301
>>> f.url
'http://diveintomark.org/xml/atom.xml'
Ésta es una URL de ejemplo que he preparado configurada para decirle a los
clientes que se redirijan temporalmente a
http://diveintomark.org/xml/atom.xml.
Los servidores no nos van a dar comprimidos los datos a menos que le digamos
que lo aceptamos así.
Ésta es la clave: una vez creado el objeto Request object, añada una cabecera
Accept-encoding para decirle al servidor que podemos aceptar datos gzip-
Bien, este paso es un rodeo un poco sucio. Python tiene un módulo gzip que
lee (y escribe) ficheros comprimidos en el disco. Pero no tenemos un fichero
sino un búfer en memoria comprimido con gzip, y no queremos escribirlo en
un fichero temporal sólo para poder descomprimirlo. Así que lo que haremos
es crear un objeto de tipo fichero partiendo de los datos en memoria
(compresseddata), haciendo uso del módulo StringIO. Conocimos este
módulo en el capítulo anterior, pero ahora le hemos encontrado un nuevo
uso.
“¡Pero espere!” puedo oírle gritar. “¡Esto podría ser incluso más sencillo!” Sé lo
que está pensando. Está pensando que opener.open devuelve un fichero de tipo
objeto así que, ¿por qué no eliminar el paso intermedio por StringIO y
limitarnos a pasar f directamente a GzipFile? Bueno vale, quizá no estuviera
pensando eso, pero no se preocupe que tampoco hubiera funcionado.
>>> f = opener.open(request)
>>> f.headers.get('Content-Encoding')
'gzip'
>>> data = gzip.GzipFile(fileobj=f).read()
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "c:\python23\lib\gzip.py", line 217, in read
self._read(readsize)
File "c:\python23\lib\gzip.py", line 252, in _read
pos = self.fileobj.tell() # Save current position
AttributeError: addinfourl instance has no attribute 'tell'
Abrir la petición nos dará las cabeceras (aunque aún no descargará datos).
Como puede ver de la cabecera Content-Encoding devuelta, estos datos se
han enviado comprimidos con gzip.
Ya hemos visto todas las partes necesarias para construir un cliente inteligente
de servicios web HTTP. Ahora veamos cómo encaja todo.
if etag:
request.add_header('If-None-Match', etag)
if lastmodified:
request.add_header('If-Modified-Since', lastmodified)
request.add_header('Accept-encoding', 'gzip')
opener = urllib2.build_opener(SmartRedirectHandler(),
DefaultErrorHandler())
return opener.open(request)
condiciones de error.
¡Eso es todo! Abre la URL y devuelve a quien invocó un objeto tipo fichero.
result['data'] = f.read()
if hasattr(f, 'headers'):
# save ETag, if the server sent one
result['etag'] = f.headers.get('ETag')
result['url'] = f.url
result['status'] = 200
if hasattr(f, 'status'):
result['status'] = f.status
f.close()
return result
Primero llamamos a la función openAnything con una URL, una suma ETag,
una fecha Last-Modified y una User-Agent.
Guarda la suma ETag devuelta por el servidor para que la aplicación que
llama pueda pasárnosla de nuevo la siguiente vez, y a su vez nosotros a
openAnything, que puede adjuntarla a la cabecera If-None-Match y enviarla al
servidor remoto.
La primera vez que pedimos un recurso no tenemos suma ETag o fecha Last-
Modified, así que no las incluimos. (Son parámetros opcionales.)
11.10. Resumen
• 12.1. Inmersión
• 12.2. Instalación de las bibliotecas de SOAP
o 12.2.1. Instalación de PyXML
o 12.2.2. Instalación de fpconst
o 12.2.3. Instalación de SOAPpy
• 12.3. Primeros pasos con SOAP
• 12.4. Depuración de servicios web SOAP
• 12.5. Presentación de WSDL
• 12.6. Introspección de servicios web SOAP con WSDL
• 12.7. Búsqueda en Google
• 12.8. Solución de problemas en servicios web SOAP
• 12.9. Resumen
Este capítulo se centrará en los servicios web SOAP, que tienen un enfoque más
estructurad. En lugar de trabajar directamente con consultas HTTP y
documentos XML SOAP nos permite simular llamadas a función que devuelven
tipos de datos nativos. Como veremos, la ilusión es casi perfecta; podemos
“invocat” una función mediante una biblioteca de SOAP, con la sintaxis
estándar de Python, y resulta que la función devuelve valores y objetos de
Python. Sin embargo, bajo esa apariencia la biblioteca de SOAP ha ejecutado en
realidad una transacción compleja que implica varios documentos XML y un
servidor remoto.
12.1. Inmersión
_server = WSDL.Proxy(WSDLFILE)
def search(q):
"""Search Google and return list of {title, link, description}"""
results = _server.doGoogleSearch(
APIKEY, q, 0, 10, False, "", False, "", "utf-8", "utf-8")
return [{"title": r.title.encode("utf-8"),
"link": r.URL.encode("utf-8"),
"description": r.snippet.encode("utf-8")}
for r in results.resultElements]
if __name__ == '__main__':
import sys
for r in search(sys.argv[1])[:5]:
print r['title']
print r['link']
print r['description']
print
Puede importar esto como módulo y usarlo desde otro programa mayor, o
puede ejecutar el script desde la línea de órdenes. Desde la línea de órdenes
puede indicar la consulta de búsqueda como argumento, y como resultado
obtendrá la URL, título y descripción de los cinco primeros resultados de
Google.
Pythonline
http://www.pythonline.com/
Antes de que pueda zambullirse en los servicios web SOAP necestará instalar
tres bibliotecas: PyXML, fpconst y SOAPpy.
Procedimiento 12.1.
Procedimiento 12.2.
Procedimiento 12.3.
Cada servicio SOAP tiene una URL que gestiona todas las consultas. Se usa la
mismoa URL en todas las llamadas. Este servicio particular sólo tiene una
función, pero más adelante en el capítulo verá ejemplos de la API de Google,
que tiene varias funciones. La URL de servicio la comparten todas las
funciones.Cada servicio de SOAP tiene también un espacio de nombres que
está definido por el servidor y es completamente arbitrario. Es parte de la
configuración que se precisa para llamar a los métodos de SOAP. Permite al
servidor compartir una única URL de servicio y entregar consultas a
diferentes servicios sin relación entre ellos. Es como dividir Python en
paquetes.
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
**********************************************************************
**
>>> temperature
80.0
<ns1:getTemp
xmlns:ns1="urn:xmethods-Temperature"
SOAP-ENC:root="1">
<v1 xsi:type="xsd:string">27502</v1>
</ns1:getTemp>
<ns1:getTempResponse
xmlns:ns1="urn:xmethods-Temperature"
SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<return xsi:type="xsd:float">80.0</return>
</ns1:getTempResponse>
WSDL le permite hacer esto con los servicios web SOAP. WSDL significa “Web
Services Description Language[14]”. Aunque se diseñó para ser lo
suficientemente flexible para describir muchos tipos de servicios web, se usa
con más frecuencia para describir servicios SOAP.
Un fichero WSDL contiene una descripción de todo lo que implica una llamada
a un servicio web SOAP:
En otras palabras, un fichero WSDL le dice todo lo que necesita saber para
poder llamar a un servicio web SOAP.
Footnotes
Como muchas otras cosas en el campo de los servicios web, WSDL tiene una
historia larga y veleidosa, llena de conflictos e intrigas políticas. Me saltaré toda
esta historia ya que me aburre hasta lo indecible. Hay otros estándares que
intentaron hacer cosas similares, pero acabó ganando WSDL así que
aprendamos cómo usarlo.
Lo más fundamental que nos permite hacer WSDL es descubrir los métodos que
nos ofrece un servidor SOAP.
Ejemplo 12.8. Descubrimiento de los métodos disponibles
Para usar un fichero WSDL debemos de nuevo utilizar una clase proxy,
WSDL.Proxy, que toma un único argumento: el fichero WSDL. Observe que en
Bien, así que sabemos que este servidor SOAP ofrece un solo método: getTemp.
Pero, ¿cómo lo invocamos? El objeto proxy WSDL también nos puede decir eso.
>>> callInfo.outparams
[<SOAPpy.wstools.WSDLTools.ParameterInfo instance at 0x00CF3AF8>]
>>> callInfo.outparams[0].name
u'return'
>>> callInfo.outparams[0].type
(u'http://www.w3.org/2001/XMLSchema', u'float')
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
**********************************************************************
**
>>> temperature
66.0
web de Google.
• q - La palabra o frase que vamos a buscar. La sintaxis es exactamente la
misma que la del formulario web de Google, así que si conoce sintaxis o
trucos avanzados funcionarán aquí igualmente.
• start - El índice del resultado en el que empezar. Igual que en la versión
resultados.
• restrict - Ponga aquí country junto con un código de país para obtener
>>> results.searchTime
0.224919
>>> results.estimatedTotalResultsCount
29800000
>>> results.directoryCategories
[<SOAPpy.Types.structType item at 14367400>:
{'fullViewableName':
'Top/Arts/Literature/World_Literature/American/19th_Century/Twain,_Mar
k',
'specialEncoding': ''}]
>>> results.directoryCategories[0].fullViewableName
'Top/Arts/Literature/World_Literature/American/19th_Century/Twain,_Mar
k'
Esta búsqueda llevó 0.224919 segundos. Eso no incluye el tiempo que tardó el
envío y recepción de documentos XML de SOAP. Sólo el tiempo que empleó
Google procesando la consulta una vez que la recibió.
Por supuesto, el mundo de los servicios web SOAP no es todo luz y felicidad.
Algunas veces las cosas van mal.
Como ha visto durante este capítulo, SOAP supone varias capas. Primero está la
capa HTTP, ya que SOAP envía y recibe documentos a y desde un servidor
HTTP. Así que entran en juego todas las técnicas de depuración que aprendió
en Capítulo 11, Servicios Web HTTP. Puede hacer import httplib y luego
httplib.HTTPConnection.debuglevel = 1 para ver el tráfico HTTP subyacente.
Más allá de la capa HTTP hay varias cosas que pueden ir mal. SOAPpy hace un
trabajo admirable ocultándonos la sintaxis de SOAP, pero eso también significa
que puede ser difícil determinar dónde está el problema cuando las cosas no
funcionan.
Aquí tiene unos pocos ejemplos de fallos comunes que he cometido al suar
servicios web SOAP, y los errores que generaron.
De nuevo, el servidor devuelve una Fault de SOAP y la parte legible del error
nos da una pista del problema: estamos invocando la función getTemp con un
valor entero, pero no hay definida una función con ese nombre que acepte un
entero. En teoría, SOAP le permite sobrecargar funciones así que podría etner
dos funciones en el mismo servicio SOAP con el mismo nombre y
argumentos en mismo número, pero con tipo de diferente. Por eso es
importante que los tipos de datos se ajusten de forma exacta y es la razón de
que WSDL.Proxy no transforme los tipos. Si lo hiciera, ¡podría acabar llamando
a una función completamente diferente! Buena suerte depurando eso en tal
caso. Es mucho más fácil ser detallista con los tipos de datos y obtener un
fallo lo antes posible si los indicamos de forma errónea.
>>> wsdlFile =
'http://www.xmethods.net/sd/2001/TemperatureService.wsdl'
>>> server = WSDL.Proxy(wsdlFile)
>>> (city, temperature) = server.getTemp(27502)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
TypeError: unpack non-sequence
¿Qué hay del servicio web de Google? El problema más común que he tenido
con él es que olvido indicar adecuadamente la clave de la aplicación.
12.9. Resumen
Requisitos de roman.py
Lecturas complementarias
• Este sitio cuenta más cosas sobre los números romanos, incluyendo una
fascinante historia sobre la manera en que los romanos y otras
civilizaciones los usaban (versión corta: descuidada e
inconsistentemente).
13.2. Inmersión
clases o funciones hace referencia a las otras. Hay buenas razones para esto,
como veremos en breve.
import roman
import unittest
class KnownValues(unittest.TestCase):
knownValues = ( (1, 'I'),
(2, 'II'),
(3, 'III'),
(4, 'IV'),
(5, 'V'),
(6, 'VI'),
(7, 'VII'),
(8, 'VIII'),
(9, 'IX'),
(10, 'X'),
(50, 'L'),
(100, 'C'),
(500, 'D'),
(1000, 'M'),
(31, 'XXXI'),
(148, 'CXLVIII'),
(294, 'CCXCIV'),
(312, 'CCCXII'),
(421, 'CDXXI'),
(528, 'DXXVIII'),
(621, 'DCXXI'),
(782, 'DCCLXXXII'),
(870, 'DCCCLXX'),
(941, 'CMXLI'),
(1043, 'MXLIII'),
(1110, 'MCX'),
(1226, 'MCCXXVI'),
(1301, 'MCCCI'),
(1485, 'MCDLXXXV'),
(1509, 'MDIX'),
(1607, 'MDCVII'),
(1754, 'MDCCLIV'),
(1832, 'MDCCCXXXII'),
(1993, 'MCMXCIII'),
(2074, 'MMLXXIV'),
(2152, 'MMCLII'),
(2212, 'MMCCXII'),
(2343, 'MMCCCXLIII'),
(2499, 'MMCDXCIX'),
(2574, 'MMDLXXIV'),
(2646, 'MMDCXLVI'),
(2723, 'MMDCCXXIII'),
(2892, 'MMDCCCXCII'),
(2975, 'MMCMLXXV'),
(3051, 'MMMLI'),
(3185, 'MMMCLXXXV'),
(3250, 'MMMCCL'),
(3313, 'MMMCCCXIII'),
(3408, 'MMMCDVIII'),
(3501, 'MMMDI'),
(3610, 'MMMDCX'),
(3743, 'MMMDCCXLIII'),
(3844, 'MMMDCCCXLIV'),
(3888, 'MMMDCCCLXXXVIII'),
(3940, 'MMMCMXL'),
(3999, 'MMMCMXCIX'))
def testToRomanKnownValues(self):
"""toRoman should give known result with known input"""
for integer, numeral in self.knownValues:
result = roman.toRoman(integer)
self.assertEqual(numeral, result)
def testFromRomanKnownValues(self):
"""fromRoman should give known result with known input"""
for integer, numeral in self.knownValues:
result = roman.fromRoman(numeral)
self.assertEqual(integer, result)
class ToRomanBadInput(unittest.TestCase):
def testTooLarge(self):
"""toRoman should fail with large input"""
self.assertRaises(roman.OutOfRangeError, roman.toRoman, 4000)
def testZero(self):
"""toRoman should fail with 0 input"""
self.assertRaises(roman.OutOfRangeError, roman.toRoman, 0)
def testNegative(self):
"""toRoman should fail with negative input"""
self.assertRaises(roman.OutOfRangeError, roman.toRoman, -1)
def testNonInteger(self):
"""toRoman should fail with non-integer input"""
self.assertRaises(roman.NotIntegerError, roman.toRoman, 0.5)
class FromRomanBadInput(unittest.TestCase):
def testTooManyRepeatedNumerals(self):
"""fromRoman should fail with too many repeated numerals"""
for s in ('MMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'):
self.assertRaises(roman.InvalidRomanNumeralError,
roman.fromRoman, s)
def testRepeatedPairs(self):
"""fromRoman should fail with repeated pairs of numerals"""
for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'):
self.assertRaises(roman.InvalidRomanNumeralError,
roman.fromRoman, s)
def testMalformedAntecedent(self):
"""fromRoman should fail with malformed antecedents"""
for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV',
'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'):
self.assertRaises(roman.InvalidRomanNumeralError,
roman.fromRoman, s)
class SanityCheck(unittest.TestCase):
def testSanity(self):
"""fromRoman(toRoman(n))==n for all n"""
for integer in range(1, 4000):
numeral = roman.toRoman(integer)
result = roman.fromRoman(numeral)
self.assertEqual(integer, result)
class CaseCheck(unittest.TestCase):
def testToRomanCase(self):
"""toRoman should always return uppercase"""
for integer in range(1, 4000):
numeral = roman.toRoman(integer)
self.assertEqual(numeral, numeral.upper())
def testFromRomanCase(self):
"""fromRoman should only accept uppercase input"""
for integer in range(1, 4000):
numeral = roman.toRoman(integer)
roman.fromRoman(numeral.upper())
self.assertRaises(roman.InvalidRomanNumeralError,
roman.fromRoman, numeral.lower())
if __name__ == "__main__":
unittest.main()
Lecturas complementarias
class KnownValues(unittest.TestCase):
knownValues = ( (1, 'I'),
(2, 'II'),
(3, 'III'),
(4, 'IV'),
(5, 'V'),
(6, 'VI'),
(7, 'VII'),
(8, 'VIII'),
(9, 'IX'),
(10, 'X'),
(50, 'L'),
(100, 'C'),
(500, 'D'),
(1000, 'M'),
(31, 'XXXI'),
(148, 'CXLVIII'),
(294, 'CCXCIV'),
(312, 'CCCXII'),
(421, 'CDXXI'),
(528, 'DXXVIII'),
(621, 'DCXXI'),
(782, 'DCCLXXXII'),
(870, 'DCCCLXX'),
(941, 'CMXLI'),
(1043, 'MXLIII'),
(1110, 'MCX'),
(1226, 'MCCXXVI'),
(1301, 'MCCCI'),
(1485, 'MCDLXXXV'),
(1509, 'MDIX'),
(1607, 'MDCVII'),
(1754, 'MDCCLIV'),
(1832, 'MDCCCXXXII'),
(1993, 'MCMXCIII'),
(2074, 'MMLXXIV'),
(2152, 'MMCLII'),
(2212, 'MMCCXII'),
(2343, 'MMCCCXLIII'),
(2499, 'MMCDXCIX'),
(2574, 'MMDLXXIV'),
(2646, 'MMDCXLVI'),
(2723, 'MMDCCXXIII'),
(2892, 'MMDCCCXCII'),
(2975, 'MMCMLXXV'),
(3051, 'MMMLI'),
(3185, 'MMMCLXXXV'),
(3250, 'MMMCCL'),
(3313, 'MMMCCCXIII'),
(3408, 'MMMCDVIII'),
(3501, 'MMMDI'),
(3610, 'MMMDCX'),
(3743, 'MMMDCCXLIII'),
(3844, 'MMMDCCCXLIV'),
(3888, 'MMMDCCCLXXXVIII'),
(3940, 'MMMCMXL'),
(3999, 'MMMCMXCIX'))
def testToRomanKnownValues(self):
"""toRoman should give known result with known input"""
for integer, numeral in self.knownValues:
result = roman.toRoman(integer)
self.assertEqual(numeral, result)
No es suficiente probar que las funciones tienen éxito cuando se les pasa valores
correctos; también debe probar que fallarán si se les da una entrada incorrecta.
Y no sólo cualquier tipo de fallo; deben fallar de la manera esperada.
Recuerde los otros requisitos de toRoman:
class ToRomanBadInput(unittest.TestCase):
def testTooLarge(self):
"""toRoman should fail with large input"""
self.assertRaises(roman.OutOfRangeError, roman.toRoman, 4000)
def testZero(self):
"""toRoman should fail with 0 input"""
self.assertRaises(roman.OutOfRangeError, roman.toRoman, 0)
def testNegative(self):
"""toRoman should fail with negative input"""
self.assertRaises(roman.OutOfRangeError, roman.toRoman, -1)
def testNonInteger(self):
"""toRoman should fail with non-integer input"""
self.assertRaises(roman.NotIntegerError, roman.toRoman, 0.5)
Los dos siguientes requisitos son similares a los tres primeros, excepto que se
aplican a fromRoman en lugar de a toRoman:
class FromRomanBadInput(unittest.TestCase):
def testTooManyRepeatedNumerals(self):
"""fromRoman should fail with too many repeated numerals"""
for s in ('MMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'):
self.assertRaises(roman.InvalidRomanNumeralError,
roman.fromRoman, s)
def testRepeatedPairs(self):
"""fromRoman should fail with repeated pairs of numerals"""
for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'):
self.assertRaises(roman.InvalidRomanNumeralError,
roman.fromRoman, s)
def testMalformedAntecedent(self):
"""fromRoman should fail with malformed antecedents"""
for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV',
'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'):
self.assertRaises(roman.InvalidRomanNumeralError,
roman.fromRoman, s)
No hay mucho nuevo que decir aquí; el patrón es exactamente el mismo que
usamos para probar la entrada incorrecta en toRoman. Señalaré brevemente
que tenemos otra excepción: roman.InvalidRomanNumeralError. Esto hace un
total de tras excepciones propias que necesitaremos definir en roman.py
(junto con roman.OutOfRangeError y roman.NotIntegerError). Veremos cómo
definir estas excepciones cuando empecemos a escribir roman.py, más
adelante en este capítulo.
class SanityCheck(unittest.TestCase):
def testSanity(self):
"""fromRoman(toRoman(n))==n for all n"""
for integer in range(1, 4000):
numeral = roman.toRoman(integer)
result = roman.fromRoman(numeral)
self.assertEqual(integer, result)
Hemos visto antes la función range, pero aquí la invocamos con dos
argumentos, que devuelven una lista de enteros que empiezan en el primero
(1) y avanzan consecutivamente hasta el segundo argumento (4000) sin
incluirlo. O sea, 1..3999, que es el rango válido para convertir a números
romanos.
Los dos últimos requisitos son diferentes de los otros porque ambos son
arbitrarios y tribiales:
class CaseCheck(unittest.TestCase):
def testToRomanCase(self):
"""toRoman should always return uppercase"""
for integer in range(1, 4000):
numeral = roman.toRoman(integer)
self.assertEqual(numeral, numeral.upper())
def testFromRomanCase(self):
"""fromRoman should only accept uppercase input"""
for integer in range(1, 4000):
numeral = roman.toRoman(integer)
roman.fromRoman(numeral.upper())
self.assertRaises(roman.InvalidRomanNumeralError,
roman.fromRoman, numeral.lower())
Lo más interesante de este caso de prueba son todas las cosas que no prueba.
No prueba si el valor devuelto por toRoman es correcto o incluso consistente;
estas cuestiones las resuelven otros casos de prueba. Tenemos un caso de
prueba sólo para probar las mayúsculas. Podría tentarle combinar esto con la
prueba de cordura, ya que ambas pasan por todo el rango de valores y
llaman a toRoman.[16] Pero eso violaría una de las reglas fundamentales: cada
caso de prueba debería responder sólo una cuestión. Imagine que combinase
este prueba de mayúsculas con la de cordura, y que falla el caso de prueba.
Necesitaríamos hacer más análisis para averiguar qué parte del caso de
prueba fallo para determinar el problema. Si necesita analizar los resultados
de las pruebas unitarias sólo para averiguar qué significan, es un caso seguro
de que ha desarrollado mal sus casos de prueba.
Aquí se puede aprender una lección similar: incluso aunque “sepa” que
toRoman siempre devuelve mayúsculas, estamos convirtiendo explícitamente
Esta línea es complicada, pero es muy similar a lo que hicimos en las pruebas
ToRomanBadInput y FromRomanBadInput. Estamos probando para aseguranos
En el siguiente capítulo, veremos cómo escribir código que pasa estas pruebas.
Footnotes
Ahora que están terminadas las pruebas unitarias, es hora de empezar a escribir
el código que intentan probar esos casos de prueba. Vamos a hacer esto por
etapas, para que pueda ver fallar todas las pruebas, y verlas luego pasar una
por una según llene los huecos de roman.py.
#Define exceptions
class RomanError(Exception): pass
class OutOfRangeError(RomanError): pass
class NotIntegerError(RomanError): pass
class InvalidRomanNumeralError(RomanError): pass
def toRoman(n):
"""convert integer to Roman numeral"""
pass
def fromRoman(s):
"""convert Roman numeral to integer"""
pass
Así es como definimos nuestras excepciones propias en Python. Las
excepciones son clases, y podemos crear las nuestras propias derivando las
excepciones existentes. Se recomienda encarecidamente (pero no se precisa)
que derive Exception, que es la clase base de la que heredan todas las
excepciones que incorpora Python. Aquí defino RomanError (que hereda de
Exception) para que actúe de clase base para todas las otras excepciones que
seguirán. Esto es cuestión de estilo; podría igualmente haber hecho que cada
excepción derivase directamente de la clase Exception.
Ejecute romantest1.py con la opción -v, que le dará una salida más prolija para
que pueda ver exactamente qué sucede mientras se ejecuta cada caso. Con algo
de suerte, su salida debería ser como ésta:
======================================================================
ERROR: fromRoman should only accept uppercase input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 154, in
testFromRomanCase
roman1.fromRoman(numeral.upper())
AttributeError: 'None' object has no attribute 'upper'
======================================================================
ERROR: toRoman should always return uppercase
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 148, in
testToRomanCase
self.assertEqual(numeral, numeral.upper())
AttributeError: 'None' object has no attribute 'upper'
======================================================================
FAIL: fromRoman should fail with malformed antecedents
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 133, in
testMalformedAntecedent
self.assertRaises(roman1.InvalidRomanNumeralError,
roman1.fromRoman, s)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with repeated pairs of numerals
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 127, in
testRepeatedPairs
self.assertRaises(roman1.InvalidRomanNumeralError,
roman1.fromRoman, s)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with too many repeated numerals
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 122, in
testTooManyRepeatedNumerals
self.assertRaises(roman1.InvalidRomanNumeralError,
roman1.fromRoman, s)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 99, in
testFromRomanKnownValues
self.assertEqual(integer, result)
File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
raise self.failureException, (msg or '%s != %s' % (first, second))
AssertionError: 1 != None
======================================================================
FAIL: toRoman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 93, in
testToRomanKnownValues
self.assertEqual(numeral, result)
File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
raise self.failureException, (msg or '%s != %s' % (first, second))
AssertionError: I != None
======================================================================
FAIL: fromRoman(toRoman(n))==n for all n
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 141, in
testSanity
self.assertEqual(integer, result)
File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
raise self.failureException, (msg or '%s != %s' % (first, second))
AssertionError: 1 != None
======================================================================
FAIL: toRoman should fail with non-integer input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 116, in
testNonInteger
self.assertRaises(roman1.NotIntegerError, roman1.toRoman, 0.5)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: NotIntegerError
======================================================================
FAIL: toRoman should fail with negative input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 112, in
testNegative
self.assertRaises(roman1.OutOfRangeError, roman1.toRoman, -1)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: OutOfRangeError
======================================================================
FAIL: toRoman should fail with large input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 104, in
testTooLarge
self.assertRaises(roman1.OutOfRangeError, roman1.toRoman, 4000)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: OutOfRangeError
======================================================================
FAIL: toRoman should fail with 0 input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 108, in
testZero
self.assertRaises(roman1.OutOfRangeError, roman1.toRoman, 0)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: OutOfRangeError
----------------------------------------------------------------------
Ran 12 tests in 0.040s
La ejecución del script lanza unittest.main(), que a su vez llama a cada caso
de prueba, que es lo mismo que decir cada método definido dentro de
romantest.py. Por cada caso de uso, imprime la cadena de documentación
Footnotes
Ahora que tenemos preparado el marco de trabajo del módulo roman, es hora de
empezar a escribir código y pasar algunos casos de prueba.
#Define exceptions
class RomanError(Exception): pass
class OutOfRangeError(RomanError): pass
class NotIntegerError(RomanError): pass
class InvalidRomanNumeralError(RomanError): pass
def toRoman(n):
"""convert integer to Roman numeral"""
result = ""
for numeral, integer in romanNumeralMap:
while n >= integer:
result += numeral
n -= integer
return result
def fromRoman(s):
"""convert Roman numeral to integer"""
pass
Si no tiene claro cómo funciona toRoman, añada una sentencia print al final del
bucle while:
while n >= integer:
result += numeral
n -= integer
print 'restando', integer, 'de la entrada,
añadiendo',numeral, 'a la salida'
>>> import roman2
>>> roman2.toRoman(1424)
restando 1000 de la entrada, añadiendo M a la salida
restando 400 de la entrada, añadiendo CD a la salida
restando 10 de la entrada, añadiendo X a la salida
restando 10 de la entrada, añadiendo X a la salida
restando 4 de la entrada, añadiendo IV a la salida
'MCDXXIV'
Aquí están las grandes noticias: esta versión de toRoman pasa la prueba de
valores conocidos. Recuerde, no es exhaustiva, pero pone a la función contra
las cuerdas con una cierta varidad de entradas correctas, incluyendo entradas
que producen cada número romano de un solo carácter, la entrada más
grande posible (3999), y la entrada que produce el número romano posible
más largo (3888). Llegados a este punto, podemos confiar razonablemente en
que la función es correcta con cualquier valor de entrada correcto que le
pasemos.
Sin embargo, la función no “trabaja” con valores incorrectos; falla todas las
pruebas de entrada incorrecta. Esto tiene sentido, porque no hemos incluido
ninguna comprobación en ese sentido. Estas pruebas buscan el lanzamiento
de excepciones específicas (mediante assertRaises), y no las estamos
lanzando. Lo haremos en la siguiente etapa.
======================================================================
FAIL: fromRoman should only accept uppercase input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 156, in
testFromRomanCase
roman2.fromRoman, numeral.lower())
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with malformed antecedents
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 133, in
testMalformedAntecedent
self.assertRaises(roman2.InvalidRomanNumeralError,
roman2.fromRoman, s)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with repeated pairs of numerals
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 127, in
testRepeatedPairs
self.assertRaises(roman2.InvalidRomanNumeralError,
roman2.fromRoman, s)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with too many repeated numerals
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 122, in
testTooManyRepeatedNumerals
self.assertRaises(roman2.InvalidRomanNumeralError,
roman2.fromRoman, s)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 99, in
testFromRomanKnownValues
self.assertEqual(integer, result)
File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
raise self.failureException, (msg or '%s != %s' % (first, second))
AssertionError: 1 != None
======================================================================
FAIL: fromRoman(toRoman(n))==n for all n
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 141, in
testSanity
self.assertEqual(integer, result)
File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
raise self.failureException, (msg or '%s != %s' % (first, second))
AssertionError: 1 != None
======================================================================
FAIL: toRoman should fail with non-integer input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 116, in
testNonInteger
self.assertRaises(roman2.NotIntegerError, roman2.toRoman, 0.5)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: NotIntegerError
======================================================================
FAIL: toRoman should fail with negative input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 112, in
testNegative
self.assertRaises(roman2.OutOfRangeError, roman2.toRoman, -1)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: OutOfRangeError
======================================================================
FAIL: toRoman should fail with large input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 104, in
testTooLarge
self.assertRaises(roman2.OutOfRangeError, roman2.toRoman, 4000)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: OutOfRangeError
======================================================================
FAIL: toRoman should fail with 0 input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 108, in
testZero
self.assertRaises(roman2.OutOfRangeError, roman2.toRoman, 0)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: OutOfRangeError
----------------------------------------------------------------------
Ran 12 tests in 0.320s
FAILED (failures=10)
14.3. roman.py, fase 3
#Define exceptions
class RomanError(Exception): pass
class OutOfRangeError(RomanError): pass
class NotIntegerError(RomanError): pass
class InvalidRomanNumeralError(RomanError): pass
def toRoman(n):
"""convert integer to Roman numeral"""
if not (0 < n < 4000):
if int(n) <> n:
result = ""
def fromRoman(s):
"""convert Roman numeral to integer"""
pass
Más excitante es el hecho de que ahora pasan todas las pruebas de entrada
incorrecta. Esta prueba, testNonInteger, pasa debido a la comprobación
int(n) <> n. Cuando se le pasa un número que no es entero a toRoman, la
testNegative.
======================================================================
FAIL: fromRoman should only accept uppercase input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 156, in
testFromRomanCase
roman3.fromRoman, numeral.lower())
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with malformed antecedents
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 133, in
testMalformedAntecedent
self.assertRaises(roman3.InvalidRomanNumeralError,
roman3.fromRoman, s)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with repeated pairs of numerals
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 127, in
testRepeatedPairs
self.assertRaises(roman3.InvalidRomanNumeralError,
roman3.fromRoman, s)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with too many repeated numerals
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 122, in
testTooManyRepeatedNumerals
self.assertRaises(roman3.InvalidRomanNumeralError,
roman3.fromRoman, s)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 99, in
testFromRomanKnownValues
self.assertEqual(integer, result)
File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
raise self.failureException, (msg or '%s != %s' % (first, second))
AssertionError: 1 != None
======================================================================
FAIL: fromRoman(toRoman(n))==n for all n
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 141, in
testSanity
self.assertEqual(integer, result)
File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
raise self.failureException, (msg or '%s != %s' % (first, second))
AssertionError: 1 != None
----------------------------------------------------------------------
Ran 12 tests in 0.401s
FAILED (failures=6)
La cosa más importante que le puede decir una prueba unitaria exhaustiva
es cuándo debe dejar de programar. Cuando una función pase todas sus
pruebas unitarias, deje de programarla. Cuando un módulo pase todas sus
pruebas unitarias, deje de programar en él.
14.4. roman.py, fase 4
#Define exceptions
class RomanError(Exception): pass
class OutOfRangeError(RomanError): pass
class NotIntegerError(RomanError): pass
class InvalidRomanNumeralError(RomanError): pass
def fromRoman(s):
"""convert Roman numeral to integer"""
result = 0
index = 0
for numeral, integer in romanNumeralMap:
while s[index:index+len(numeral)] == numeral:
result += integer
index += len(numeral)
return result
Si no tiene claro cómo funciona fromRoman, añada una sentencia print al final
del bucle while:
======================================================================
FAIL: fromRoman should only accept uppercase input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage4\romantest4.py", line 156, in
testFromRomanCase
roman4.fromRoman, numeral.lower())
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with malformed antecedents
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage4\romantest4.py", line 133, in
testMalformedAntecedent
self.assertRaises(roman4.InvalidRomanNumeralError,
roman4.fromRoman, s)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with repeated pairs of numerals
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage4\romantest4.py", line 127, in
testRepeatedPairs
self.assertRaises(roman4.InvalidRomanNumeralError,
roman4.fromRoman, s)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with too many repeated numerals
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage4\romantest4.py", line 122, in
testTooManyRepeatedNumerals
self.assertRaises(roman4.InvalidRomanNumeralError,
roman4.fromRoman, s)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
----------------------------------------------------------------------
Ran 12 tests in 1.222s
FAILED (failures=4)
Como vio en Sección 7.3, “Caso de estudio: números romanos”, hay varias
reglas simples para construir un número romano usando las letras M, D, C, L, X, V,
e I. Revisemos las reglas:
#Define exceptions
class RomanError(Exception): pass
class OutOfRangeError(RomanError): pass
class NotIntegerError(RomanError): pass
class InvalidRomanNumeralError(RomanError): pass
def toRoman(n):
"""convert integer to Roman numeral"""
if not (0 < n < 4000):
raise OutOfRangeError, "number out of range (must be 1..3999)"
if int(n) <> n:
raise NotIntegerError, "non-integers can not be converted"
result = ""
for numeral, integer in romanNumeralMap:
while n >= integer:
result += numeral
n -= integer
return result
result = 0
index = 0
for numeral, integer in romanNumeralMap:
while s[index:index+len(numeral)] == numeral:
result += integer
index += len(numeral)
return result
Esto es sólo una continuación del patrón que comentamos en Sección 7.3,
“Caso de estudio: números romanos”. Los lugares de las decenas son XC (90),
XL (40), o una L opcional seguida de 0 a 3 caracteres X opcionales. El lugar de
Llegados aquí, se le permite ser excéptico al pensar que esa expresión regular
grande y fea pueda capturar posiblemente todos los tipos de números romanos
no válidos. Pero no se limite a aceptar mi palabra, mire los resultados:
----------------------------------------------------------------------
Ran 12 tests in 2.864s
OK
Una cosa que no mencioné sobre las expresiones regulares es que, por
omisión, diferencian las mayúsculas de las minúsculas. Como la expresión
regular romanNumeralPattern se expresó en letras mayúsculas, la
comprobación re.search rechazará cualquier entrada que no esté
completamente en mayúsculas. Así que pasa el test de entrada en
mayúsculas.
class FromRomanBadInput(unittest.TestCase):
def testBlank(self):
"""fromRoman should fail with blank string"""
self.assertRaises(roman.InvalidRomanNumeralError,
roman.fromRoman, "")
Cosas muy simples aquí. Invocamos fromRoman con una cadena vacía y nos
aseguramos de que lanza una excepción InvalidRomanNumeralError. La parte
dura fue encontrar el fallo; ahora que lo sabemos, probarlo es la parte
sencilla.
Como el código tiene un fallo, y ahora tenemos un caso de prueba que busca ese
fallo, la prueba fallará:
======================================================================
FAIL: fromRoman should fail with blank string
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage6\romantest61.py", line 137, in
testBlank
self.assertRaises(roman61.InvalidRomanNumeralError,
roman61.fromRoman, "")
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
----------------------------------------------------------------------
Ran 13 tests in 2.864s
FAILED (failures=1)
def fromRoman(s):
"""convert Roman numeral to integer"""
if not s:
raise InvalidRomanNumeralError, 'Input can not be blank'
if not re.search(romanNumeralPattern, s):
raise InvalidRomanNumeralError, 'Invalid Roman numeral: %s' %
s
result = 0
index = 0
for numeral, integer in romanNumeralMap:
while s[index:index+len(numeral)] == numeral:
result += integer
index += len(numeral)
return result
----------------------------------------------------------------------
Ran 13 tests in 2.834s
OK
Todos los demás casos de prueba siguen pasando, lo que significa que este
arreglo no ha roto nada más. Deje de programar.
Programar de esta manera no hace más fácil arreglar fallos. Los fallos simples
(como éste) precisan casos de prueba sencillos; los fallos complejos precisan
casos de prueba complejos. En un entorno centrado en las pruebas puede
parecer que se tarda más en corregir un fallo, ya que se necesita articular en el
código exactamente cual es el fallo (escribir el caso de prueba) y luego corregir
el fallo en sí. Si el caso de prueba no tiene éxito ahora, tendremos que averiguar
si el arreglo fue incorrecto o si el propio caso de prueba tiene un fallo. Sin
embargo, a la larga, este tira y afloja entre código de prueba y código probado
se amortiza por sí solo. Además, ya que podemos ejecutar de nuevo de forma
sencilla todos los casos de prueba junto con el código nuevo, es mucho menos
probable que rompamos código viejo al arreglar el nuevo. La prueba unitaria de
hoy es la prueba de regresión de mañana.
import roman71
import unittest
class KnownValues(unittest.TestCase):
knownValues = ( (1, 'I'),
(2, 'II'),
(3, 'III'),
(4, 'IV'),
(5, 'V'),
(6, 'VI'),
(7, 'VII'),
(8, 'VIII'),
(9, 'IX'),
(10, 'X'),
(50, 'L'),
(100, 'C'),
(500, 'D'),
(1000, 'M'),
(31, 'XXXI'),
(148, 'CXLVIII'),
(294, 'CCXCIV'),
(312, 'CCCXII'),
(421, 'CDXXI'),
(528, 'DXXVIII'),
(621, 'DCXXI'),
(782, 'DCCLXXXII'),
(870, 'DCCCLXX'),
(941, 'CMXLI'),
(1043, 'MXLIII'),
(1110, 'MCX'),
(1226, 'MCCXXVI'),
(1301, 'MCCCI'),
(1485, 'MCDLXXXV'),
(1509, 'MDIX'),
(1607, 'MDCVII'),
(1754, 'MDCCLIV'),
(1832, 'MDCCCXXXII'),
(1993, 'MCMXCIII'),
(2074, 'MMLXXIV'),
(2152, 'MMCLII'),
(2212, 'MMCCXII'),
(2343, 'MMCCCXLIII'),
(2499, 'MMCDXCIX'),
(2574, 'MMDLXXIV'),
(2646, 'MMDCXLVI'),
(2723, 'MMDCCXXIII'),
(2892, 'MMDCCCXCII'),
(2975, 'MMCMLXXV'),
(3051, 'MMMLI'),
(3185, 'MMMCLXXXV'),
(3250, 'MMMCCL'),
(3313, 'MMMCCCXIII'),
(3408, 'MMMCDVIII'),
(3501, 'MMMDI'),
(3610, 'MMMDCX'),
(3743, 'MMMDCCXLIII'),
(3844, 'MMMDCCCXLIV'),
(3888, 'MMMDCCCLXXXVIII'),
(3940, 'MMMCMXL'),
(3999, 'MMMCMXCIX'),
(4000, 'MMMM'),
(4500, 'MMMMD'),
(4888, 'MMMMDCCCLXXXVIII'),
(4999, 'MMMMCMXCIX'))
def testToRomanKnownValues(self):
"""toRoman should give known result with known input"""
for integer, numeral in self.knownValues:
result = roman71.toRoman(integer)
self.assertEqual(numeral, result)
def testFromRomanKnownValues(self):
"""fromRoman should give known result with known input"""
for integer, numeral in self.knownValues:
result = roman71.fromRoman(numeral)
self.assertEqual(integer, result)
class ToRomanBadInput(unittest.TestCase):
def testTooLarge(self):
"""toRoman should fail with large input"""
self.assertRaises(roman71.OutOfRangeError, roman71.toRoman,
5000)
def testZero(self):
"""toRoman should fail with 0 input"""
self.assertRaises(roman71.OutOfRangeError, roman71.toRoman, 0)
def testNegative(self):
"""toRoman should fail with negative input"""
self.assertRaises(roman71.OutOfRangeError, roman71.toRoman, -
1)
def testNonInteger(self):
"""toRoman should fail with non-integer input"""
self.assertRaises(roman71.NotIntegerError, roman71.toRoman,
0.5)
class FromRomanBadInput(unittest.TestCase):
def testTooManyRepeatedNumerals(self):
"""fromRoman should fail with too many repeated numerals"""
for s in ('MMMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'):
self.assertRaises(roman71.InvalidRomanNumeralError,
roman71.fromRoman, s)
def testRepeatedPairs(self):
"""fromRoman should fail with repeated pairs of numerals"""
for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'):
self.assertRaises(roman71.InvalidRomanNumeralError,
roman71.fromRoman, s)
def testMalformedAntecedent(self):
"""fromRoman should fail with malformed antecedents"""
for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV',
'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'):
self.assertRaises(roman71.InvalidRomanNumeralError,
roman71.fromRoman, s)
def testBlank(self):
"""fromRoman should fail with blank string"""
self.assertRaises(roman71.InvalidRomanNumeralError,
roman71.fromRoman, "")
class SanityCheck(unittest.TestCase):
def testSanity(self):
"""fromRoman(toRoman(n))==n for all n"""
for integer in range(1, 5000):
numeral = roman71.toRoman(integer)
result = roman71.fromRoman(numeral)
self.assertEqual(integer, result)
class CaseCheck(unittest.TestCase):
def testToRomanCase(self):
"""toRoman should always return uppercase"""
for integer in range(1, 5000):
numeral = roman71.toRoman(integer)
self.assertEqual(numeral, numeral.upper())
def testFromRomanCase(self):
"""fromRoman should only accept uppercase input"""
for integer in range(1, 5000):
numeral = roman71.toRoman(integer)
roman71.fromRoman(numeral.upper())
self.assertRaises(roman71.InvalidRomanNumeralError,
roman71.fromRoman, numeral.lower())
if __name__ == "__main__":
unittest.main()
'MMMMM'.
Ahora nuestros casos de prueba están actualizados a los nuevos requisitos, pero
nuestro código no, así que espere que varios de ellos fallen.
válido.
======================================================================
ERROR: fromRoman should only accept uppercase input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 161, in
testFromRomanCase
numeral = roman71.toRoman(integer)
File "roman71.py", line 28, in toRoman
raise OutOfRangeError, "number out of range (must be 1..3999)"
OutOfRangeError: number out of range (must be 1..3999)
======================================================================
ERROR: toRoman should always return uppercase
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 155, in
testToRomanCase
numeral = roman71.toRoman(integer)
File "roman71.py", line 28, in toRoman
raise OutOfRangeError, "number out of range (must be 1..3999)"
OutOfRangeError: number out of range (must be 1..3999)
======================================================================
ERROR: fromRoman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 102, in
testFromRomanKnownValues
result = roman71.fromRoman(numeral)
File "roman71.py", line 47, in fromRoman
raise InvalidRomanNumeralError, 'Invalid Roman numeral: %s' % s
InvalidRomanNumeralError: Invalid Roman numeral: MMMM
======================================================================
ERROR: toRoman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 96, in
testToRomanKnownValues
result = roman71.toRoman(integer)
File "roman71.py", line 28, in toRoman
raise OutOfRangeError, "number out of range (must be 1..3999)"
OutOfRangeError: number out of range (must be 1..3999)
======================================================================
ERROR: fromRoman(toRoman(n))==n for all n
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 147, in
testSanity
numeral = roman71.toRoman(integer)
File "roman71.py", line 28, in toRoman
raise OutOfRangeError, "number out of range (must be 1..3999)"
OutOfRangeError: number out of range (must be 1..3999)
----------------------------------------------------------------------
Ran 13 tests in 2.213s
FAILED (errors=5)
Ahora que tenemos casos de prueba que fallan debido a los nuevos requisitos,
debemos pensar en arreglar el código para que quede de acuerdo a estos casos
de prueba. (Una cosa a la que lleva tiempo acostumbrase cuando se empieza a
trabajar usando pruebas unitarias es que el código probado nunca está “por
delante” de los casos de prueba. Mientras esté por detrás sigue habiendo trabajo
que hacer, y en cuanto alcanzan a los casos de uso, se deja de programar).
Ejemplo 15.8. Programación de los nuevos requisitos
(roman72.py)
#Define exceptions
class RomanError(Exception): pass
class OutOfRangeError(RomanError): pass
class NotIntegerError(RomanError): pass
class InvalidRomanNumeralError(RomanError): pass
def toRoman(n):
"""convert integer to Roman numeral"""
if not (0 < n < 5000):
result = ""
for numeral, integer in romanNumeralMap:
while n >= integer:
result += numeral
n -= integer
return result
def fromRoman(s):
"""convert Roman numeral to integer"""
if not s:
raise InvalidRomanNumeralError, 'Input can not be blank'
if not re.search(romanNumeralPattern, s):
raise InvalidRomanNumeralError, 'Invalid Roman numeral: %s' %
s
result = 0
index = 0
for numeral, integer in romanNumeralMap:
while s[index:index+len(numeral)] == numeral:
result += integer
index += len(numeral)
return result
Donde solíamos comprobar 0 < n < 4000 ahora comprobamos 0 < n <
5000. Y cambiamos el mensaje de error que lanzamos con raise para reflejar
Puede que no se crea que estos dos pequeños cambios sean todo lo que
necesitamos. ¡Hey!, no tiene por qué creer en mi palabra; véalo usted mismo:
----------------------------------------------------------------------
Ran 13 tests in 3.685s
OK
15.3. Refactorización
>>> import re
>>> pattern = '^M?M?M?$'
>>> re.search(pattern, 'M')
<SRE_Match object at 01090490>
>>> compiledPattern = re.compile(pattern)
>>> compiledPattern
<SRE_Pattern object at 00F06E28>
>>> dir(compiledPattern)
['findall', 'match', 'scanner', 'search', 'split', 'sub', 'subn']
>>> compiledPattern.search('M')
<SRE_Match object at 01104928>
Llamar a la función search del objeto patrón compilado con la cadena 'M'
cumple la misma función que llamar a re.search con la expresión regular y
la cadena 'M'. Sólo que mucho, mucho más rápido. (En realidad, la función
re.search simplemente compila la expresión regular y llama al método
Siempre que vaya a usar una expresión regular más de una vez, debería
compilarla para obtener un objeto patrón y luego llamar directamente a los
métodos del patrón.
romanNumeralPattern = \
re.compile('^M?M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$
')
def fromRoman(s):
"""convert Roman numeral to integer"""
if not s:
raise InvalidRomanNumeralError, 'Input can not be blank'
if not romanNumeralPattern.search(s):
result = 0
index = 0
for numeral, integer in romanNumeralMap:
while s[index:index+len(numeral)] == numeral:
result += integer
index += len(numeral)
return result
devolvió re.compile.
¿Cuánto más rápido es compilar las expresiones regulares? Véalo por sí mismo:
.............
----------------------------------------------------------------------
Ran 13 tests in 3.385s
OK
Sólo una nota al respecto: esta vez he ejecutado la prueba unitaria sin la
opción -v, así que en lugar de la cadena de documentación completa por
cada prueba lo que obtenemos es un punto cada vez que pasa una. (Si una
prueba falla obtendremos una F y si tiene un error veremos una E. Seguimos
obteniendo volcados de pila completos por cada fallo y error, para poder
encontrar cualquier problema).
#versión antigua
#romanNumeralPattern = \
#
re.compile('^M?M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$
')
#versión nueva
romanNumeralPattern = \
re.compile('^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$')
Esta forma de expresión regular es un poco más corta (aunque no más legible).
La gran pregunta es, ¿es más rápida?
.............
----------------------------------------------------------------------
Ran 13 tests in 3.315s
OK
#versión antigua
#romanNumeralPattern = \
#
re.compile('^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$')
#versión nueva
romanNumeralPattern = re.compile('''
^ # beginning of string
M{0,4} # thousands - 0 to 4 M's
(CM|CD|D?C{0,3}) # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3
C's),
# or 500-800 (D, followed by 0 to 3
C's)
(XC|XL|L?X{0,3}) # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 X's),
# or 50-80 (L, followed by 0 to 3 X's)
(IX|IV|V?I{0,3}) # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 I's),
# or 5-8 (V, followed by 0 to 3 I's)
$ # end of string
''', re.VERBOSE)
.............
----------------------------------------------------------------------
Ran 13 tests in 3.315s
OK
Esta versión nueva y “prolija” pasa todas las mismas pruebas que la antigua.
No ha cambiado nad excepto que el programador que vuelva sobre este
módulo dentro de seis meses tiene la oportunidad de comprender cómo
trabaja la función.
15.4. Epílogo
#Define exceptions
class RomanError(Exception): pass
class OutOfRangeError(RomanError): pass
class NotIntegerError(RomanError): pass
class InvalidRomanNumeralError(RomanError): pass
def toRoman(n):
"""convert integer to Roman numeral"""
if not (0 < n <= MAX_ROMAN_NUMERAL):
raise OutOfRangeError, "number out of range (must be 1..%s)" %
MAX_ROMAN_NUMERAL
if int(n) <> n:
raise NotIntegerError, "non-integers can not be converted"
return toRomanTable[n]
def fromRoman(s):
"""convert Roman numeral to integer"""
if not s:
raise InvalidRomanNumeralError, "Input can not be blank"
if not fromRomanTable.has_key(s):
raise InvalidRomanNumeralError, "Invalid Roman numeral: %s" %
s
return fromRomanTable[s]
def toRomanDynamic(n):
"""convert integer to Roman numeral using dynamic programming"""
result = ""
for numeral, integer in romanNumeralMap:
if n >= integer:
result = numeral
n -= integer
break
if n > 0:
result += toRomanTable[n]
return result
def fillLookupTables():
"""compute all the possible roman numerals"""
#Save the values in two global tables to convert to and from
integers.
for integer in range(1, MAX_ROMAN_NUMERAL + 1):
romanNumber = toRomanDynamic(integer)
toRomanTable.append(romanNumber)
fromRomanTable[romanNumber] = integer
fillLookupTables()
¿Cómo es de rápido?
.............
----------------------------------------------------------------------
Ran 13 tests in 0.791s
OK
Recuerde, el mejor rendimiento que llegó a conseguir con la versión original fue
de 13 pruebas en 3.315 segundos. Por supuesto, no es una comparación
completamente justa por que esta versión tardará más en importar (cuando
llena las tablas de búsqueda). Pero como la importación se hace una sola vez,
esto es obviable a la large.
15.5. Resumen
Este capítulo ha cubeirto mucho terreno del que bastante ni siquiera era
específico de Python. Hay infraestructura de pruebas unitarias para muchos
lenguajes, y todos precisan que entienda los mismos conceptos básicos:
Lecturas complementarias
• 16.1. Inmersión
• 16.2. Encontrar la ruta
• 16.3. Revisión del filtrado de listas
• 16.4. Revisión de la relación de listas
• 16.5. Programación "datocéntrica"
• 16.6. Importación dinámica de módulos
• 16.7. Todo junto
• 16.8. Resumen
16.1. Inmersión
This module will search for scripts in the same directory named
XYZtest.py. Each such script should be a test suite that tests a
module through PyUnit. (As of Python 2.1, PyUnit is included in
the standard library as "unittest".) This script will aggregate all
found test suites into one big test suite and run them all at once.
"""
def regressionTest():
path = os.path.abspath(os.path.dirname(sys.argv[0]))
files = os.listdir(path)
test = re.compile("test\.py$", re.IGNORECASE)
files = filter(test.search, files)
filenameToModuleName = lambda f: os.path.splitext(f)[0]
moduleNames = map(filenameToModuleName, files)
modules = map(__import__, moduleNames)
load = unittest.defaultTestLoader.loadTestsFromModule
return unittest.TestSuite(map(load, modules))
if __name__ == "__main__":
unittest.main(defaultTest="regressionTest")
Ejecutar este script en el mismo directorio del resto de los script de ejemplo del
libro hará que encuentre todas las pruebas unitarias, llamadas módulotest.py,
las ejecutará como una sola prueba y todas pasarán o fallarán a la vez.
----------------------------------------------------------------------
Ran 29 tests in 2.799s
OK
The first 5 tests are from apihelpertest.py, which tests the example script
from Capítulo 4, El poder de la introspección.
import sys, os
>>> import os
>>> os.getcwd()
/home/usted
>>> os.path.abspath('')
/home/usted
>>> os.path.abspath('.ssh')
/home/usted/.ssh
>>> os.path.abspath('/home/usted/.ssh')
/home/usted/.ssh
>>> os.path.abspath('.ssh/../foo/')
/home/usted/foo
manipulación de cadenas.
No hace falta que existan los nombres de rutas y ficheros que se le pasan a
os.path.abspath.
sys.argv[0] = common/py/fullpath.py
path = common/py
full path = /home/usted/diveintopython/common/py
[usted@localhost diveintopython]$ cd common/py
[usted@localhost py]$ python fullpath.py
sys.argv[0] = fullpath.py
path =
full path = /home/usted/diveintopython/common/py
En el primer caso, sys.argv[0] incluye la ruta completa del script. Puede usar
la función os.path.dirname para eliminar el nombre del script y devolver el
nombre completo del directorio, y os.path.abspath nos devuelve
simplemente lo mismo que le pasamos.
def regressionTest():
path = os.getcwd()
sys.path.append(path)
files = os.listdir(path)
En lugar de poner en path el directorio donde está situado el script que está
ejecutándose, ponemos el directorio actual de trabajo. Éste será el directorio
donde estuviésemos en el momento de ejecutarlo, y no tiene por qué
coincidir necesariamente con el del script (lea esta frase un par de veces hasta
que la entienda bien).
Ya está familiarizado con el uso de listas por comprensión para filtrar listas.
Hay otra manera de conseguir lo mismo que algunas personas consideran más
expresiva.
Python incorpora una función filter que toma dos argumentos, una función y
una lista, y devuelve una lista.[18] La función que se pasa como primer
argumento a filter debe a su vez tomar un argumento, y la lista que devuelva
filter contendrá todos los elementos de la lista que se le pasó a filter para los
filter toma dos argumentos, una función (odd) y una lista (li). Itera sobre la
lista e invoca odd con cada elemento. Si odd devuelve verdadero (recuerde,
cualquier valor diferente a cero es verdadero en Python), entonces se incluye
el elemento en la lista devuelta, en caso contrario se filtra. El resultado es una
lista con sólo los números impares que había en la original, en el mismo
orden en que aparecían allí.
files = os.listdir(path)
test = re.compile("test\.py$", re.IGNORECASE)
files = filter(test.search, files)
Como vio en Sección 16.2, “Encontrar la ruta”, path puede contener la ruta
completa o parcial del directorio del script que se está ejecutando o una
cadena vacía si se ejecutó desde el directorio actual. De cualquier manera,
files acabará teniendo los nombres de los ficheros que están en el mismo
Nota histórica. Las versiones de Python anteriores a la 2.0 no tienen listas por
comprensión, así que no podría filtrar usándolas; la función filter, era lo único
a lo que se podía jugar. Incluso cuando se introdujeron las listas por
comprensión en 2.0 algunas personas siguieron prefiriendo la vieja filter (y su
compañera map, que veremos en este capítulo). Ambas técnicas funcionan y la
que deba usar es cuestión de estilo. Se está comentando que map y filter
podrían quedar obsoletas en una versión futura de Python, pero no se ha
tomado aún una decisión.
files = os.listdir(path)
test = re.compile("test\.py$", re.IGNORECASE)
files = [f for f in files if test.search(f)]
Footnotes
map toma una función y una lista[19] y devuelve una lista nueva invocando la
función sobre cada elemento de la lista, por orden. En este caso la función
simplemente multiplica cada elemento por 2.
Podría conseguir lo mismo con una lista por comprensión. Las listas por
comprensión se introdujeron por primera vez en Python 2.0; map lleva ahí
desde el principio.
Como verá en el resto del capítulo, podemos extender este tipo de forma de
pensar centrada en los datos hasta el objetivo final, que es definir y ejecutar una
única batería de pruebas que contenga las pruebas de todas esas otras baterías a
menor escala.
Footnotes
[19] De nuevo, debería señalar que map puede tomar una lista, una tupla o
cualquier objeto que actúe como una secuencia. Vea la nota anterior sobre
filter.
En este caso hemos empezado sin datos; lo primero que hicimos fue obtener la
ruta del directorio del script en ejecución y la lista de ficheros en ese directorio.
Eso fue la preparación y nos dio datos reales con los que trabajar: una lista de
nombres de fichero.
Sin embargo, sabíamos que no nos interesaban todos esos ficheros sino sólo los
que hacen las baterías de prueba. Teníamos demasiados datos, así que
necesitabamos filter-arlos. ¿Cómo sabíamos qué datos mantener?
Necesitábamos una prueba que lo decidiese así que definimos uno y se lo
pasamos a la función filter. En este caso hemos usado una expresión regular
para decidir, pero el concepto sería el mismo independientemente de la manera
en que construyésemos la prueba.
Ahora teníamos los nombres de fichero de cada una de las baterías de prueba (y
sólo eso, ya que hemos filtrado el resto), pero lo que queríamos en realidad eran
nombres de módulos. Teníamos la cantidad de datos correctos, pero estaban en
el formato erróneo. Así que definimos una función que transformara un nombre
de fichero en el de un módulo, y pasamos la función sobre la lista entera. De un
nombre de fichero podemos sacar el nombre de un módulo; de una lista de
nombres de ficheros podemos obtener una lista de nombres de módulos.
Podíamos haber usado un bucle for con una sentencia if en lugar de filter.
En lugar de map podríamos haber usado un bucle for con una llamada a
función. Pero usar bucles for de esa manera es trabajoso. En el mejor de los
casos simplemente es un desperdicio de tiempo; y en el peor introduce fallos
difíciles de detectar. Por ejemplo, necesitamos imaginar la manera de
comprobar la condición “¿este fichero es una batería de pruebas?” de todas
maneras; és la lógica específica de la aplicación y ningún lenguaje puede
escribir eso por nosotros. Pero una vez que ya lo tiene, ¿de verdad quiere tener
que crear una nueva lista vacía y escribir un bucle for y una sentencia if y
luego llamar manualmente a append por cada elemento de la nueva lista si pasa
la condición y llevar un seguimiento de qué variable lleva los datos filtrados y
cual los que no están filtrados? ¿Por qué no limitarse a definir la condición de
prueba y dejar que Python haga el resto del trabajo por nosotros?
Sí, claro, podría intentar ser imaginativo y borrar elementos de la lista original
sin crear una nueva. Pero eso seguro que ya se ha quemado con eso antes.
Intentar modificar una estructura de datos sobre la que se está iterando puede
tener truco. Borramos un elemento, iteramos sobre el siguiente y de repente nos
hemos saltado uno. ¿Es Python uno de los lenguajes que funciona así? ¿Cuánto
nos llevaría averiguarlo? ¿Estamos seguros de recordar si era seguro la
siguiente vez que lo intentemos? Los programadores pierden tanto tiempo y
cometente tantos errores tratando con problemas puramente técnicos como este,
y encima no tiene sentido. No hace avanzar nuestro programa; es todo trabajo
en vano.
quedándome con las familiares formas de los bucles for y las sentencias if y la
programación paso a paso centrada en el código. Y mis programas en Python se
parecían mucho a los de Visual Basic, detallando cada paso de cada operación
de cada función. Y todos tenían los mismos tipos de pequeños problemas y
fallos complicados de encontrar. Y nada de esto tenía sentido.
lo importa. Incluso puede importar varios módulos de una vez de esta manera,
con una lista separada por comas. Hicimos esto en la primera línea del script de
este capítulo.
Esto importa cuatro módulos de unavez: sys (con las funciones del sistema y
acceso a parámetros de línea de órdenes), os (con funciones del sistema
operativo como listado de directorios), re (para las expresiones regulares) y
unittest (para las pruebas unitarias).
Así que __import__ importa un módulo, pero toma una cadena como
argumento para hacerlo. En este caso el módulo que importó era sólo una
cadena fija, pero podría ser también una variable o el resultado de invocar una
función. Y la variable a la que asignemos el módulo tampoco tiene por qué
coincidir con el nombre del módulo. Podría importar varios módulos y
asignarlos a una lista.
que las cadenas resultan ser los nombres de módulos que podríamos
importar, si así quisiéramos.
Para comprobar que estos son módulos reales, veamos algunos de sus
atributos. Recuerde que modules[0] es el módulo sys, así que
modules[0].version es sys.version. También están disponibles todos los
Ahora debería ser capaz de juntar todo esto e imaginarse lo que hace la mayoría
del código de ejemplo de este capítulo.
Hemos aprendido suficiente para hacer disección de las primeras siete líneas
del código de ejemplo de este capítulo: lectura de un directorio e importación
de los módulos seleccionados dentro de él.
def regressionTest():
path = os.path.abspath(os.path.dirname(sys.argv[0]))
files = os.listdir(path)
test = re.compile("test\.py$", re.IGNORECASE)
files = filter(test.search, files)
filenameToModuleName = lambda f: os.path.splitext(f)[0]
moduleNames = map(filenameToModuleName, files)
modules = map(__import__, moduleNames)
load = unittest.defaultTestLoader.loadTestsFromModule
return unittest.TestSuite(map(load, modules))
Veámoslo línea por línea, de forma interactiva. Asuma que el directorio actual
es c:\diveintopython\py, que contiene los ejemplos que acompañan a este libro
incluido el script de este capítulo. Como vimos en Sección 16.2, “Encontrar la
ruta” el directorio del script acabará en la variable path, así que empecemos a
trabajar sobre eso y partamos desde ahí.
files es una lista de todos los ficheros y directorios dentro de aquél donde se
La expresión regular compilada actúa como una función, así que podemos
usarla para filtrar la gran lista de ficheros y directorios, para encontrar los
que coinciden con ella.
Y nos queda la lista de scripts de pruebas unitarias, que eran los únicos con
nombres ALGOtest.py.
lambda respecto a las funciones normales que definimos con una sentencia
Éstos son objetos de módulo reales. No sólo puede acceder a ellos igual que a
cualquier otro módulo, instanciar clases y llamar a funciones, sino que puede
introspeccionar el módulo y averiguar qué clases y funciones contiene. Eso es
lo que hace el método loadTestsFromModule: introspecciona cada módulo y
devuelve un objeto unittest.TestSuite por cada uno. Cada objeto TestSuite
contiene realmente una lista de objetos TestSuite, uno por cada clase
TestCase del módulo, y cada uno de esos objetos TestSuite contiene una lista
ejecutándose ahora. Podría escribir un libro con todos los trucos y técnicas que
se usan en el módulo unittest, pero entonces no acabaría éste nunca).
if __name__ == "__main__":
unittest.main(defaultTest="regressionTest")
16.8. Resumen
• 17.1. Inmersión
• 17.2. plural.py, fase 1
• 17.3. plural.py, fase 2
• 17.4. plural.py, fase 3
• 17.5. plural.py, fase 4
• 17.6. plural.py, fase 5
• 17.7. plural.py, fase 6
• 17.8. Resumen
17.1. Inmersión
Quiero hablar sobre los sustantivos en plural. También sobre funciones que
devuelven otras funciones, expresiones regulares avanzadas y generadores. Los
generadores son nuevos en Python 2.3. Pero primero hablemos sobre cómo
hacer nombres en plural[20].
Footnotes
[20] N. del T.: por supuesto, Mark se refiere a los plurales en inglés. Téngalo en
cuenta el resto del capítulo, puesto que no voy a adaptarlo a los sustantivos en
castellano para no tener que modificar los ejemplos.
Así que estamos mirando las palabras, que al menos en inglés son cadenas de
caracteres. Y tenemos reglas que dicen que debe encontrar diferentes
combinaciones de caracteres y luego hacer cosas distintas con ellos. Esto suena a
trabajo para las expresiones regulares.
Ejemplo 17.1. plural1.py
import re
def plural(noun):
if re.search('[sxz]$', noun):
return re.sub('$', 'es', noun)
elif re.search('[^aeioudgkprt]h$', noun):
return re.sub('$', 'es', noun)
elif re.search('[^aeiou]y$', noun):
return re.sub('y$', 'ies', noun)
else:
return noun + 's'
Bien, esto es una expresión regular, pero usa una sintaxis que no vimos en
Capítulo 7, Expresiones regulares. Los corchetes significan “coincide
exactamente con uno de estos caracteres”. Así que [sxz] significa “s, o x, o
z”, pero sólo una de ellas. La $ debería serle conocida; coincide con el final de
>>> import re
>>> re.search('[abc]', 'Mark')
<_sre.SRE_Match object at 0x001C1FA8>
>>> re.sub('[abc]', 'o', 'Mark')
'Mork'
>>> re.sub('[abc]', 'o', 'rock')
'rook'
>>> re.sub('[abc]', 'o', 'caps')
'oops'
import re
def plural(noun):
if re.search('[sxz]$', noun):
return re.sub('$', 'es', noun)
elif re.search('[^aeioudgkprt]h$', noun):
return re.sub('$', 'es', noun)
elif re.search('[^aeiou]y$', noun):
return re.sub('y$', 'ies', noun)
else:
return noun + 's'
a, e, i, o o u.
Querría señalar sólo de pasada que es posible combinar estas dos expresiones
regulares (una para ver si se aplica la regla y otra para aplicarla) en una sola
expresión regular. Éste es el aspecto que tendría. En gran parte le será
familiar: estamos usando grupos a recordar, que ya aprendimos en
Sección 7.6, “Caso de estudio: análisis de números de teléfono”, para guardar
el carácter antes de la y. Y en la cadena de sustitución usamos una sintaxis
nueva, \1, que significa “eh, ¿sabes el primer grupo que recordaste? ponlo
aquí”. En este caso hemos recordado la c antes de la y y luego cuando
hacemos la sustitución ponemos c en el sitio de la c, e ies en el sitio de la y (si
tiene más de un grupo que recordar puede usar \2, \3, etc.).
import re
def match_sxz(noun):
return re.search('[sxz]$', noun)
def apply_sxz(noun):
return re.sub('$', 'es', noun)
def match_h(noun):
return re.search('[^aeioudgkprt]h$', noun)
def apply_h(noun):
return re.sub('$', 'es', noun)
def match_y(noun):
return re.search('[^aeiou]y$', noun)
def apply_y(noun):
return re.sub('y$', 'ies', noun)
def match_default(noun):
return 1
def apply_default(noun):
return noun + 's'
def plural(noun):
for matchesRule, applyRule in rules:
if matchesRule(noun):
return applyRule(noun)
Esta versión parece más complicada (ciertamente es más larga), pero hace
exactamente lo mismo: comprueba cuatro reglas en orden, y aplica la
expresión regular apropiada cuando encuentra una coincidencia. La
diferencia es que cada coincidencia y regla a aplicar individuales están
definidas en su propia función, y las funciones están listadas en esta variable
rules que es una tupla de tuplas.
def plural(noun):
if match_sxz(noun):
return apply_sxz(noun)
if match_h(noun):
return apply_h(noun)
if match_y(noun):
return apply_y(noun)
if match_default(noun):
return apply_default(noun)
Aquí el beneficio es que esa función plural queda simplificada. Toma una lista
de reglas definidas en otra parte e itera sobre ellas de manera genérica.
Tomamos una regla de coincidencia. ¿Coincide? Entonces llamamos a la regla
de modificación. Las reglas se pueden definir en cualquier sitio, de cualquier
manera. A la función plural no le importa.
Ahora bien, ¿merecía la pena añadir este nivel de abstracción? Bueno, aún no.
Consideremos lo que implicaría añadir una nueva regla a la función. En el
ejemplo anterior hubiera precisado añadir una sentencia if a la función plural.
En este ejemplo precisaría añadir dos funciones, match_foo y apply_foo, y luego
actualizar la lista rules para especificar en qué orden deberían invocarse las
nuevas funciones con relación a las otras reglas.
import re
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)
)
)
def plural(noun):
for matchesRule, applyRule in rules:
if matchesRule(noun):
return applyRule(noun)
Ahora todo lo que tenemos que hacer para añadir una regla es definir las
funciones directamente en la lista rules: una regla para coincidencia y otra de
transformación. Pero definir las funciones de reglas en línea de esta manera deja
muy claro que tenemos algo de duplicidad innecesaria. Tenemos cuatro pares
de funciones y todas siguen el mismo patrón. La función de comparación es una
simple llamada a re.search, y la función de transformación es una simple
llamada a re.sub. Saquemos el factor común de estas similaridades.
Eliminemos la duplicación del código para que sea más sencillo definir nuevas
reglas.
import re
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!
patterns = \
(
('[sxz]$', '$', 'es'),
('[^aeioudgkprt]h$', '$', 'es'),
('(qu|[^aeiou])y$', 'y$', 'ies'),
('$', '$', 's')
)
rules = map(buildMatchAndApplyFunctions, patterns)
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 esl
a de comparación que llama a re.search, y la segunda función es la de
transformación que llama a re.sub.
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:
if matchesRule(noun):
return applyRule(noun)
otro vistazo.
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
Footnotes
[sxz]$ $ es
[^aeioudgkprt]h$ $ es
[^aeiou]y$ y$ ies
$ $ s
import re
import string
Como pudo ver, cada línea del fichero tiene en realidad tres valores, pero
están separados por espacios en blanco (tabuladores o espacios, no hay
diferencia). Relacionar la función string.split sobre esta lista creará una
lista nueva en la que cada elemento es una tupla de tres cadenas. De manera
que una línea como [sxz]$ $ es quedará dividida en la tupla ('[sxz]$',
'$', 'es'). Esto significa que patterns contendrá una lista de tuplas, igual
Si patterns es una lista de tuplas, entonces rules será una lista de las
funciones creadas de forma dinámica con cada llamada a buildRule. Llamar
a buildRule(('[sxz]$', '$', 'es')) devuelve una función que toma un
único parámetros, word. Cuando se invoque esta función devuelta, ejecutará
re.search('[sxz]$', word) and re.sub('$', 'es', word).
Footnotes
import re
def rules(language):
for line in file('rules.%s' % language):
pattern, search, replace = line.split()
yield lambda word: re.search(pattern, word) and re.sub(search,
replace, word)
Esto usa una técnica llamada generadores que no voy siquiera a intentar
explicar hasta que eche un vistazo antes a un ejemplo más simple.
Para crear una instancia del generador make_counter basta invocarla como
cualquier otra función. Advierta que esto no ejecuta realmente el código de la
función. Lo sabemos porque la primera línea de make_counter es una
sentencia print, pero aún no se ha mostrado nada.
def fibonacci(max):
a, b = 0, 1
while a < max:
yield a
a, b = b, a+b
Ahora tenemos una función que escupe valores sucesivos de Fibonacci. Bien,
podíamos haber hecho eso con recursividad pero de esta manera es más fácil de
leer. Además, funciona bien con bucles for.
Por cada iteración sobre el bucle for n obtiene un valor nuevo de la sentencia
yield de fibonacci, y todo lo que hacemos es imprimirlo. Una vez fibonacci
llega al límite (a se hace mayor que max, que en este caso es 1000), entonces el
bucle for termina sin novedad.
def rules(language):
for line in file('rules.%s' % language):
for line in file(...) es una construcción común que se usa para leer
Aquí no hay magia. Recuerde que las líneas del fichero de reglas tienen tres
valores separados por espacios en blanco, así que line.split() devuelve una
tupla de 3 valores, y los asignamos a 3 variables locales.
Lecturas complementarias
Footnotes
[23] yielded
17.8. Resumen
• 18.1. Inmersión
• 18.2. Uso del módulo timeit
• 18.3. Optimización de expresiones regulares
• 18.4. Optimización de búsquedas en diccionarios
• 18.5. Optimización de operaciones con listas
• 18.6. Optimización de manipulación de cadenas
• 18.7. Resumen
18.1. Inmersión
Empecemos por aquí: ¿está seguro de que lo necesita? ¿Es tan malo su código?
¿Merece la pena el tiempo de afinarlo? Durante el tiempo de vida de su
aplicación, ¿cuánto tiempo va a pasar ese código ejecutándose, comparado al
tiempo que habrá de esperar por una base de datos remota o por entrada de un
usuario?
Otro ejemplo: Woo pasa a ser W99, que deriva en W9, que a su vez se convierte
en W, que hay que rellenar con ceros dejando W000.
import string, re
def soundex(source):
"convert string to Soundex equivalent"
# Soundex requirements:
# source string must be at least 1 character
# and must consist entirely of letters
allChars = string.uppercase + string.lowercase
if not re.search('^[%s]+$' % allChars, source):
return "0000"
# Soundex algorithm:
# 1. make first character uppercase
source = source[0].upper() + source[1:]
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())
La cosa más importante que debe saber sobre optimización de código en Python
es que no debería escribir su propia función de cronometraje.
El módulo timeit define una clase Timer que toma dos argumentos. Ambos
argumentos son cadenas. El primero es una sentencia que deseamos
cronometrar; en este caso cronometramos una llamada a la función Soundex
dentro de soundex con 'Pilgrim' como argumento. El segundo argumento de
la clase Timer es la sentencia que importará el entorno de la ejecución. timeit
crea internamente un entorno virtual aislado, ejecuta manualmente la
sentencia de configuración (importa el módulo soundex), y luego compila y
ejecuta manualmente la sentencia cronometrada (invocando la función
Soundex).
Una vez tenemos el objeto Timer, lo más sencillo es invocar timeit(), que
llama a nuestra función 1 millón de veces y devuelve el número de segundos
que le llevó hacerlo.
El otro método principal del objeto Timer es repeat(), que toma dos
argumentos opcionales. El primero es el número de veces que habrá de
repetir la prueba completa, y el segundo es el número de veces que ha de
llamar a la sentencia cronometrada dentro de cada prueba. Ambos
argumentos son opcionales y por omisión serán 3 y 1000000 respectivamente.
El método repeat() devuelve una lista del tiempo en segundos que llevó
terminar cada ciclo de prueba.
Observe que repeat() devuelve una lista de tiempos. Los tiempos serán
diferentes casi siempre, debido a ligeras variaciones en la cantidad de tiempo de
procesador que se le asigna al intérprete de Python (y esos dichosos procesos en
segundo plano de los que no se pudo librar). Su primera idea podría ser decir
“Hallemos la media, a la que llamaremos El Número Verdadero.”
De hecho, eso es incorrecto casi con seguridad. Las pruebas que tardaron más
no lo hicieron debido a variaciones en el código o en el intérprete de Python;
sino debido a esos fastidiosos procesos en segundo plano, y otros factores
ajenos al intérprete de Python que no se pudieron eliminar completamente. Si
los diferentes resultados de cronometraje difieren por más de un pequeño
porcentaje, aún tenemos demasiada variabilidad para confiar en los resultados.
En caso contrario tome el valor más pequeño y descarte los demás.
Python tiene una función min bastante a mano que toma una lista y devuelve el
valor más pequeño:
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())
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:
C:\samples\soundex\stage1>python soundex1b.py
Woo W000 17.1361133887
Pilgrim P426 21.8201693232
Flingjingwaller F452 32.7262294509
isOnlyChars = re.compile('^[A-Za-z]+$').search
def soundex(source):
if not isOnlyChars(source):
return "0000"
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 enterametne 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"
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:
C:\samples\soundex\stage1>python soundex1e.py
Woo W000 13.5069504644
Pilgrim P426 18.2199394057
Flingjingwaller F452 28.9975225902
import string, re
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())
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.
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().
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
lambda:
source = source.upper()
digits = source[0] + "".join([charToSoundex[c] for c in
source[1:]])
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
This is soundex/stage2/soundex2c.py:
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
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())
digits2 = digits[0]
for d in digits[1:]:
if digits2[-1] != d:
digits2 += d
C:\samples\soundex\stage2>python soundex2c.py
Woo W000 12.6070768771
Pilgrim P426 14.4033353401
Flingjingwaller F452 19.7774882003
digits2 = ''
last_digit = ''
for d in digits:
if d != last_digit:
digits2 += d
last_digit = d
C:\samples\soundex\stage3>python soundex3a.py
Woo W000 11.5346048171
Pilgrim P426 13.3950636184
Flingjingwaller F452 18.6108927252
¿Por qué no es más rápido soundex3a.py? Resulta que los índices de las listas en
Python son extremadamente eficientes. Acceder varias veces a digits2[-1] no
es problema. Por otro lado, mantener el último dígito visto en una variable
aparte significa que tenemos dos asignaciones de variables por cada dígito que
almacenamos, lo que elimina cualquier pequeña ganancia que hubiéramos
obtenido por eliminar la búsqueda en la lista.
Probemos algo totalmente diferente. Si es posible tratar una cadena como una
lista de caracteres, debería ser posible usar una lista por comprensión para
iterar sobre la lista. El problema es que el código necesita acceder al carácter
previo en la lista, y no es fácil hacerlo con una lista por comprensión sencilla.
Sin embargo, es posible crear una lista de números índice usando la función
range(), y usar esos números de índice para buscar progresivamente en la lista
y extraer cada carácter que sea diferente del anterior. Esto nos dará una lista de
caracteres y podemos usar el método de cadena join() para reconstruir una
cadena partiendo de ella.
Éste es soundex/stage3/soundex3b.py:
C:\samples\soundex\stage3>python soundex3b.py
Woo W000 14.2245271396
Pilgrim P426 17.8337165757
Flingjingwaller F452 25.9954005327
Es posible que las técnicas hasta ahora hayan sido “centradas en la caden”.
Python puede convertir una cadena en una lista de caracteres con una sola
orden: list('abc') devuelve ['a', 'b', 'c']. Más aún, las listas se pueden
modificar muy rápidamente. En lugar de crear incrementalmente una nueva lista
(o cadena) partiendo de la original, ¿por qué no trabajar con elementos dentro
de una única lista?
digits = list(source[0].upper() +
source[1:].translate(charToSoundex))
i=0
for item in digits:
if item==digits[i]: continue
i+=1
digits[i]=item
del digits[i+1:]
digits2 = "".join(digits)
¿Es más rápido que soundex3a.py o soundex3b.py? No, de hecho es el método
más lento:
C:\samples\soundex\stage3>python soundex3c.py
Woo W000 14.1662554878
Pilgrim P426 16.0397885765
Flingjingwaller F452 22.1789341942
import string, re
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())
El último paso del algoritmo Soundex es rellenar los resultados cortos con ceros
y truncar los largos. ¿Cual es la mejor manera de hacerlo?
C:\samples\soundex\stage2>python soundex2c.py
Woo W000 12.6070768771
Pilgrim P426 14.4033353401
Flingjingwaller F452 19.7774882003
La primera cosa que hemos de considerar es sustituir esa expresión regular con
un bucle. Este código es de soundex/stage4/soundex4a.py:
digits3 = ''
for d in digits2:
if d != '9':
digits3 += d
C:\samples\soundex\stage4>python soundex4a.py
Woo W000 6.62865531792
Pilgrim P426 9.02247576158
Flingjingwaller F452 13.6328416042
Pero espere un momento. ¿Un bucle para eliminar caracteres de una cadena?
Podemos usar un simple método de cadenas para eso. Aquí está
soundex/stage4/soundex4b.py:
C:\samples\soundex\stage4>python soundex4b.py
Woo W000 6.75477414029
Pilgrim P426 7.56652144337
Flingjingwaller F452 10.8727729362
Por último pero no por ello menos, examinemos los dos pasos finales del
algoritmo: rellenar los resultados cortos con ceros y truncar los resultados
largos a cuatro caracteres. El código que vimos en soundex4b.py hace eso
mismo, pero es horriblemente ineficiente. Eche un vistazo a
soundex/stage4/soundex4c.py para ver por qué:
digits3 += '000'
return digits3[:4]
¿Par qué necesitamos un bucle while sólo para rellenar el resultado? Sabemos
con antelación que vamos a truncar el resultado a cuatro caracteres, y ya
sabemos que tenemos al menos un carácter (la letra inicial, que llega sin
cambiar de la variable source original). Eso significa que podemos añadir
simplemente tres ceros a la salida y luego truncarla. No se ciña totalmente al
enunciado exacto del problema; verlo desde un ángulo ligeramente diferente
puede llevar a una solución más simple.
C:\samples\soundex\stage4>python soundex4c.py
Woo W000 4.89129791636
Pilgrim P426 7.30642134685
Flingjingwaller F452 10.689832367
Por último, aún hay una cosa que podemos hacer con esas tres líneas de código
para acelerarlas: podemos combinarlas en una sola. Eche un vistazo a
soundex/stage4/soundex4d.py:
Poner todo este código en una sola línea en soundex4d.py es ligeramente más
rápido que soundex4c.py:
C:\samples\soundex\stage4>python soundex4d.py
Woo W000 4.93624105857
Pilgrim P426 7.19747593619
Flingjingwaller F452 10.5490700634
18.7. Resumen
• 9.4. Unicode
o Unicode.org es la página del estándar unicode, e incluye una
breve introducción técnica.
o El Unicode Tutorial tiene muchos más ejemplos sobre el uso de
funciones unicode de Python, incluyendo la manera de forzar a
Python a convertir unicode en ASCII incluso cuando él
probablemente no querría.
o El PEP 263 entra en detalles sobre cómo y cuándo definir una
codificación de caracteres en sus ficheros .py.
• 11.1. Inmersión
o Paul Prescod cree que los servichos web HTTP puros son el futuro
de Internet.
• 12.1. Inmersión
o http://www.xmethods.net/ es un repositorio de servicios web
SOAP de acceso público.
o La especificación de SOAP es sorprendentemente legible, si es que
le gustan ese tipo de cosas.
• 12.8. Solución de problemas en servicios web SOAP
o New developments for SOAPpy analiza algunos intentos de
conectar a otro servicio SOAP que no funciona exactamente tal
como se esperaba.
• 15.5. Resumen
o XProgramming.com tiene enlaces para descargar infraestructuras
de prueba unitaria para muchos lenguajes distintos.
• 18.1. Inmersión
o Soundexing and Genealogy proporciona una cronología de la
evolución de Soundex y sus variantes regionales.
Apéndice B. Repaso en 5 minutos
• 1.9. Resumen
• 2.1. Inmersión
• 3.8. Resumen
El programa odbchelper.py y su salida debería tener total
sentido ahora.
• 4.1. Inmersión
• 4.9. Resumen
• 5.1. Inmersión
• 5.10. Resumen
• 6.7. Resumen
• 7.1. Inmersión
• 7.7. Resumen
• 8.1. Inmersión
• 8.10. Resumen
• 9.1. Inmersión
• 9.2. Paquetes
• 9.4. Unicode
• 9.7. Transición
• 10.8. Resumen
• 11.1. Inmersión
y zambullirnos en urllib2.
• 11.10. Resumen
ahora.
• 12.9. Resumen
• 13.2. Inmersión
Ahora que ya hemos definido completamente el
comportamiento esperado de nuestras funciones de
conversión, vamos a hacer algo un poco inesperado: vamos
a escribir una batería de pruebas que ponga estas funciones
contra las cuerdas y se asegure de que se comportan de la
manera en que queremos. Ha leído bien: va a escribir
código que pruebe código que aún no hemos escrito.
• 15.4. Epílogo
• 15.5. Resumen
• 16.1. Inmersión
• 16.8. Resumen
• 17.1. Inmersión
Quiero hablar sobre los sustantivos en plural. También
sobre funciones que devuelven otras funciones, expresiones
regulares avanzadas y generadores. Los generadores son
nuevos en Python 2.3. Pero primero hablemos sobre cómo
hacer nombres en plural[20].
• 17.8. Resumen
• 18.1. Inmersión
• 18.7. Resumen
• 2.1. Inmersión
Las comillas triples también son una manera sencilla de definir una
cadena que contenga comillas tanto simples como dobles, como
qq/.../ en Perl.
import en Python es como require en Perl. Una vez que hace import
conversión de tipos. Juntar una lista que tenga uno o más elementos
que no sean cadenas provocará una excepción.
Los métodos __init__ son opcionales, pero cuando define uno, debe
recordar llamar explícitamente al método __init__ del ancestro (si
define uno). Suele ocurrir que siempre que un descendiente quiera
extender el comportamiento de un ancestro, el método descendiente
deba llamar al del ancestro en el momento adecuado, con los
argumentos adecuados.
fileinfo_fromdict.py.
• 9.2. Paquetes
del paquete. No tiene por qué definir nada; puede ser un fichero
vacío, pero ha de existir. Pero si no existe __init__.py el directorio
es sólo eso, un directorio, no un paquete, y no puede ser importado o
contener módulos u otros paquetes.
• 13.2. Inmersión
unittest está incluido en Python 2.1 y posteriores. Los usuarios de
• 15.3. Refactorización
Siempre que vaya a usar una expresión regular más de una vez,
debería compilarla para obtener un objeto patrón y luego llamar
directamente a los métodos del patrón.
• 2.1. Inmersión
o Ejemplo 2.1. odbchelper.py
• 2.3. Documentación de funciones
o Ejemplo 2.2. Definición de la cadena de documentación de la
función buildConnectionString
• 2.4. Todo es un objeto
o Ejemplo 2.3. Acceso a la cadena de documentación de la función
buildConnectionString
• 2.4.1. La ruta de búsqueda de import
o Ejemplo 2.4. Ruta de búsqueda de import
• 2.5. Sangrado (indentado) de código
o Ejemplo 2.5. Sangrar la función buildConnectionString
o Ejemplo 2.6. Sentencias if
• 4.1. Inmersión
o Ejemplo 4.1. apihelper.py
o Ejemplo 4.2. Ejemplo de uso de apihelper.py
o Ejemplo 4.3. Uso avanzado de apihelper.py
• 4.2. Argumentos opcionales y con nombre
o Ejemplo 4.4. Llamadas válidas a info
• 4.3.1. La función type
o Ejemplo 4.5. Presentación de type
• 4.3.2. La función str
o Ejemplo 4.6. Presentación de str
o Ejemplo 4.7. Presentación de dir
o Ejemplo 4.8. Presentación de callable
• 4.3.3. Funciones incorporadas
o Ejemplo 4.9. Atributos y funciones incorporados
• 4.4. Obtención de referencias a objetos con getattr
o Ejemplo 4.10. Presentación de getattr
• 4.4.1. getattr con módulos
o Ejemplo 4.11. La función getattr en apihelper.py
• 4.4.2. getattr como dispatcher
o Ejemplo 4.12. Creación de un dispatcher con getattr
o Ejemplo 4.13. Valores por omisión de getattr
• 4.5. Filtrado de listas
o Ejemplo 4.14. Presentación del filtrado de listas
• 4.6. La peculiar naturaleza de and y or
o Ejemplo 4.15. Presentación de and
o Ejemplo 4.16. Presentación de or
• 4.6.1. Uso del truco the and-or
o Ejemplo 4.17. Presentación del truco and-or
o Ejemplo 4.18. Cuando falla el truco and-or
o Ejemplo 4.19. Utilización segura del truco and-or
• 4.7. Utilización de las funciones lambda
o Ejemplo 4.20. Presentación de las funciones lambda
• 4.7.1. Funciones lambda en el mundo real
o Ejemplo 4.21. split sin argumentos
• 4.8. Todo junto
o Ejemplo 4.22. Obtención de una cadena de documentación de
forma dinámica
o Ejemplo 4.23. ¿Por qué usar str con una cadena de
documentación?
o Ejemplo 4.24. Presentación de ljust
o Ejemplo 4.25. Mostrar una List
• 5.1. Inmersión
o Ejemplo 5.1. fileinfo.py
• 5.2. Importar módulos usando from módulo import
o Ejemplo 5.2. import módulo frente a from módulo import
• 5.3. Definición de clases
o Ejemplo 5.3. La clase más simple en Python
o Ejemplo 5.4. Definición de la clase FileInfo
• 5.3.1. Inicialización y programación de clases
o Ejemplo 5.5. Inicialización de la clase FileInfo
o Ejemplo 5.6. Programar la clase FileInfo
• 5.4. Instanciación de clases
o Ejemplo 5.7. Creación de una instancia de FileInfo
• 5.4.1. Recolección de basura
o Ejemplo 5.8. Intento de implementar una pérdida de memoria
• 5.5. Exploración de UserDict: Una clase cápsula
o Ejemplo 5.9. Definición de la clase UserDict
o Ejemplo 5.10. Métodos normales de UserDict
o Ejemplo 5.11. Herencia directa del tipo de datos dict
• 5.6.1. Consultar y modificar elementos
o Ejemplo 5.12. El método especial __getitem__
o Ejemplo 5.13. El método especial __setitem__
o Ejemplo 5.14. Reemplazo de __setitem__ en MP3FileInfo
o Ejemplo 5.15. Dar valor al name de una MP3FileInfo
• 5.7. Métodos especiales avanzados
o Ejemplo 5.16. Más métodos especiales de UserDict
• 5.8. Presentación de los atributos de clase
o Ejemplo 5.17. Presentación de los atributos de clase
o Ejemplo 5.18. Modificación de atributos de clase
• 5.9. Funciones privadas
o Ejemplo 5.19. Intento de invocación a un método privado
• 8.1. Inmersión
o Ejemplo 8.1. BaseHTMLProcessor.py
o Ejemplo 8.2. dialect.py
o Ejemplo 8.3. Salida de dialect.py
• 8.2. Presentación de sgmllib.py
o Ejemplo 8.4. Prueba de ejemplo de sgmllib.py
• 8.3. Extracción de datos de documentos HTML
o Ejemplo 8.5. Presentación de urllib
o Ejemplo 8.6. Presentación de urllister.py
o Ejemplo 8.7. Uso de urllister.py
• 8.4. Presentación de BaseHTMLProcessor.py
o Ejemplo 8.8. Presentación de BaseHTMLProcessor
o Ejemplo 8.9. Salida de BaseHTMLProcessor
• 8.5. locals y globals
o Ejemplo 8.10. Presentación de locals
o Ejemplo 8.11. Presentación de globals
o Ejemplo 8.12. locals es de sólo lectura, globals no
• 8.6. Cadenas de formato basadas en diccionarios
o Ejemplo 8.13. Presentación de la cadena de formato basada en
diccionarios
o Ejemplo 8.14. Formato basado en diccionarios en
BaseHTMLProcessor.py
o Ejemplo 8.15. Más cadenas de formato basadas en diccionarios
• 8.7. Poner comillas a los valores de los atributos
o Ejemplo 8.16. Poner comillas a valores de atributo
• 8.8. Presentación de dialect.py
o Ejemplo 8.17. Manipulación de etiquetas específicas
o Ejemplo 8.18. SGMLParser
o Ejemplo 8.19. Sustitución del método handle_data
• 8.9. Todo junto
o Ejemplo 8.20. La función translate, parte 1
o Ejemplo 8.21. La función translate, parte 2: curiorífico y curiorífico
o Ejemplo 8.22. La función translate, parte 3
• 9.1. Inmersión
o Ejemplo 9.1. kgp.py
o Ejemplo 9.2. toolbox.py
o Ejemplo 9.3. Ejemplo de la salida de kgp.py
o Ejemplo 9.4. Salida de kgp.py, más simple
• 9.2. Paquetes
o Ejemplo 9.5. Carga de un documento XML (vistazo rápido)
o Ejemplo 9.6. Estructura de ficheros de un paquete
o Ejemplo 9.7. Los paquetes también son módulos
• 9.3. Análisis de XML
o Ejemplo 9.8. Carga de un documento XML (ahora de verdad)
o Ejemplo 9.9. Obtener nodos hijos
o Ejemplo 9.10. toxml funciona en cualquier nodo
o Ejemplo 9.11. Los nodos hijos pueden ser un texto
o Ejemplo 9.12. Explorando en busca del texto
• 9.4. Unicode
o Ejemplo 9.13. Presentación de unicode
o Ejemplo 9.14. Almacenamiento de caracteres no ASCII
o Ejemplo 9.15. sitecustomize.py
o Ejemplo 9.16. Efectos de cambiar la codificación por omisión
o Ejemplo 9.17. Especificación de la codificación en ficheros .py
o Ejemplo 9.18. russiansample.xml
o Ejemplo 9.19. Análisis de russiansample.xml
• 9.5. Búsqueda de elementos
o Ejemplo 9.20. binary.xml
o Ejemplo 9.21. Presentación de getElementsByTagName
o Ejemplo 9.22. Puede buscar cualquier elemento
o Ejemplo 9.23. La búsqueda es recursiva
• 9.6. Acceso a atributos de elementos
o Ejemplo 9.24. Acceso a los atributos de un elemento
o Ejemplo 9.25. Acceso a atributos individuales
• 11.1. Inmersión
o Ejemplo 11.1. openanything.py
• 11.2. Cómo no obtener datos mediante HTTP
o Ejemplo 11.2. Descarga de una sindicación a la manera rápida y
fea
• 11.4. Depuración de servicios web HTTP
o Ejemplo 11.3. Depuración de HTTP
• 11.5. Establecer el User-Agent
o Ejemplo 11.4. Presentación de urllib2
o Ejemplo 11.5. Añadir cabeceras con la Request
• 11.6. Tratamiento de Last-Modified y ETag
o Ejemplo 11.6. Pruebas con Last-Modified
o Ejemplo 11.7. Definición de manipuladores de URL
o Ejemplo 11.8. Uso de manipuladores URL personalizados
o Ejemplo 11.9. Soporte de ETag/If-None-Match
• 11.7. Manejo de redirecciones
o Ejemplo 11.10. Acceso a servicios web sin admitir redirecciones
o Ejemplo 11.11. Definición del manipulador de redirección
o Ejemplo 11.12. Uso del manejador de redirección para detectar
redirecciones permanentes
o Ejemplo 11.13. Uso del manejador de redirección para detectar
redirecciones temporales
• 11.8. Tratamiento de datos comprimidos
o Ejemplo 11.14. Le decimos al servidor que queremos datos
comprimidos
o Ejemplo 11.15. Descompresión de los datos
o Ejemplo 11.16. Descompresión de datos directamente del servidor
• 11.9. Todo junto
o Ejemplo 11.17. La función openanything
o Ejemplo 11.18. La función fetch
o Ejemplo 11.19. Uso de openanything.py
• 12.1. Inmersión
o Ejemplo 12.1. search.py
o Ejemplo 12.2. Ejemplo de uso de search.py
• 12.2.1. Instalación de PyXML
o Ejemplo 12.3. Verificación de la instalación de PyXML
• 12.2.2. Instalación de fpconst
o Ejemplo 12.4. Verificación de la instalación de fpconst
• 12.2.3. Instalación de SOAPpy
o Ejemplo 12.5. Verificación de la instalación de SOAPpy
• 12.3. Primeros pasos con SOAP
o Ejemplo 12.6. Obtención de la temperatura actual
• 12.4. Depuración de servicios web SOAP
o Ejemplo 12.7. Depuración de servicios web SOAP
• 12.6. Introspección de servicios web SOAP con WSDL
o Ejemplo 12.8. Descubrimiento de los métodos disponibles
o Ejemplo 12.9. Descubrimiento de los argumentos de un método
o Ejemplo 12.10. Descubrimiento de los valores de retorno de un
método
o Ejemplo 12.11. Invocación de un servicio web mediante un proxy
WSDL
• 12.7. Búsqueda en Google
o Ejemplo 12.12. Introspección de los Google Web Services
o Ejemplo 12.13. Búsqueda en Google
o Ejemplo 12.14. Acceso a información secundaria de Google
• 12.8. Solución de problemas en servicios web SOAP
o Ejemplo 12.15. Invocación de un método con un proxy
configurado incorrectamente
o Ejemplo 12.16. Invocación de un método con argumentos
equivocados
o Ejemplo 12.17. Invocación de un método esperando una cantidad
errónea de valores de retorno
o Ejemplo 12.18. Invocación de un método con un error específico a
la aplicació
• 16.1. Inmersión
o Ejemplo 16.1. regression.py
o Ejemplo 16.2. Salida de ejemplo de regression.py
• 16.2. Encontrar la ruta
o Ejemplo 16.3. fullpath.py
o Ejemplo 16.4. Más explicaciones sobre os.path.abspath
o Ejemplo 16.5. Salida de ejemplo de fullpath.py
o Ejemplo 16.6. Ejecución de scripts en el directorio actual
• 16.3. Revisión del filtrado de listas
o Ejemplo 16.7. Presentación de filter
o Ejemplo 16.8. filter en regression.py
o Ejemplo 16.9. Filtrado usando listas de comprensión esta vez
• 16.4. Revisión de la relación de listas
o Ejemplo 16.10. Presentación de map
o Ejemplo 16.11. map con listas de tipos de datos distintos
o Ejemplo 16.12. map en regression.py
• 16.6. Importación dinámica de módulos
o Ejemplo 16.13. Importación de varios módulos a la vez
o Ejemplo 16.14. Importación dinámica de módulos
o Ejemplo 16.15. Importación de una lista de módulos de forma
dinámica
• 16.7. Todo junto
o Ejemplo 16.16. La función regressionTest
o Ejemplo 16.17. Paso 1: Obtener todos los ficheros
o Ejemplo 16.18. Paso 2: Filtrar para obtener los ficheros que nos
interesan
o Ejemplo 16.19. Paso 3: Relacionar los nombres de fichero a los de
módulos
o Ejemplo 16.20. Paso 4: Relacionar los nombres de módulos a
módulos
o Ejemplo 16.21. Paso 5: Cargar los módulos en la batería de
pruebas
o Ejemplo 16.22. Paso 6: Decirle a unittest que use nuestra batería de
pruebas
• 18.1. Inmersión
o Ejemplo 18.1. soundex/stage1/soundex1a.py
• 18.2. Uso del módulo timeit
o Ejemplo 18.2. Presentación de timeit
• 18.3. Optimización de expresiones regulares
o Ejemplo 18.3. El mejor resultado por mucho:
soundex/stage1/soundex1e.py
• 18.4. Optimización de búsquedas en diccionarios
o Ejemplo 18.4. El mejor resultado hasta ahora:
soundex/stage2/soundex2c.py
• 18.5. Optimización de operaciones con listas
o Ejemplo 18.5. El mejor resultado hasta ahora:
soundex/stage2/soundex2c.py
Apéndice E. Historial de revisiones
Si está usted interesado en aprender más sobre DocBook para escribir textos
técnicos, puede descargar los ficheros fuente XML, y los scripts de compilación,
que incluyen también las hojas de estilo XSL modificadas que se usan para crear
los distintos formatos. Debería leer también el libro canónico, DocBook: The
Definitive Guide. Si desea hacer algo serio con DocBook, le recomiendo que se
suscriba a las listas de correo de DocBook.