Fluent Python - Clear, Concise, and Effective Programming Español
Fluent Python - Clear, Concise, and Effective Programming Español
Python fluido
La simplicidad de Python le permite volverse productivo rápidamente, pero esto a menudo “Estoy orgulloso de haber sido
significa que no está usando todo lo que tiene para ofrecer. Con esta guía práctica, aprenderá a
un revisor técnico para esto
escribir código Python idiomático y eficaz aprovechando sus mejores funciones, y posiblemente
las más olvidadas. El autor Luciano Ramalho lo lleva a través de las funciones y bibliotecas del
excelente libro, no solo
lenguaje central de Python y le muestra cómo hacer que su código sea más corto, más rápido y ¿Ayudará a muchos?
más legible al mismo tiempo. Python intermedio
Muchos programadores experimentados intentan adaptar Python para que se ajuste a los programadores en su
patrones que aprendieron de otros lenguajes y nunca descubren características de Python fuera
camino hacia la maestría,
de su experiencia. Con este libro, los programadores de Python aprenderán a fondo cómo
dominar Python 3.
pero me ha enseñado bastante
¡Algunas cosas también!
Este libro cubre: —Álex Martelli
Miembro de la Fundación de Software Python
ÿ El modelo de datos de Python: comprender cómo se utilizan los métodos especiales
la clave para el comportamiento consistente de los objetos
ÿ Modismos orientados a objetos: construya clases aprendiendo sobre codificadores Python avanzados
referencias, mutabilidad, interfaces, sobrecarga de operadores y herencia múltiple que quiere empujar el
límites de sus
ÿ Flujo de control: aproveche los administradores de contexto, generadores,
conocimiento."
rutinas y concurrencia con los paquetes concurrent.futures y asyncio
—Daniel y Audrey Roy Greenfeld
autores de Two Scoops of Django
PROGRAMACIÓN/PYTHON
Twitter: @oreillymedia
facebook.com/oreilly
EE.UU. $49,99 57,99 dólares canadienses
ISBN: 978-1-491-9-46008
Machine Translated by Google
Python fluido
Luciano Ramallo
Bostón
Machine Translated by Google
Python fluido
por Luciano Ramalho
Copyright © 2015 Luciano Gama de Sousa Ramalho. Reservados todos los derechos.
Publicado por O'Reilly Media, Inc., 1005 Gravenstein Highway North, Sebastopol, CA 95472.
Los libros de O'Reilly se pueden comprar con fines educativos, comerciales o de promoción de ventas. Las ediciones en línea también están
disponibles para la mayoría de los títulos (http:// safaribooksonline.com). Para obtener más información, comuníquese con nuestro departamento
de ventas corporativo/institucional: 800-998-9938 o [email protected].
Consulte http:// oreilly.com/ catalog/ errata.csp?isbn=9781491946008 para conocer los detalles de la versión.
El logotipo de O'Reilly es una marca comercial registrada de O'Reilly Media, Inc. Fluent Python, la imagen de portada y la imagen comercial
relacionada son marcas comerciales de O'Reilly Media, Inc.
Si bien el editor y el autor se han esforzado de buena fe para garantizar que la información y las instrucciones contenidas en este trabajo sean
precisas, el editor y el autor renuncian a toda responsabilidad por errores u omisiones, incluida, entre otras, la responsabilidad por los daños
que resulten del uso o la confianza. en este trabajo. El uso de la información e instrucciones contenidas en este trabajo es bajo su propio
riesgo. Si alguna muestra de código u otra tecnología que este trabajo contiene o describe está sujeta a licencias de código abierto o a los
derechos de propiedad intelectual de otros, es su responsabilidad asegurarse de que su uso cumpla con dichas licencias y/o derechos.
ISBN: 978-1-491-94600-8
[LSI]
Machine Translated by Google
Tabla de contenido
Prefacio. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . XV
Parte I. Prólogo
Representación de cadenas 11
de un tipo personalizado 12
Otras lecturas 15
Expresiones generadoras 25
Desempaquetado de tuplas 27
v
Machine Translated by Google
rebanar 33
Rebanar objetos 34
Asignación a sectores 36
arreglos 48
Vistas de memoria 51
NumPy y SciPy 52
Otras lecturas 59
3. Diccionarios y Conjuntos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
Comprensiones de dict 66
El método __perdido__ 72
Variaciones de dict 75
Subclasificación de UserDict 76
Asignaciones inmutables 77
Literales de conjuntos
79
de teoría de conjuntos 80
Establecer comprensiones 81
Establecer operaciones 82
dictar y establecer Bajo el capó 85
Un experimento de rendimiento 85
Tablas hash en diccionarios 87
vi | Tabla de contenido
Machine Translated by Google
Otras lecturas 94
Esenciales de byte 99
funciones os 130
adicionales 237
239
240
242
243
Tabla de contenido | ix
Machine Translated by Google
x | Tabla de contenido
Machine Translated by Google
Tabla de contenido | xi
Machine Translated by Google
adicionales 530
530
531
xi | Tabla de contenido
Machine Translated by Google
Epílogo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 683
Índice. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 725
Prefacio
Este es el plan: cuando alguien use una característica que no entiendas, simplemente dispárale.
Esto es más fácil que aprender algo nuevo, y en poco tiempo los únicos codificadores vivos
estarán escribiendo en un pequeño subconjunto fácil de entender de Python 0.9.6 <guiño>.1
—Tim Peters
Desarrollador principal legendario y autor de The Zen of Python
"Python es un lenguaje de programación potente y fácil de aprender". Esas son las primeras palabras del
tutorial oficial de Python. Eso es cierto, pero hay una trampa: debido a que el lenguaje es fácil de aprender y
poner en uso, muchos programadores practicantes de Python aprovechan solo una fracción de sus poderosas
características.
Un programador experimentado puede comenzar a escribir código Python útil en cuestión de horas.
A medida que las primeras horas productivas se convierten en semanas y meses, muchos desarrolladores
continúan escribiendo código Python con un acento muy fuerte derivado de los lenguajes aprendidos anteriormente.
Incluso si Python es su primer idioma, a menudo en la academia y en los libros introductorios se presenta
evitando cuidadosamente las características específicas del idioma.
Como profesor que presenta Python a programadores con experiencia en otros lenguajes, veo otro problema
que este libro trata de abordar: solo nos perdemos cosas que conocemos.
Viniendo de otro idioma, cualquiera puede adivinar que Python admite expresiones regulares y buscarlo en los
documentos. Pero si nunca antes ha visto el desempaquetado de tuplas o los descriptores, probablemente no
los buscará y puede terminar sin usar esas funciones solo porque son específicas de Python.
Este libro no es una referencia exhaustiva de Python de la A a la Z. Su énfasis está en las características del
lenguaje que son exclusivas de Python o que no se encuentran en muchos otros lenguajes populares. Este es
también principalmente un libro sobre el lenguaje central y algunas de sus bibliotecas.
Rara vez hablaré de paquetes que no están en la biblioteca estándar, aunque el
XV
Machine Translated by Google
El índice de paquetes de Python ahora enumera más de 60,000 bibliotecas y muchas de ellas son
increíblemente útiles.
Sin embargo, Fluent Python se trata de aprovechar al máximo Python 3.4, y no detallo las correcciones
necesarias para que el código funcione en versiones anteriores. La mayoría de los ejemplos deberían
ejecutarse en Python 2.7 con pocos o ningún cambio, pero en algunos casos, la adaptación requeriría
una reescritura significativa.
Habiendo dicho eso, creo que este libro puede ser útil incluso si debe ceñirse a Python 2.7, porque los
conceptos básicos siguen siendo los mismos. Python 3 no es un lenguaje nuevo y la mayoría de las
diferencias se pueden aprender en una tarde. What's New in Python 3.0 es un buen punto de partida.
Por supuesto, ha habido cambios desde que se lanzó Python 3.0 en 2009, pero ninguno tan importante
como los de 3.0.
Si no está seguro de saber lo suficiente sobre Python para seguirlo, revise los temas del tutorial oficial
de Python. Los temas tratados en el tutorial no se explicarán aquí, a excepción de algunas funciones
que son nuevas en Python 3.
Traté de enfatizar el uso de lo que está disponible antes de discutir cómo construir uno propio.
Por ejemplo, en la Parte II, el Capítulo 2 cubre los tipos de secuencia que están listos para usar,
incluidos algunos que no reciben mucha atención, como collections.deque. La creación de secuencias
definidas por el usuario solo se trata en la Parte IV, donde también vemos cómo aprovechar las clases
base abstractas (ABC) de collections.abc. La creación de su propio ABC se discute incluso
xi | Prefacio
Machine Translated by Google
más adelante en la Parte IV, porque creo que es importante sentirse cómodo usando un ABC antes de
escribir uno propio.
Este enfoque tiene algunas ventajas. En primer lugar, saber qué está listo para usar puede evitar que
tenga que reinventar la rueda. Usamos las clases de colección existentes con más frecuencia que
implementamos las nuestras, y podemos prestar más atención al uso avanzado de las herramientas
disponibles aplazando la discusión sobre cómo crear otras nuevas. También es más probable que
heredemos de los ABC existentes que crear un nuevo ABC desde cero. Y finalmente, creo que es más
fácil entender las abstracciones después de haberlas visto en acción.
La desventaja de esta estrategia son las referencias hacia adelante dispersas a lo largo de los capítulos.
Espero que estos sean más fáciles de tolerar ahora que sabes por qué elegí este camino.
Parte II
Los capítulos de esta parte cubren el uso de tipos de colección: secuencias, asignaciones y
conjuntos, así como la división de str versus bytes , la causa de mucha celebración entre los
usuarios de Python 3 y mucho dolor para los usuarios de Python 2 que aún no tienen migraron sus
bases de código. Los objetivos principales son recordar lo que ya está disponible y explicar algunos
comportamientos que a veces sorprenden, como el reordenamiento de las teclas de dictado cuando
no estamos mirando, o las advertencias de la clasificación de cadenas Unicode dependiente de la
configuración regional. Para lograr estos objetivos, la cobertura es a veces de alto nivel y amplia
(p. ej., cuando se presentan muchas variaciones de secuencias y asignaciones) y, a veces,
profunda (p. ej., cuando nos sumergimos en las tablas hash debajo de los tipos dict y set ).
Parte III
Aquí hablamos de funciones como objetos de primera clase en el lenguaje: qué significa eso, cómo
afecta algunos patrones de diseño populares y cómo implementar decoradores de funciones
aprovechando los cierres. Aquí también se cubre el concepto general de invocables en Python,
atributos de funciones, introspección, anotaciones de parámetros y la nueva declaración no local
en Python 3.
Parte IV
Ahora la atención se centra en la creación de clases. En la Parte II, la declaración de clase aparece
en algunos ejemplos; La Parte IV presenta muchas clases. Como cualquier lenguaje orientado a
objetos (OO), Python tiene su conjunto particular de características que pueden o no estar
presentes en el lenguaje en el que usted y yo aprendimos la programación basada en clases. Los
capítulos explican cómo funcionan las referencias, qué significa realmente la mutabilidad, el ciclo de vida de
Prefacio | xvii
Machine Translated by Google
ces, cómo crear sus propias colecciones y ABC, cómo hacer frente a la herencia múltiple y cómo
implementar la sobrecarga de operadores, cuando eso tiene sentido.
Parte V
En esta parte se tratan las construcciones y bibliotecas del lenguaje que van más allá del flujo de
control secuencial con condicionales, bucles y subrutinas. Comenzamos con los generadores, luego
visitamos los administradores de contexto y las corrutinas, incluido el nuevo rendimiento desafiante
pero poderoso de la sintaxis. La Parte V cierra con una introducción de alto nivel a la concurrencia
moderna en Python con collections.futures (usando subprocesos y procesos ocultos con la ayuda de
futuros) y haciendo E/S orientada a eventos con asyncio (aprovechando futuros sobre corrutinas y
rendimiento de).
Parte VI
Esta parte comienza con una revisión de las técnicas para construir clases con atributos creados
dinámicamente para manejar datos semiestructurados como conjuntos de datos JSON. A
continuación, cubrimos el mecanismo de propiedades familiar, antes de sumergirnos en cómo
funciona el acceso a atributos de objetos en un nivel inferior en Python usando descriptores. Se
explica la relación entre funciones, métodos y descriptores. A lo largo de la Parte VI, la implementación
paso a paso de una biblioteca de validación de campos descubre problemas sutiles que conducen al
uso de las herramientas avanzadas del capítulo final: decoradores de clases y metaclases.
Enfoque práctico
A menudo, usaremos la consola interactiva de Python para explorar el lenguaje y las bibliotecas. Siento
que es importante enfatizar el poder de esta herramienta de aprendizaje, particularmente para aquellos
lectores que han tenido más experiencia con lenguajes compilados estáticos que no proporcionan un ciclo
de lectura-evaluación-impresión # (REPL).
Uno de los paquetes de prueba estándar de Python, doctest, funciona simulando sesiones de consola y
verificando que las expresiones evalúen las respuestas mostradas. Usé doctest para comprobar la mayor
parte del código de este libro, incluidos los listados de la consola. No es necesario que use ni conozca
doctest para seguirlo: la característica clave de doctests es que parecen transcripciones de sesiones
interactivas de la consola de Python, por lo que puede probar fácilmente las demostraciones usted mismo.
A veces explicaré lo que queremos lograr mostrando un doctest antes del código que lo hace pasar.
Establecer firmemente lo que se debe hacer antes de pensar en cómo hacerlo ayuda a enfocar nuestro
esfuerzo de codificación. Escribir primero las pruebas es la base del desarrollo basado en pruebas (TDD)
y también lo he encontrado útil cuando enseño. Si no está familiarizado con doctest, eche un vistazo a su
documentación y al repositorio de código fuente de este libro .
Descubrirá que puede verificar la corrección de la mayor parte del código en el libro escribiendo python3
-m doctest example_script.py en el shell de comandos de su sistema operativo.
xviii | Prefacio
Machine Translated by Google
Jerga de Python
Quería que este fuera un libro no solo sobre Python sino también sobre la cultura que lo rodea.
Durante más de 20 años de comunicación, la comunidad de Python ha desarrollado su propia jerga y
acrónimos particulares. Al final de este libro, Python Jergon contiene una lista de términos que tienen un
significado especial entre Pythonistas.
Casi todo el código del libro debería funcionar con cualquier intérprete compatible con Python 3.x, incluido
PyPy3 2.4.0, que es compatible con Python 3.2.5. Las excepciones notables son los ejemplos que usan
yield from y asyncio, que solo están disponibles en Python 3.3 o posterior.
La mayoría del código también debería funcionar con Python 2.7 con cambios menores, excepto los
ejemplos relacionados con Unicode en el Capítulo 4 y las excepciones ya señaladas para las versiones de
Python 3 anteriores a la 3.3.
Prefacio | xix
Machine Translated by Google
Itálico
Indica nuevos términos, URL, direcciones de correo electrónico, nombres de archivo y extensiones de archivo.
Ancho constante Se
utiliza para listas de programas, así como dentro de párrafos para hacer referencia a elementos de programas como
nombres de variables o funciones, bases de datos, tipos de datos, variables de entorno, declaraciones y palabras clave.
Tenga en cuenta que cuando un salto de línea cae dentro de un término de ancho_constante , no se agrega un guión; podría
malinterpretarse como parte del término.
el texto que se debe reemplazar con valores proporcionados por el usuario o por valores determinados por el contexto.
XX | Prefacio
Machine Translated by Google
Apreciamos, pero no requerimos, atribución. Una atribución suele incluir el título, el autor, el editor y el
ISBN. Por ejemplo: “Fluent Python de Luciano Ramalho (O'Reilly). Copyright 2015 Luciano Ramalho,
978-1-491-94600-8.”
Los profesionales de la tecnología, los desarrolladores de software, los diseñadores web y los profesionales
creativos y de negocios utilizan Safari Books Online como su recurso principal para la investigación, la resolución
de problemas, el aprendizaje y la capacitación para la certificación.
Safari Books Online ofrece una gama de mezclas de productos y programas de precios para
organizaciones, agencias gubernamentales e individuos. Los suscriptores tienen acceso a miles de
libros, videos de capacitación y manuscritos previos a la publicación en una base de datos de búsqueda
completa de editoriales como O'Reilly Media, Prentice Hall Professional, Addison-Wesley Professional,
Microsoft Press, Sams, Que, Peachpit Press, Focal Press , Cisco Press, John Wiley & Sons, Syngress,
Morgan Kaufmann, IBM Redbooks, Packt, Adobe Press, FT Press, Apress, Manning, New Riders,
McGraw-Hill, Jones & Bartlett, Course Technology y muchos más. Para obtener más información sobre
Safari Books Online, visítenos en línea.
Cómo contactarnos
Dirija sus comentarios y preguntas sobre este libro a la editorial:
Tenemos una página web para este libro, donde enumeramos las erratas, ejemplos y cualquier
información adicional. Puede acceder a esta página en http:// bit.ly/ fluent-python.
Para comentar o hacer preguntas técnicas sobre este libro, envíe un correo electrónico a bookques
[email protected].
Para obtener más información sobre nuestros libros, cursos, conferencias y noticias, visite nuestro sitio
web en http:// www.oreilly.com.
Prefacio | xxx
Machine Translated by Google
Expresiones de gratitud
El juego de ajedrez Bauhaus de Josef Hartwig es un ejemplo de excelente diseño: hermoso, simple y claro.
Guido van Rossum, hijo de un arquitecto y hermano de un maestro diseñador de fuentes, creó una obra maestra
del diseño de lenguaje. Me encanta enseñar Python porque es hermoso, simple y claro.
Alex Martelli y Anna Ravenscroft fueron las primeras personas en ver el esquema de este libro y me animaron a
enviarlo a O'Reilly para su publicación. Sus libros me enseñaron Python idiomático y son modelos de claridad,
precisión y profundidad en la escritura técnica.
Las más de 5000 publicaciones de Stack Overflow de Alex son una fuente de información sobre el lenguaje y su
uso adecuado.
Martelli y Ravenscroft también fueron revisores técnicos de este libro, junto con Lennart Regebro y Leonardo
Rochael. Todos en este destacado equipo de revisión técnica tienen al menos 15 años de experiencia en Python,
con muchas contribuciones a proyectos Python de alto impacto en estrecho contacto con otros desarrolladores
de la comunidad. Juntos me enviaron cientos de correcciones, sugerencias, preguntas y opiniones, agregando
un valor tremendo al libro. Victor Stinner revisó amablemente el Capítulo 18, aportando su experiencia como
mantenedor de asyncio al equipo de revisión técnica. Fue un gran privilegio y un placer colaborar con ellos
durante estos últimos meses.
La editora Meghan Blanchette fue una mentora sobresaliente, me ayudó a mejorar la organización y el flujo del
libro, me hizo saber cuándo estaba aburrido y evitó que me retrasara aún más. Brian MacDonald editó capítulos
en la Parte III mientras Meghan estaba fuera. Disfruté trabajar con ellos y con todas las personas con las que
me comuniqué en O'Reilly, incluido el equipo de soporte y desarrollo de Atlas (Atlas es la plataforma de
publicación de libros de O'Reilly, que tuve la suerte de utilizar para escribir este libro).
Mario Domenech Goulart proporcionó numerosas sugerencias detalladas a partir del primer lanzamiento
anticipado. También recibí valiosos comentarios de Dave Pawson, Elias Dorneles, Leonardo Alexandre Ferreira
Leite, Bruce Eckel, JS Bueno, Rafael Gonçalves, Alex Chiaranda, Guto Maia, Lucas Vido y Lucas Brunialti.
A lo largo de los años, varias personas me instaron a convertirme en autor, pero los más persuasivos fueron
Rubens Prates, Aurelio Jargas, Rudá Moura y Rubens Altimari. Mauricio Bussab me abrió muchas puertas,
incluida mi primera oportunidad real de escribir un libro. Renzo Nuccitelli apoyó este proyecto de escritura en
todo momento, incluso si eso significó un comienzo lento para nuestra asociación en python.pro.br.
XXII | Prefacio
Machine Translated by Google
Dorneles Tremea fue un gran amigo (increíblemente generoso con su tiempo y conocimiento), un
hacker increíble y el líder más inspirador de la Asociación Brasileña de Python.
Nos dejó demasiado pronto.
Mis alumnos a lo largo de los años me enseñaron mucho a través de sus preguntas, ideas, comentarios
y soluciones creativas a los problemas. Érico Andrei y Simples Consultoria me permitieron enfocarme
en ser profesor de Python por primera vez.
Martijn Faassen fue mi mentor de Grok y compartió conmigo conocimientos invaluables sobre Python
y los neandertales. Su trabajo y el de Paul Everitt, Chris McDonough, Tres Seaver, Jim Fulton, Shane
Hathaway, Lennart Regebro, Alan Runyan, Alexander Limi, Martijn Pieters, Godefroid Chapelle y otros
de los planetas Zope, Plone y Pyramid han sido decisivos en mi carrera. Gracias a Zope y a navegar
por la primera ola web, pude comenzar a ganarme la vida con Python en 1998. José Octavio Castro
Neves fue mi socio en la primera casa de software centrada en Python en Brasil.
Tengo demasiados gurús en la comunidad Python más amplia para enumerarlos a todos, pero además
de los ya mencionados, estoy en deuda con Steve Holden, Raymond Hettinger, AM Kuchling, David
Beazley, Fredrik Lundh, Doug Hellmann, Nick Coghlan, Mark Pilgrim, Martijn Pieters, Bruce Eckel,
Michele Simionato, Wesley Chun, Brandon Craig Rhodes, Philip Guo, Daniel Greenfeld, Audrey Roy y
Brett Slatkin por enseñarme nuevas y mejores formas de enseñar Python.
La mayoría de estas páginas fueron escritas en la oficina de mi casa y en dos laboratorios: CoffeeLab
y Garoa Hacker Clube. CoffeeLab es la sede de los geeks de la cafeína en Vila Madalena, São Paulo,
Brasil. Garoa Hacker Clube es un hackerspace abierto a todos: un laboratorio comunitario donde
cualquiera puede probar libremente nuevas ideas.
La comunidad de Garoa proporcionó inspiración, infraestructura y holgura. Creo que Aleph disfrutaría
este libro.
Mi madre, María Lucía, y mi padre, Jairo, siempre me apoyaron en todo sentido. Ojalá estuviera aquí
para ver el libro; Me alegro de poder compartirlo con ella.
Mi esposa, Marta Mello, soportó 15 meses de un esposo que siempre estaba trabajando, pero siguió
apoyándome y guiándome en algunos momentos críticos del proyecto cuando temía que podría
abandonar el maratón.
Prefacio | XXIII
Machine Translated by Google
Machine Translated by Google
PARTE I
Prólogo
Machine Translated by Google
Machine Translated by Google
CAPÍTULO 1
El sentido de la estética del diseño del lenguaje de Guido es asombroso. He conocido a muchos buenos
diseñadores de lenguajes que podrían crear lenguajes teóricamente hermosos que nadie usaría nunca,
pero Guido es una de esas personas raras que pueden crear un lenguaje que es un poco menos hermoso
teóricamente pero que, por lo tanto, es un placer escribir programas en él. .1
— Jim Hugunin
Creador de Jython, cocreador de AspectJ, arquitecto de .Net DLR
Una de las mejores cualidades de Python es su consistencia. Después de trabajar con Python por un
tiempo, puede comenzar a hacer conjeturas correctas e informadas sobre las características que son
nuevas para usted.
Sin embargo, si aprendió otro lenguaje orientado a objetos antes de Python, es posible que le haya
resultado extraño usar len(colección) en lugar de colección.len(). Esta aparente rareza es la punta de un
iceberg que, cuando se entiende correctamente, es la clave de todo lo que llamamos Pythonic. El
iceberg se llama modelo de datos de Python y describe la API que puede usar para hacer que sus
propios objetos funcionen bien con las funciones de lenguaje más idiomáticas.
Puede pensar en el modelo de datos como una descripción de Python como marco. Formaliza las
interfaces de los componentes básicos del propio lenguaje, como secuencias, iteradores, funciones,
clases, administradores de contexto, etc.
Mientras codifica con cualquier marco, pasa mucho tiempo implementando métodos que son llamados
por el marco. Lo mismo sucede cuando aprovecha el modelo de datos de Python. El intérprete de
Python invoca métodos especiales para realizar operaciones básicas de objetos, a menudo
desencadenadas por una sintaxis especial. Los nombres de métodos especiales siempre se escriben
con guiones bajos dobles al principio y al final (es decir, __getitem__). Por ejemplo, la sinÿ
1. Historia de Jython, escrita como prólogo de Jython Essentials (O'Reilly, 2002), por Samuele Pedroni y Noel
rapeando
3
Machine Translated by Google
tax obj[key] es compatible con el método especial __getitem__ . Para evaluar mi_colección[clave], el
intérprete llama a mi_colección.__getitem__(clave).
Los nombres de métodos especiales permiten que sus objetos implementen, admitan e interactúen con
construcciones de lenguaje básicas como:
• Iteración
• Colecciones
• Acceso a atributos
• Sobrecarga de operadores
• Invocación de funciones y métodos
Magia y Dunder
El término método mágico es argot para método especial, pero
cuando se habla de un método específico como __getitem__,
algunos desarrolladores de Python toman el atajo de decir "bajo-
bajo-getitem", que es ambiguo, porque la sintaxis __x tiene otro
significado especial .2 Ser preciso y pronunciar “under-under-
getitem under-under” es tedioso, así que sigo el ejemplo del autor
y profesor Steve Holden y digo “dunder-getitem”. Todos los
pitonistas experimentados entienden ese atajo. Como resultado,
los métodos especiales también se conocen como métodos dunder. 3
El siguiente es un ejemplo muy simple, pero demuestra el poder de implementar solo dos métodos
especiales, __getitem__ y __len__.
3. Personalmente, escuché por primera vez "dunder" de Steve Holden. Wikipedia acredita a Mark Johnson y Tim Hochberg
por los primeros registros escritos de "dunder" en respuestas a la pregunta "¿Cómo se pronuncia __ (doble guión
bajo)?" en la lista de python el 26 de septiembre de 2002: el mensaje de Johnson; Hochberg's (11 minutos después).
clase FrenchDeck:
rangos = [str(n) for n in range(2, 11)] + list('JQKA') palos = 'picas
diamantes tréboles corazones'.split()
def __init__(self):
self._cards = [Carta(rango, palo) para palo en self.palos para rango en
self.ranks]
Lo primero a tener en cuenta es el uso de collections.namedtuple para construir una clase simple para
representar tarjetas individuales. Desde Python 2.6, namedtuple se puede usar para crear clases de
objetos que son solo paquetes de atributos sin métodos personalizados, como un registro de base de
datos. En el ejemplo, lo usamos para brindar una buena representación de las cartas en el mazo, como
se muestra en la sesión de la consola:
Pero el punto de este ejemplo es la clase FrenchDeck . Es corto, pero tiene un gran impacto.
Primero, como cualquier colección estándar de Python, un mazo responde a la función len() devolviendo
la cantidad de cartas que contiene:
Leer cartas específicas del mazo, digamos, la primera o la última, debería ser tan fácil como mazo[0] o
mazo[-1], y esto es lo que proporciona el método __getitem__ :
>>> mazo[0]
Carta(rango='2', palo='picas') >>>
mazo[-1]
Carta(rango='A', palo='corazones')
¿Deberíamos crear un método para elegir una carta al azar? No hay necesidad. Python ya tiene una
función para obtener un elemento aleatorio de una secuencia: random.choice. Podemos usarlo en una
instancia de cubierta:
>>> elección(mazo)
Carta(rango='2', palo='tréboles')
Acabamos de ver dos ventajas de usar métodos especiales para aprovechar el modelo de datos de
Python:
• Los usuarios de sus clases no tienen que memorizar nombres de métodos arbitrarios para
operaciones estándar (“¿Cómo obtener el número de elementos? ¿Es .size(), .length(), o qué?”).
Debido a que nuestro __getitem__ delega al operador [] de self._cards, nuestro mazo admite
automáticamente el corte. Así es como miramos las tres primeras cartas de una baraja nueva y luego
elegimos solo los ases comenzando en el índice 12 y saltando 13 cartas a la vez:
>>> mazo[:3]
[Carta(rango='2', palo='picas'), Carta(rango='3', palo='picas'),
Carta(rango='4', palo='picas')] >>>
mazo[12::13]
[Carta(rango='A', palo='picas'), Carta(rango='A', palo='diamantes'),
Carta(rango='A', palo='tréboles'), Carta(rango='A', palo='corazones')]
Con solo implementar el método especial __getitem__ , nuestro mazo también es iterable:
Carta(rango='A', palo='corazones')
Carta(rango='K', palo='corazones')
Carta(rango='Q', palo='corazones')
...
Puntos suspensivos
en las pruebas documentales Siempre que fue posible, los listados de la
consola de Python en este libro se extrajeron de las pruebas documentales
para garantizar la precisión. Cuando la salida fue demasiado larga, la parte
elidida se marca con puntos suspensivos (...) como en la última línea del
código anterior. En tales casos, usamos la directiva # doctest: +ELLIPSIS para
hacer que el doctest pase. Si está probando estos ejemplos en la consola
interactiva, puede omitir las directivas doctest por completo.
¿Qué hay de clasificar? Un sistema común de clasificación de cartas es por rango (siendo los ases el
más alto), luego por palo en el orden de picas (el más alto), luego corazones, diamantes y tréboles (el
más bajo). Aquí hay una función que clasifica las cartas según esa regla, devolviendo 0 para el 2 de
tréboles y 51 para el as de picas:
def picas_alto(carta):
valor_clasificación = Mazo
Francés.clasificaciones.índice(carta.clasificación) return valor_clasificación * len(valores_palo) + valores_palo[carta.palo]
Dado spades_high, ahora podemos enumerar nuestro mazo en orden de rango creciente:
4. En Python 2, tendría que ser explícito y escribir FrenchDeck(objeto), pero ese es el valor predeterminado en Python 3.
Lo primero que debe saber acerca de los métodos especiales es que están destinados a ser llamados
por el intérprete de Python, y no por usted. No escribes my_object.__len__(). Escribe len(mi_objeto)
y, si mi_objeto es una instancia de una clase definida por el usuario, Python llama al método de
instancia __len__ que implementó.
Pero para tipos integrados como list, str, bytearray, etc., el intérprete toma un atajo: la implementación
de CPython de len() en realidad devuelve el valor del campo ob_size en la estructura PyVarObject C
que representa cualquier variable . objeto incorporado de tamaño en la memoria. Esto es mucho más
rápido que llamar a un método.
La mayoría de las veces, la llamada al método especial está implícita. Por ejemplo, la instrucción para
i en x: en realidad provoca la invocación de iter(x), que a su vez puede llamar a x.__iter__() si está
disponible.
Normalmente, su código no debería tener muchas llamadas directas a métodos especiales. A menos
que esté haciendo mucha metaprogramación, debería implementar métodos especiales con más
frecuencia que invocarlos explícitamente. El único método especial que el código de usuario llama
directamente con frecuencia es __init__, para invocar el inicializador de la superclase en su propia
implementación de __init__ .
Si necesita invocar un método especial, normalmente es mejor llamar a la función integrada relacionada
(p. ej., len, iter, str, etc.). Estos integrados llaman al método especial correspondiente, pero a menudo
proporcionan otros servicios y, para los tipos integrados, son más rápidos que las llamadas a métodos.
Véase, por ejemplo, “Una mirada más cercana a la función iter” en la página 436 en el Capítulo 14.
Evite crear atributos personalizados arbitrarios con la sintaxis __foo__ porque dichos nombres pueden
adquirir significados especiales en el futuro, incluso si no se usan hoy.
métodos especiales permiten que los objetos de usuario respondan a operadores como +. Cubriremos eso con
más detalle en el Capítulo 13, pero aquí nuestro objetivo es ilustrar más el uso de métodos especiales a través
de otro ejemplo simple.
Implementaremos una clase para representar vectores bidimensionales, es decir, vectores euclidianos como los
que se usan en matemáticas y física (consulte la figura 1-1).
Figura 1-1. Ejemplo de suma vectorial bidimensional; Vector(2, 4) + Vector(2, 1) resulta en Vector(4, 5).
Comenzaremos diseñando la API para dicha clase escribiendo una sesión de consola simulada que podemos
usar más tarde como una prueba de documento. El siguiente fragmento prueba la suma de vectores que se
muestra en la Figura 1-1:
Observe cómo el operador + produce un resultado vectorial , que se muestra de manera amigable en la consola.
La función integrada abs devuelve el valor absoluto de los números enteros y flotantes, y la magnitud
de los números complejos , por lo que, para ser coherentes, nuestra API también usa abs para
calcular la magnitud de un vector:
También podemos implementar el operador * para realizar una multiplicación escalar (es decir,
multiplicar un vector por un número para producir un nuevo vector con la misma dirección y una
magnitud multiplicada):
>>> v *3
Vector(9, 12)
>>> abs(v * 3)
15.0
El ejemplo 1-2 es una clase Vector que implementa las operaciones recién descritas mediante
el uso de los métodos especiales __repr__, __abs__, __add__ y __mul__.
clase vectorial:
def __repr__(self):
devuelve 'Vector(%r, %r)' % (self.x, self.y)
def __abs__(auto):
return hipot(auto.x, auto.y)
def __bool__(auto):
return bool(abs(auto))
Tenga en cuenta que aunque implementamos cuatro métodos especiales (aparte de __init__),
ninguno de ellos se llama directamente dentro de la clase o en el uso típico de la clase ilustrado por
las listas de la consola. Como se mencionó antes, el intérprete de Python es el único frecuente
llamador de la mayoría de los métodos especiales. En las siguientes secciones, analizamos el código para
cada método especial.
Representación de cadena El
método especial __repr__ es llamado por el repr integrado para obtener la representación de cadena del
objeto para su inspección. Si no implementáramos __repr__, las instancias de vector se mostrarían en la
consola como <Vector object at 0x10e100070>.
La consola interactiva y la llamada del depurador repr sobre los resultados de las expresiones evaluadas, al
igual que el marcador de posición %r en formato clásico con el operador % , y el campo de conversión !r en
la nueva sintaxis de cadena de formato utilizada en el método str.format .
Tenga en cuenta que en nuestra implementación de __repr__ , usamos %r para obtener la representación
estándar de los atributos que se mostrarán. Esta es una buena práctica, porque muestra la diferencia crucial
entre Vector(1, 2) y Vector('1', '2'); este último no funcionaría en el contexto de este ejemplo, porque los
argumentos del constructor deben ser numéricos. bers, no str.
La cadena devuelta por __repr__ no debe ser ambigua y, si es posible, debe coincidir con el código fuente
necesario para volver a crear el objeto que se representa. Es por eso que nuestra representación elegida
parece llamar al constructor de la clase (por ejemplo, Vector(3, 4)).
Contrasta __repr__ con __str__, que es llamado por el constructor str() e implícitamente usado por la función
de impresión . __str__ debería devolver una cadena adecuada para que finalice la visualización
usuarios
Si solo implementa uno de estos métodos especiales, elija __repr__, porque cuando no hay disponible
__str__ personalizado , Python llamará a __repr__ como respaldo.
Operadores aritméticos El
ejemplo 1-2 implementa dos operadores: + y *, para mostrar el uso básico de __add__ y __mul__. Tenga
en cuenta que en ambos casos, los métodos crean y devuelven una nueva instancia de Vector, y no
modifican ninguno de los operandos: self u other simplemente se leen. Este es el comportamiento
esperado de los operadores infijos: crear nuevos objetos y no tocar sus operandos.
Tendré mucho más que decir al respecto en el Capítulo 13.
Aunque Python tiene un tipo booleano , acepta cualquier objeto en un contexto booleano, como la
expresión que controla una declaración if o while , o como operandos para and, or y not. Para determinar
si un valor x es verdadero o falso, Python aplica bool(x), que siempre devuelve verdadero o falso.
De forma predeterminada, las instancias de clases definidas por el usuario se consideran verdaderas, a
menos que se implemente __bool__ o __len__ . Básicamente, bool(x) llama a x.__bool__() y usa el
resultado. Si __bool__ no está implementado, Python intenta invocar x.__len__(), y si eso devuelve cero,
bool devuelve False. De lo contrario , bool devuelve True.
Tenga en cuenta cómo el método especial __bool__ permite que sus objetos sean consistentes con las
reglas de prueba de valor de verdad definidas en el capítulo "Tipos integrados" de la documentación de la
biblioteca estándar de Python .
Esto es más difícil de leer, pero evita el viaje a través de abs, __abs__, los
cuadrados y la raíz cuadrada. La conversión explícita a bool es necesaria
porque __bool__ debe devolver un booleano y o devuelve cualquiera de
los operandos tal cual: x o y se evalúa como x si eso es cierto, de lo
contrario, el resultado es y, sea lo que sea.
El capítulo "Modelo de datos" de The Python Language Reference enumera 83 métodos especiales
nombres, 47 de los cuales se utilizan para implementar operaciones aritméticas, bit a bit y de comparación
tores
Como descripción general de lo que está disponible, consulte las tablas 1-1 y 1-2.
Servicios de clase
__preparar__, __instanciaverificar__, __subclaseverificar__
Operadores de comparación enriquecidos __lt__ >, __le__ <=, __eq__ ==, __ne__ !=, __gt__ >, __ge__ >=
__rdivmod__, __rpow__
Operadores bit a bit __invertir__ ~, __lshift__ <<, __rshift__ >>, __y__ &, __o__ |,
^
__xor__
En otras palabras, len no se llama como método porque recibe un tratamiento especial como parte del
modelo de datos de Python, al igual que abs. Pero gracias al método especial __len__, también puedes
hacer que len funcione con tus propios objetos personalizados. Este es un compromiso justo entre la
necesidad de objetos incorporados eficientes y la consistencia del lenguaje. También de The Zen of
Python: "Los casos especiales no son lo suficientemente especiales como para romper las reglas".
Gracias a la sobrecarga de operadores, Python ofrece una rica selección de tipos numéricos, desde
los incorporados hasta decimal.Decimal y fracciones.Fracción, todos compatibles con operadores
aritméticos infijos. Los operadores de implementación, incluidos los operadores invertidos y la
asignación aumentada, se mostrarán en el Capítulo 13 a través de mejoras del ejemplo de Vector .
El uso y la implementación de la mayoría de los métodos especiales restantes del modelo de datos
de Python se tratan a lo largo de este libro.
Otras lecturas
El capítulo "Modelo de datos" de The Python Language Reference es la fuente canónica para el tema
de este capítulo y gran parte de este libro.
Python in a Nutshell, 2nd Edition (O'Reilly) de Alex Martelli tiene una excelente cobertura del modelo
de datos. Mientras escribo esto, la edición más reciente del libro Nutshell es de 2006 y se enfoca en
Python 2.5, pero ha habido muy pocos cambios en el modelo de datos desde entonces, y la descripción
de Martelli de la mecánica del acceso a los atributos es la más original. itativo que he visto aparte del
código fuente C real de CPython. Martelli también es un colaborador prolífico de Stack Overflow, con
más de 5000 respuestas publicadas. Vea su perfil de usuario en Stack Overflow.
David Beazley tiene dos libros que cubren el modelo de datos en detalle en el contexto de Python 3:
Python Essential Reference, 4.ª edición (Addison-Wesley Professional) y Python Cookbook, 3.ª edición
(O'Reilly), en coautoría con Brian K. Jones.
The Art of the Metaobject Protocol (AMOP, MIT Press) de Gregor Kiczales, Jim des Rivieres y Daniel
G. Bobrow explica el concepto de un protocolo de metaobjetos (MOP), del cual el modelo de datos de
Python es un ejemplo.
Plataforma improvisada
Lo que la documentación de Python llama el "modelo de datos de Python", la mayoría de los autores dirían que
es el "modelo de objetos de Python". Python in a Nutshell 2E de Alex Martelli y Python Essential Reference 4E
de David Beazley son los mejores libros que cubren el "modelo de datos de Python", pero
Lectura adicional | 15
Machine Translated by Google
Métodos mágicos
La comunidad de Ruby llama a su equivalente de los métodos especiales métodos mágicos. Muchos
en la comunidad de Python también adoptan ese término. Creo que los métodos especiales son en
realidad lo opuesto a la magia. Python y Ruby son iguales en este sentido: ambos empoderan a sus
usuarios con un rico protocolo de metaobjetos que no es mágico, pero les permite aprovechar las
mismas herramientas disponibles para los desarrolladores principales.
Por el contrario, considere JavaScript. Los objetos en ese idioma tienen características que son
mágicas, en el sentido de que no puedes emularlas en tus propios objetos definidos por el usuario. Por
ejemplo, antes de JavaScript 1.8.5, no podía definir atributos de solo lectura en sus objetos de
JavaScript, pero algunos objetos integrados siempre tenían atributos de solo lectura. En JavaScript, los
atributos de solo lectura eran "mágicos", y requerían poderes sobrenaturales que un usuario del
lenguaje no tenía hasta que salió ECMAScript 5.1 en 2009. El protocolo de metaobjetos de JavaScript
está evolucionando, pero históricamente ha sido más limitado que las de Python y Ruby.
metaobjetos
El arte del protocolo de metaobjetos (AMOP) es mi título de libro de computadora favorito. Menos
subjetivamente, el término protocolo de metaobjetos es útil para pensar en el modelo de datos de
Python y características similares en otros lenguajes. La parte del metaobjeto se refiere a los objetos
que son los componentes básicos del lenguaje mismo. En este contexto, protocolo es sinónimo de
interfaz. Entonces, un protocolo de metaobjetos es un sinónimo elegante de modelo de objetos: una
API para construcciones de lenguaje central.
Un rico protocolo de metaobjetos permite extender un lenguaje para admitir nuevos paradigmas de
programación. Gregor Kiczales, el primer autor del libro AMOP , luego se convirtió en pionero en la
programación orientada a aspectos y en el autor inicial de AspectJ, una extensión de Java que
implementa ese paradigma. La programación orientada a aspectos es mucho más fácil de implementar
en un lenguaje dinámico como Python, y varios marcos lo hacen, pero el más importante es
zope.interface, que se analiza brevemente en "Lectura adicional" en la página 342 del Capítulo 11.
PARTE II
Estructuras de datos
Machine Translated by Google
Machine Translated by Google
CAPITULO 2
Como te habrás dado cuenta, varias de las operaciones mencionadas funcionan igualmente para textos,
listas y tablas. Los textos, listas y tablas en conjunto se denominan trenes. […] El comando FOR también
funciona genéricamente en trenes.1
Antes de crear Python, Guido fue colaborador del lenguaje ABC, un proyecto de investigación de 10
años para diseñar un entorno de programación para principiantes. ABC introdujo muchas ideas que
ahora consideramos "Pythonic": operaciones genéricas en secuencias, tipos de mapeo y tupla
incorporados, estructura por sangría, tipado fuerte sin declaraciones de variables, y más. No es
casualidad que Python sea tan fácil de usar.
Python heredó de ABC el manejo uniforme de secuencias. Cadenas, listas, secuencias de bytes,
arreglos, elementos XML y resultados de bases de datos comparten un amplio conjunto de
operaciones comunes que incluyen iteración, división, clasificación y concatenación.
La mayor parte de la discusión en este capítulo se aplica a las secuencias en general, desde la lista
familiar hasta los tipos str y bytes que son nuevos en Python 3. Aquí también se cubren temas
específicos sobre listas, tuplas, matrices y colas, pero el enfoque en Las cadenas Unicode y las
secuencias de bytes se remiten al Capítulo 4. Además, la idea aquí es cubrir los tipos de secuencia
que están listos para usar. La creación de sus propios tipos de secuencias es el tema del Capítulo 10.
1. Leo Geurts, Lambert Meertens y Steven Pemberton, Manual del programador de ABC, pág. 8.
19
Machine Translated by Google
lista de secuencias de contenedor , la tupla y las colecciones . Deque puede contener elementos de
diferentes tipos.
Las secuencias
planas str, bytes, bytearray, memoryview y array.array contienen elementos de un tipo.
Las secuencias contenedoras contienen referencias a los objetos que contienen, que pueden ser de
cualquier tipo, mientras que las secuencias planas almacenan físicamente el valor de cada elemento dentro
de su propio espacio de memoria, y no como objetos distintos. Por lo tanto, las secuencias planas son más
compactas, pero se limitan a contener valores primitivos como caracteres, bytes y números.
Secuencias inmutables
tuple, str y bytes
La Figura 2-1 ayuda a visualizar cómo las secuencias mutables se diferencian de las inmutables, al mismo
tiempo que heredan varios métodos de ellas. Tenga en cuenta que los tipos de secuencia concretos
integrados en realidad no subclasifican las clases base abstractas (ABC) Sequence y MutableSequence
representadas, pero las ABC siguen siendo útiles como una formalización de qué funcionalidad esperar de
un tipo de secuencia con todas las funciones.
Figura 2-1. Diagrama de clases UML para algunas clases de collections.abc (las superclases están a la
izquierda; las flechas de herencia apuntan de subclases a superclases; los nombres en cursiva son clases
abstractas y métodos abstractos)
Teniendo en cuenta estos rasgos comunes: mutable versus inmutable; contenedor frente a plano: es útil
para extrapolar lo que sabe sobre un tipo de secuencia a otros.
El tipo de secuencia más fundamental es la lista: mutable y de tipo mixto. Estoy seguro de que se siente cómodo
manejándolos, por lo que pasaremos directamente a la comprensión de listas, una forma poderosa de crear listas
que está algo infrautilizada porque la sintaxis puede ser desconocida. Dominar la comprensión de listas abre la
puerta a expresiones generadoras que, entre otros usos, pueden producir elementos para completar secuencias
de cualquier tipo. Ambos son el tema de la siguiente sección.
Si duda de mi afirmación de que estas construcciones son "más legibles", siga leyendo. Intentaré convencerte.
Ejemplo 2-1. Cree una lista de puntos de código Unicode a partir de una cadena
Ejemplo 2-2. Cree una lista de puntos de código Unicode a partir de una cadena, tome dos
Cualquiera que conozca un poco de Python puede leer el Ejemplo 2-1. Sin embargo, después de aprender sobre
listcomps, encuentro que el Ejemplo 2-2 es más legible porque su intención es explícita.
Un bucle for se puede usar para hacer muchas cosas diferentes: escanear una secuencia para contar
o seleccionar elementos, calcular agregados (sumas, promedios) o cualquier otra tarea de procesamiento.
El código del Ejemplo 2-1 está construyendo una lista. Por el contrario, un listcomp está destinado a
hacer una sola cosa: construir una nueva lista.
Por supuesto, es posible abusar de la comprensión de listas para escribir código realmente
incomprensible. He visto código de Python con listas comps utilizadas solo para repetir un bloque de
código por sus efectos secundarios. Si no está haciendo algo con la lista producida, no debe usar esa
sintaxis. Además, trata de que sea breve. Si la comprensión de la lista abarca más de dos líneas,
probablemente sea mejor dividirla o reescribirla como un simple bucle for . Use su mejor juicio: tanto
para Python como para inglés, no existen reglas estrictas para una escritura clara.
Sugerencia
de sintaxis En el código de Python, los saltos de línea se ignoran dentro de
los pares de [], {} o (). Por lo tanto, puede crear listas multilínea, listas comps,
genexs, diccionarios y similares sin usar el feo escape de continuación de línea \ .
>>> x
'C'
Como puede ver, el valor inicial de x fue aplastado. Esto ya no sucede en Python
3.
Las comprensiones de lista, las expresiones generadoras y sus hermanos set y dict comprensiones
ahora tienen su propio alcance local, como funciones. Las variables asignadas dentro de la
expresión son locales, pero aún se puede hacer referencia a las variables en el ámbito circundante.
Aún mejor, las variables locales no enmascaran las variables del ámbito circundante.
Esto es Python 3:
>>> x = 'ABC'
>>> dummy = [ord(x) para x en x]
>>> x
'ABC'
>>> maniquí
El valor de x se conserva.
La lista por comprensión produce la lista esperada.
Las listas de comprensión construyen listas a partir de secuencias o cualquier otro tipo iterable filtrando y
transformando elementos. Los elementos integrados de filtro y mapa se pueden componer para hacer lo mismo,
pero la legibilidad se ve afectada, como veremos a continuación.
hacen todo lo que hacen las funciones de mapear y filtrar , sin las contorsiones del funcionalmente desafiado
Python lambda. Considere el Ejemplo 2-3.
Ejemplo 2-3. La misma lista construida por un listcomp y una composición de mapa/ filtro
Solía creer que map y filter eran más rápidos que los listcomps equivalentes, pero Alex Martelli señaló que ese no
es el caso, al menos no en los ejemplos anteriores. El script 02-array-seq/ listcomp_speed.py en el repositorio de
código de Fluent Python es una prueba de velocidad simple que compara listcomp con filter/map.
Tendré más que decir sobre el mapa y el filtro en el Capítulo 5. Ahora pasamos al uso de listcomps para calcular
productos cartesianos: una lista que contiene tuplas construidas a partir de todos los elementos de dos o más
listas.
Productos cartesianos
Listcomps puede generar listas a partir del producto cartesiano de dos o más iterables. Los elementos que
componen el producto cartesiano son tuplas formadas por elementos de cada iterable de entrada. La lista
resultante tiene una longitud igual a las longitudes de los iterables de entrada multiplicados. Consulte la Figura
2-2.
Figura 2-2. El producto cartesiano de una secuencia de tres rangos de cartas y una secuencia de cuatro palos da como
resultado una secuencia de doce parejas.
Por ejemplo, imagine que necesita producir una lista de camisetas disponibles en dos colores y tres tamaños. El ejemplo 2-4
muestra cómo producir esa lista usando un listcomp. el resultado tiene
seis artículos.
...
... imprimir ((color, tamaño))
...
('negro', 'S')
('negro', 'M')
('negro', 'L')
('blanco', 'S')
('blanco', 'M')
('blanco', 'L') >>>
camisetas = [(color, talla) para talla en tallas para color
... en colores]
>>> camisetas
[('negro', 'S'), ('blanco', 'S'), ('negro', 'M'), ('blanco', 'M'), ('negro', 'L'), ('blanco',
'L')]
Esto genera una lista de tuplas ordenadas por color y luego por tamaño.
Observe cómo se organiza la lista resultante como si los bucles for estuvieran anidados en el mismo
orden en que aparecen en la compilación de lista.
Para ordenar los elementos por tamaño y luego por color, simplemente reorganice las cláusulas for ;
agregar un salto de línea al listcomp facilita ver cómo se ordenará el resultado.
En el Ejemplo 1-1 (Capítulo 1), se utilizó la siguiente expresión para inicializar un mazo de cartas con una lista
compuesta por 52 cartas de los 13 rangos de cada uno de los 4 palos, agrupados por palo:
Listcomps son un pony de un solo truco: construyen listas. Para llenar otros tipos de secuencias, un genex es
el camino a seguir. La siguiente sección es una breve mirada a genexs en el contexto de la construcción de
secuencias que no son listas.
inicializar tuplas, arreglos y otros tipos de secuencias, también puede comenzar con un listcomp, pero un genex
ahorra memoria porque produce elementos uno por uno usando el protocolo iterador en lugar de construir una
lista completa solo para alimentar a otro constructor.
Genexps usa la misma sintaxis que listcomps, pero están encerrados entre paréntesis en lugar de corchetes.
El ejemplo 2-5 muestra el uso básico de genexs para construir una tupla y una matriz.
Ejemplo 2-5. Inicializar una tupla y una matriz a partir de una expresión generadora
El constructor de matriz toma dos argumentos, por lo que los paréntesis alrededor de la expresión del
generador son obligatorios. El primer argumento del constructor de arreglos define el tipo de
almacenamiento utilizado para los números en el arreglo, como veremos en “Arreglos” en la página 48.
El ejemplo 2-6 utiliza un genexp con un producto cartesiano para imprimir una lista de camisetas de dos colores
en tres tamaños. En contraste con el ejemplo 2-4, aquí la lista de camisetas de seis elementos nunca se
construye en la memoria: la expresión del generador alimenta el ciclo for que produce uno
elemento a la vez. Si las dos listas utilizadas en el producto cartesiano tuvieran 1000 elementos cada
una, el uso de una expresión generadora ahorraría el gasto de crear una lista con un millón de elementos
solo para alimentar el bucle for .
La expresión del generador produce elementos uno por uno; En este ejemplo nunca se produce
una lista con las seis variaciones de camisetas.
El capítulo 14 está dedicado a explicar cómo funcionan los generadores en detalle. Aquí, la idea era
simplemente mostrar el uso de expresiones generadoras para inicializar secuencias que no sean listas, o
para producir resultados que no necesita guardar en la memoria.
registros Las tuplas contienen registros: cada elemento de la tupla contiene los datos de un campo y la
posición del elemento da su significado.
Si piensa en una tupla como una lista inmutable, la cantidad y el orden de los elementos pueden o no ser
importantes, según el contexto. Pero cuando se usa una tupla como una colección de campos, la cantidad
de elementos suele ser fija y su orden siempre es vital.
El ejemplo 2-7 muestra el uso de tuplas como registros. Tenga en cuenta que en cada expresión, ordenar
la tupla destruiría la información porque el significado de cada elemento de datos viene dado por su
posición en la tupla.
ESP/XDA205856
EE. UU./31195855
>>> para país, _ en traveler_ids: print(país)
...
...
EE.UU
SOSTÉN
ESP
Datos sobre Tokio: nombre, año, población (millones), cambio de población (%),
superficie (km²).
A medida que iteramos sobre la lista, el pasaporte está vinculado a cada tupla.
El operador de formato % entiende las tuplas y trata cada elemento como un elemento separado.
campo.
El bucle for sabe cómo recuperar los elementos de una tupla por separado; esto se llama
"desempacar". Aquí no estamos interesados en el segundo elemento, por lo que está asignado a
_, una variable ficticia.
Las tuplas funcionan bien como registros debido al mecanismo de desempaquetado de tuplas, nuestro próximo subÿ
proyecto
Desempaquetado de tuplas
En el Ejemplo 2-7, asignamos ('Tokyo', 2003, 32450, 0.66, 8014) a ciudad, año,
pop, chg, area en una sola sentencia. Luego, en la última línea, el operador % asignado
La forma más visible de desempaquetado de tuplas es la asignación en paralelo; es decir, asignando elementos
de un iterable a una tupla de variables, como puedes ver en este ejemplo:
33.9425
>>> longitud
-118.408056
Una aplicación elegante del desempaquetado de tuplas es intercambiar los valores de las variables sin usar una
variable temporal:
>>> b, a = a, b
Otro ejemplo de desempaquetado de tuplas es prefijar un argumento con una estrella al llamar a una función:
El código anterior también muestra un uso adicional del desempaquetado de tuplas: permitir que las funciones
devuelvan múltiples valores de una manera que sea conveniente para la persona que llama. Por ejemplo, la
función os.path.split() crea una tupla (ruta, última parte) a partir de una ruta del sistema de archivos:
>>> import os
>>> _, filename = os.path.split('/home/luciano/.ssh/idrsa.pub')
>>> nombre de
archivo 'idrsa.pub'
A veces, cuando solo nos preocupamos por ciertas partes de una tupla al desempaquetar, una variable ficticia
como _ se utiliza como marcador de posición, como en el ejemplo anterior.
Otra forma de enfocarse solo en algunos de los elementos al desempaquetar una tupla es usar el *, como veremos
enseguida.
En el contexto de la asignación paralela, el prefijo * se puede aplicar exactamente a una variable, pero puede aparecer
en cualquier posición:
Finalmente, una característica poderosa del desempaquetado de tuplas es que funciona con estructuras anidadas.
tupla que recibe una expresión para desempaquetar puede tener tuplas anidadas, como (a, b, (c, d)), y Python hará lo
correcto si la expresión coincide con la estructura anidada.
El ejemplo 2-8 muestra el desempaquetado de tuplas anidadas en acción.
áreas_metro =
[ ('Tokio', 'JP', 36.933, (35.689722, 139.691667)), # ('Delhi NCR',
'IN', 21.935, (28.613889, 77.208889)), ('Ciudad de México', 'MX' ,
20.142, (19.433333, -99.133333)),
Cada tupla contiene un registro con cuatro campos, el último de los cuales es un par de coordenadas.
longitud <= 0: limita la salida a las áreas metropolitanas del hemisferio occidental.
Tal como están diseñadas, las tuplas son muy útiles. Pero falta una característica al usarlos como registros: a
veces es deseable nombrar los campos. Por eso se inventó la función de tupla nombrada . sigue leyendo
La función collections.namedtuple es una fábrica que produce subclases de tupla mejoradas con nombres de
campo y un nombre de clase, lo que ayuda a la depuración.
Las instancias de una clase que crea con namedtuple ocupan exactamente
la misma cantidad de memoria que las tuplas porque los nombres de los
campos se almacenan en la clase. Utilizan menos memoria que un objeto
normal porque no almacenan atributos en un __dict__ por instancia.
El ejemplo 2-9 muestra cómo podríamos definir una tupla con nombre para contener información sobre una
ciudad.
'JP'
Se requieren dos parámetros para crear una tupla con nombre: un nombre de clase y una lista de
nombres de campo, que se pueden proporcionar como un iterable de cadenas o como una cadena
delimitada por un solo espacio.
Los datos deben pasarse como argumentos posicionales al constructor (por el contrario, el constructor
de tuplas toma un solo iterable).
Un tipo de tupla con nombre tiene algunos atributos además de los heredados de la tupla.
El ejemplo 2-10 muestra los más útiles: el atributo de clase _fields , el método de clase _make(iterable) y el
método de instancia _asdict() .
Ejemplo 2-10. Atributos y métodos de tupla con nombre (continuación del ejemplo anterior)
>>> Ciudad._campos
('nombre', 'país', 'población', 'coordenadas')
>>> LatLong = tupla con nombre('LatLong', 'lat long') >>>
delhi_data = ('Delhi NCR', 'IN', 21.935, LatLong(28.613889, 77.208889)) >>> delhi =
City._make(delhi_data ) >>> delhi._asdict()
_make() le permite instanciar una tupla con nombre a partir de un iterable; Ciudad(*del
hi_data) haría lo mismo.
Ahora que hemos explorado el poder de las tuplas como registros, podemos considerar su segundo
rol como una variante inmutable del tipo de lista .
Cuando se usa una tupla como una variación inmutable de la lista, es útil saber qué tan similar
en realidad lo son. Como puede ver en la Tabla 2-1, tuple admite todos los métodos de lista que no
no implica agregar o eliminar elementos, con una excepción: la tupla carece del __re
método versado__ . Sin embargo, eso es solo para optimización; funciona invertido (my_tuple)
sin ello.
Tabla 2-1. Métodos y atributos encontrados en lista o tupla (métodos implementados por obÿ
se omiten por brevedad)
tupla lista
s.__añadir__(s2) •• s + s2: concatenación
s.__contiene__(e) •• e en s
tupla lista
s.__imul__(n) • s *= n: concatenación repetida en el lugar
s.__rmul__(n) •• norte
* s: concatenación repetida invertidaa
s.sort([clave], [reversa]) • Ordene los elementos en su lugar con la clave de argumentos de palabras clave opcionales y revierta
Todo programador de Python sabe que las secuencias se pueden dividir utilizando la sintaxis s[a:b] .
Pasamos ahora a algunos hechos menos conocidos sobre el corte.
rebanar
Una característica común de list, tuple, str y todos los tipos de secuencias en Python es el soporte
de operaciones de rebanado, que son más poderosas de lo que la mayoría de la gente piensa.
La convención Pythonic de excluir el último elemento en segmentos y rangos funciona bien con
la indexación basada en cero utilizada en Python, C y muchos otros lenguajes. algunos convenientes
Las características de la convención son:
• Es fácil ver la longitud de un segmento o rango cuando solo se proporciona la posición de parada:
range(3) y my_list[:3] producen tres elementos.
• Es fácil calcular la longitud de un segmento o rango cuando se dan inicio y fin: simplemente
restar parada - inicio.
• Es fácil dividir una secuencia en dos partes en cualquier índice x, sin superposición: simplemente
obtener mi_lista[:x] y mi_lista[x:]. Por ejemplo:
Rebanar | 33
Machine Translated by Google
Pero los mejores argumentos para esta convención fueron escritos por el científico informático holandés
Edsger W. Dijkstra (consulte la última referencia en “Lectura adicional” en la página 59).
Rebanar objetos
Esto no es ningún secreto, pero vale la pena repetirlo por si acaso: s[a:b:c] se puede usar para especificar un
zancada o paso c, lo que hace que el corte resultante omita elementos. La zancada también puede ser negativa,
devolución de artículos al revés. Tres ejemplos lo aclaran:
>>> s = 'bicicleta'
>>> s[::3]
'adiós'
>>> s[::-1]
'elcycib'
>>> s[::-2]
'eccb'
Otro ejemplo se mostró en el Capítulo 1 cuando usamos deck[12::13] para obtener todos los
ases en la baraja sin barajar:
>>> mazo[12::13]
[Carta(rango='A', palo='picas'), Carta(rango='A', palo='diamantes'),
Carta(rango='A', palo='tréboles'), Carta(rango='A', palo='corazones')]
La notación a:b:c solo es válida dentro de [] cuando se usa como índice o subíndice
operador, y produce un objeto de corte: corte (a, b, c). Como veremos en “Cómo rebanar
Works” en la página 281, para evaluar la expresión seq[start:stop:step], Python llama
seq.__getitem__(segmento(inicio, parada, paso)). Incluso si no está implementando
sus propios tipos de secuencias, conocer los objetos de corte es útil porque le permite asignar
nombres a sectores, al igual que las hojas de cálculo permiten nombrar rangos de celdas.
Suponga que necesita analizar datos de archivo sin formato como la factura que se muestra en el Ejemplo 2-11. En cambio
de llenar su código con segmentos codificados, puede nombrarlos. Mira lo legible que es esto
hace el bucle for al final del ejemplo.
Volveremos a dividir objetos cuando analicemos la creación de sus propias colecciones en “Toma vectorial
n.º 2: una secuencia que se puede dividir” en la página 280. Mientras tanto, desde la perspectiva del
usuario, la división incluye funciones adicionales, como secciones multidimensionales y puntos suspensivos
(.. .) notación. sigue leyendo
operador [] también puede tomar múltiples índices o cortes separados por comas. Esto se usa, por ejemplo,
en el paquete NumPy externo, donde los elementos de un numpy.ndarray bidimensional se pueden obtener
usando la sintaxis a[i, j] y un segmento bidimensional obtenido con una expresión como a[m: n, k:l]. El
ejemplo 2-22 más adelante en este capítulo muestra el uso de esta notación. Los métodos especiales
__getitem__ y __setitem__ que manejan el operador [] simplemente reciben los índices en a[i, j] como una
tupla. En otras palabras, para evaluar a[i, j], Python llama a.__getitem__((i, j)).
Los tipos de secuencia incorporados en Python son unidimensionales, por lo que solo admiten un índice o
segmento, y no una tupla de ellos.
Los puntos suspensivos, escritos con tres puntos (...) y no ... (Unicode U+2026), son reconocidos como un
token por el analizador de Python. Es un alias del objeto Ellipsis , la única instancia de la clase ellipsis.2
Como tal, se puede pasar como argumento a funciones y como parte de una especificación de segmento,
como en f(a, ..., z) o un[yo:...]. NumPy usa ... como un atajo al cortar matrices de muchas dimensiones; por
ejemplo, si x es una matriz de cuatro dimensiones, x[i, ...] es un atajo para x[i, :, :, :,]. Consulte el tutorial
tentativo de NumPy para obtener más información al respecto.
En el momento de escribir este artículo, desconozco los usos de los puntos suspensivos o los índices y
sectores multidimensionales en la biblioteca estándar de Python. Si ves uno, házmelo saber. Estas
funciones sintácticas existen para admitir tipos y extensiones definidos por el usuario, como NumPy.
2. No, no entendí esto al revés: el nombre de la clase de puntos suspensivos está realmente en minúsculas y la instancia es una
en puntos suspensivos con nombre, al igual que bool está en minúsculas pero sus instancias son verdaderas y falsas.
Rebanar | 35
Machine Translated by Google
Los cortes no solo son útiles para extraer información de secuencias; también se pueden usar para
cambiar secuencias mutables en su lugar, es decir, sin reconstruirlas desde cero.
Asignación a segmentos
Las secuencias mutables pueden injertarse, eliminarse y modificarse en su lugar utilizando la notación
de segmento en el lado izquierdo de una declaración de asignación o como destino de una declaración del .
Los siguientes ejemplos dan una idea del poder de esta notación:
Cuando el objetivo de la asignación es un segmento, el lado derecho debe ser un objeto iterable,
incluso si tiene solo un elemento.
Todo el mundo sabe que la concatenación es una operación común con secuencias de cualquier tipo.
Cualquier texto introductorio de Python explica el uso de + y * para ese propósito, pero hay algunos
detalles sutiles sobre cómo funcionan, que cubrimos a continuación.
Para concatenar varias copias de la misma secuencia, multiplíquela por un número entero. Nuevamente,
se crea una nueva secuencia:
>>> 5 * 'abcd'
'abcdabcdabcdabcdabcd'
Tanto + como * siempre crean un nuevo objeto y nunca cambian sus operandos.
La siguiente sección cubre los peligros de tratar de usar * para inicializar una lista de listas.
veces necesitamos inicializar una lista con un cierto número de listas anidadas, por ejemplo, para distribuir
a los estudiantes en una lista de equipos o para representar cuadrados en un tablero de juego. La mejor
forma de hacerlo es con una lista de comprensión, como en el Ejemplo 2-12.
Ejemplo 2-12. Una lista con tres listas de longitud 3 puede representar un tablero de tres en raya
Cree una lista de tres listas de tres elementos cada una. Inspeccionar la estructura.
Coloque una marca en la fila 1, columna 2 y verifique el resultado.
Ejemplo 2-13. Una lista con tres referencias a la misma lista es inútil
La lista exterior está formada por tres referencias a la misma lista interior. Si bien no ha cambiado,
todo parece correcto.
Colocar una marca en la fila 1, columna 2, revela que todas las filas son alias que se refieren al
mismo objeto.
El problema con el Ejemplo 2-13 es que, en esencia, se comporta como este código:
fila = ['_'] * 3
tablero = [] for i in
range(3):
tablero.append(fila)
Por otro lado, la lista de comprensión del Ejemplo 2-12 es equivalente a este código:
>>> tablero = []
>>> for i en rango(3): fila
... = ['_'] * 3 #
... tablero.agregar(fila)
...
>>> tablero
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']] >>> tablero[2][0] = 'X' >>>
tablero #
Hasta ahora hemos discutido el uso de los operadores simples + y * con secuencias, pero también
existen los operadores += y *= , que producen resultados muy diferentes dependiendo de la mutabilidad
de la secuencia objetivo. La siguiente sección explica cómo funciona.
El método especial que hace que += funcione es __iadd__ (para “suma en el lugar”). Sin embargo, si
no se implementa __iadd__ , Python vuelve a llamar a __add__. Considere esta simple expresión:
>>> un += segundo
En general, para secuencias mutables, es una buena apuesta que se implemente __iadd__ y que +=
suceda en su lugar. Para secuencias inmutables, claramente no hay forma de que eso suceda.
Lo que acabo de escribir sobre += también se aplica a *=, que se implementa a través de __imul__. Los
métodos especiales __iadd__ y __imul__ se analizan en el Capítulo 13.
Aquí hay una demostración de *= con una secuencia mutable y luego una inmutable:
>>> l *= 2
>>> l [1, 2,
3, 1, 2, 3] >>> id(l)
4311953800
>>> t *= 2
>>> id(t)
4301348296
ID de la lista inicial
ID de la tupla inicial
Hemos visto casos de uso comunes para +=. La siguiente sección muestra un caso de esquina intrigante
que destaca lo que realmente significa "inmutable" en el contexto de las tuplas.
3. str es una excepción a esta descripción. Debido a que la construcción de cadenas con += en bucles es tan común en la
naturaleza, CPython está optimizado para este caso de uso. Las instancias de str se asignan en la memoria con espacio de
sobra, por lo que la concatenación no requiere copiar la cadena completa cada vez.
A += Rompecabezas de asignación
Intenta responder sin usar la consola: ¿cuál es el resultado de evaluar las dos expresiones en el Ejemplo
2-14?4
d. Tanto a como b.
Cuando vi esto, estaba bastante seguro de que la respuesta era b, ¡pero en realidad es d, "tanto a como b".!
El ejemplo 2-15 es el resultado real de una consola de Python 3.4 (en realidad, el resultado es el mismo en
una consola de Python 2.7).5
Online Python Tutor es una increíble herramienta en línea para visualizar cómo funciona Python en detalle.
La figura 2-3 es una combinación de dos capturas de pantalla que muestran los estados inicial y final de la
tupla t del ejemplo 2-15.
4. Gracias a Leonardo Rochael y Cesar Kawakami por compartir este acertijo en la Conferencia PythonBrasil 2013.
encia
5. Un lector sugirió que la operación del ejemplo se puede realizar con t[2].extend([50,60]), sin errores. Somos
conscientes de eso, pero la intención del ejemplo es analizar el comportamiento extraño del operador +=.
Figura 2-3. Estado inicial y final del rompecabezas de asignación de tuplas (diagrama generado
por el tutor de Python en línea)
Realizar TOS += b. Esto tiene éxito si TOS se refiere a un objeto mutable (es una lista, en
Ejemplo 2-15).
Asigne s[a] = TOS. Esto falla si s es inmutable (la tupla t en el Ejemplo 2-15).
Este ejemplo es un caso de esquina: en 15 años de usar Python, nunca había visto esto
comportamiento extraño en realidad muerde a alguien.
• La asignación aumentada no es una operación atómica: solo vimos que lanzaba una excepción después de
hacer parte de su trabajo. • Inspeccionar el código de bytes de Python no es demasiado difícil y, a menudo,
Después de presenciar las sutilezas de usar + y * para la concatenación, podemos cambiar el tema a otra
operación esencial con secuencias: clasificación.
Por el contrario, la función incorporada sorted crea una nueva lista y la devuelve. De hecho, acepta cualquier
objeto iterable como argumento, incluidas las secuencias inmutables y los generadores (consulte el Capítulo 14).
Independientemente del tipo de iterable dado a sorted, siempre devuelve una lista recién creada.
Tanto list.sort como sorted toman dos argumentos opcionales de solo palabras clave:
reverso
Si es True, los elementos se devuelven en orden descendente (es decir, invirtiendo la comparación de los
elementos). El valor predeterminado es Falso.
clave Una función de un argumento que se aplicará a cada elemento para producir su clave de clasificación. Por
ejemplo, al ordenar una lista de cadenas, key=str.lower se puede usar para realizar una ordenación que no
distingue entre mayúsculas y minúsculas, y key=len ordenará las cadenas por longitud de carácter. El valor
predeterminado es la función de identidad (es decir, se comparan los elementos mismos).
Aquí hay algunos ejemplos para aclarar el uso de estas funciones y argumentos de palabras clave6 :
Una nueva lista de cadenas, ahora ordenadas por longitud. Debido a que el algoritmo de
clasificación es estable, "uva" y "manzana", ambos de longitud 5, están en el orden original.
Estas son las cadenas ordenadas en orden descendente de longitud. No es el resultado inverso
al anterior porque la clasificación es estable, por lo que nuevamente aparece “uva” antes que
“manzana”.
Una vez que sus secuencias están ordenadas, se pueden buscar de manera muy eficiente.
Afortunadamente, el algoritmo de búsqueda binaria estándar ya se proporciona en el módulo bisect de la
biblioteca estándar de Python. Discutimos sus características esenciales a continuación, incluyendo la conveniente
6. Los ejemplos también demuestran que Timsort, el algoritmo de clasificación utilizado en Python, es estable (es decir, conserva
la ordenación relativa de elementos que se comparan iguales). Timsort se analiza con más detalle en la barra lateral
"Soapbox" al final de este capítulo.
función bisect.insort , que puede usar para asegurarse de que sus secuencias ordenadas permanezcan
ordenadas.
aguja) realiza una búsqueda binaria de aguja en el pajar, que debe ser una secuencia ordenada, para
ubicar la posición en la que se puede insertar la aguja mientras se mantiene el pajar en orden
ascendente. En otras palabras, todos los elementos que aparecen hasta esa posición son menores o
iguales que la aguja. Podrías usar el resultado de bisect(pajar, aguja) como argumento de índice para
haystack.insert(índice, aguja); sin embargo, usar insort hace ambos pasos y es más rápido.
El ejemplo 2-17 utiliza un conjunto cuidadosamente seleccionado de "agujas" para demostrar las posiciones de
inserción devueltas por bisect. Su salida está en la Figura 2-4.
Ejemplo 2-17. bisect encuentra puntos de inserción para elementos en una secuencia ordenada
importar sistema de
importación de bisect
PAJAR = [1, 4, 5, 6, 8, 12, 15, 20, 21, 23, 23, 26, 29, 30]
AGUJAS = [0, 1, 2, 5, 8, 10, 22, 23, 29, 30, 31]
def demo(bisect_fn):
para aguja en invertida(AGUJAS):
posición = bisect_fn(HAYSTACK, aguja)
desplazamiento = posición * ' |'(ROW_FMT.format
imprimir
(aguja, posición, desplazamiento))
si __nombre__ == '__principal__':
if sys.argv[-1] == 'izquierda':
bisect_fn = bisect.bisect_left else:
bisect_fn = bisect.bisect
print('DEMO:', bisect_fn.__name__)
print('pajar ->', ' '.join('%2d' % n for n en HAYSTACK)) demo(bisect_fn)
Imprima una fila con formato que muestre la aguja y el punto de inserción.
Elija la función bisect para usar de acuerdo con el último argumento de la línea de comandos.
Figura 2-4. Salida del ejemplo 2-17 con bisect en uso: cada fila comienza con la notación aguja en la posición
@ y el valor de la aguja aparece nuevamente debajo de su punto de inserción en el pajar
Primero, un par de argumentos opcionales, lo y hi, permiten estrechar la región en la secuencia que se buscará
al insertar. lo por defecto es 0 y hola para el len() de la secuencia.
En segundo lugar, bisect es en realidad un alias para bisect_right, y hay una función hermana llamada
bisect_left. Su diferencia es evidente solo cuando la aguja se compara con un elemento de la lista: bisect_right
devuelve un punto de inserción después del elemento existente y bisect_left devuelve la posición del elemento
existente, por lo que la inserción se produciría antes.
eso. Con tipos simples como int esto no hace ninguna diferencia, pero si la secuencia contiene objetos que son
distintos pero se comparan iguales, entonces puede ser relevante. Por ejemplo, 1 y 1.0 son distintos, pero 1 ==
1.0 es Verdadero. La Figura 2-5 muestra el resultado de usar bisect_left.
Figura 2-5. Resultado del Ejemplo 2-17 con bisect_left en uso (compárelo con la Figura 2-4 y observe los puntos
de inserción para los valores 1, 8, 23, 29 y 30 a la izquierda de los mismos números en el pajar).
Una aplicación interesante de bisect es realizar búsquedas en tablas por valores numéricos, por ejemplo, para
convertir puntajes de exámenes en calificaciones con letras, como en el ejemplo 2-18.
Ejemplo 2-18. Dado el puntaje de una prueba, grade devuelve la calificación de letra correspondiente
El código en el Ejemplo 2-18 es de la documentación del módulo bisect , que también enumera funciones para
usar bisect como un reemplazo más rápido para el método de índice cuando se busca a través de largas
secuencias ordenadas de números.
Estas funciones no solo se utilizan para buscar, sino también para insertar elementos en secuencias ordenadas,
como se muestra en la siguiente sección.
insort(seq, item) inserta item en seq para mantener seq en orden ascendente. Vea el Ejemplo
2-19 y su salida en la Figura 2-6.
importar bisect
importación aleatoria
TAMAÑO = 7
my_list = [] for i
in range(SIZE): new_item =
random.randrange(SIZE*2) bisect.insort(my_list,
new_item) print('%2d ->' % new_item, my_list)
Al igual que bisect , insort toma argumentos opcionales lo, hi para limitar la búsqueda a una
subsecuencia. También hay una variación insort_left que usa bisect_left para encontrar puntos
de inserción.
Mucho de lo que hemos visto hasta ahora en este capítulo se aplica a las sucesiones en
general, no solo a las listas o tuplas. Los programadores de Python a veces abusan del tipo de
lista porque es muy útil, sé que lo he hecho. Si está manejando listas de números, las matrices
son el camino a seguir. El resto del capítulo está dedicado a ellos.
En el resto de este capítulo, analizaremos los tipos de secuencias mutables que pueden reemplazar
listas en muchos casos, comenzando con arreglos.
Arrays
Si la lista solo contendrá números, un array.array es más eficiente que una lista:
admite todas las operaciones de secuencia mutable (incluidos .pop, .insert y .extend)
y métodos adicionales para cargar y guardar rápidamente, como . frombytes y .tofile.
Una matriz de Python es tan simple como una matriz de C. Al crear una matriz, proporciona un código
de tipo, una letra para determinar el tipo de C subyacente utilizado para almacenar cada elemento en la
matriz. Por ejemplo, b es el código de tipo para char firmado. Si crea una matriz ('b'), cada elemento se
almacenará en un solo byte y se interpretará como un número entero de –128 a 127. Para grandes
secuencias de números, esto ahorra mucha memoria. Y Python no le permitirá poner ningún número
que no coincida con el tipo de la matriz.
El ejemplo 2-20 muestra cómo crear, guardar y cargar una matriz de 10 millones de números aleatorios
de punto flotante.
Verdadero
Cree una matriz de flotantes de precisión doble (código de tipo 'd') a partir de cualquier objeto iterable;
en este caso, una expresión generadora.
Como puede ver, array.tofile y array.fromfile son fáciles de usar. Si prueba el ejemplo, notará que también son
muy rápidos. Un experimento rápido muestra que array.fromfile tarda aproximadamente 0,1 s en cargar 10
millones de flotantes de doble precisión desde un archivo binario creado con array.tofile. Eso es casi 60 veces
más rápido que leer los números de un archivo de texto, lo que también implica analizar cada línea con el
flotante incorporado. Guardar con array.tofile es unas 7 veces más rápido que escribir un flotante por línea en
un archivo de texto.
Además, el tamaño del archivo binario con 10 millones de dobles es de 80.000.000 bytes (8 bytes por doble,
cero sobrecarga), mientras que el archivo de texto tiene 181.515.739 bytes, para los mismos datos.
Para el caso específico de arreglos numéricos que representan datos binarios, como imágenes raster, Python
tiene los tipos de bytes y bytearray discutidos en el Capítulo 4.
Concluimos esta sección sobre arreglos con la Tabla 2-2, comparando las características de lista y
matriz.matriz.
Tabla 2-2. Métodos y atributos encontrados en una lista o matriz (métodos de matriz en desuso
y aquellos también implementados por objeto fueron omitidos por brevedad)
matriz de lista
s.intercambio de bytes() • Intercambie bytes de todos los elementos en la matriz para la conversión de endianess
s.__contiene__(e) •• e en s
s.frombytes(b) • Agregar elementos de la secuencia de bytes interpretados como valores de máquina empaquetados
s.fromfile(f, n) • Agregue n elementos del archivo binario f interpretados como valores de máquina empaquetados
de la lista (l) • Agregar elementos de la lista; si uno causa TypeError, no se agrega ninguno
s.sort([clave], [reversa]) • Ordene los elementos en su lugar con la clave de argumentos de palabras clave opcionales y revierta
en el archivo (f) • Guardar elementos como valores de máquina empaquetados en un archivo binario f
matriz de lista
•
s.typecode Cadena de un carácter que identifica el tipo C de los elementos
a = array.array(a.typecode, sorted(a))
Para mantener ordenada una matriz ordenada mientras le agrega
elementos, use la función bisect.insort (como se ve en “Insertar con
bisect.insort” en la página 47).
Si trabaja mucho con arreglos y no conoce la vista de memoria, se lo está perdiendo. Consulte el siguiente
tema.
Vistas de memoria
La clase memorview incorporada es un tipo de secuencia de memoria compartida que le permite manejar
segmentos de matrices sin copiar bytes. Se inspiró en la biblioteca NumPy (de la que hablaremos en breve
en “NumPy y SciPy” en la página 52). Travis Oliphant, autor principal de Num-Py, responde ¿Cuándo se
debe usar una vista de memoria? como esto:
Usando una notación similar al módulo de matriz , el método memoryview.cast le permite cambiar la forma
en que se leen o escriben múltiples bytes como unidades sin mover los bits (al igual que el operador C cast ).
memoryview.cast devuelve otro objeto de vista de memoria, que siempre comparte la misma memoria.
Consulte el Ejemplo 2-21 para ver un ejemplo de cambio de un solo byte de una matriz de enteros de 16 bits.
Ejemplo 2-21. Cambiar el valor de un elemento de la matriz empujando uno de sus bytes
>>> memv[0]
-2 >>>
memv_oct = memv.cast('B') >>>
memv_oct.tolist() [254, 255, 255,
255, 0, 0, 1, 0, 2, 0 ] >>> memv_oct[5] = 4
>>> matriz
de números ('h', [-2, -1, 1024, 1, 2])
Cree una vista de memoria a partir de una matriz de 5 enteros cortos con signo (código de
Cree memv_oct convirtiendo los elementos de memv en el código de tipo 'B' (caracter sin firmar).
Tenga en cuenta el cambio de números: un 4 en el byte más significativo de un entero sin signo de 2
bytes es 1024.
Veremos otro breve ejemplo con memoryview en el contexto de manipulaciones de secuencias binarias con
struct (Capítulo 4, Ejemplo 4-4).
Mientras tanto, si está realizando un procesamiento numérico avanzado en matrices, debería usar las bibliotecas
NumPy y SciPy. Echaremos un breve vistazo a ellos de inmediato.
de este libro, insisto en resaltar lo que ya está en la biblioteca estándar de Python para que pueda aprovecharlo
al máximo. Pero NumPy y SciPy son tan asombrosos que se justifica un desvío.
Para operaciones avanzadas de arreglos y matrices, NumPy y SciPy son la razón por la cual Python se convirtió
en la corriente principal de las aplicaciones informáticas científicas. NumPy implementa arreglos homogéneos
multidimensionales y tipos de matrices que contienen no solo números sino también registros definidos por el
usuario, y proporciona operaciones eficientes por elementos.
SciPy es una biblioteca, escrita sobre NumPy, que ofrece muchos algoritmos de computación científica de
álgebra lineal, cálculo numérico y estadística. SciPy es rápido y confiable porque aprovecha la base de código C
y Fortran ampliamente utilizada del repositorio de Netlib. En otras palabras, SciPy brinda a los científicos lo mejor
de ambos mundos: un indicador interactivo y API de Python de alto nivel, junto con funciones de procesamiento
de números de potencia industrial optimizadas en C y Fortran.
Como demostración muy breve, el ejemplo 2-22 muestra algunas operaciones básicas con arreglos
bidimensionales en NumPy.
>>> tipo(a)
<clase 'numpy.ndarray'>
>>> una
forma (12,)
>>> a.forma = 3, 4
>>> un
matriz ([[ 0, 1, 2, 3],
[4, 5, 6, 7],
[8, 9, 10, 11]])
>>> una
matriz [2] ([ 8, 9, 10, 11])
>>> un[2, 1]
9
>>> a[:, 1]
array([1, 5, 9]) >>>
a.transponer()
matriz ([[ 0, 4, 8],
[ 1, 5, 9],
[2, 6, 10],
[3, 7, 11]])
NumPy también admite operaciones de alto nivel para cargar, guardar y operar en todos
elementos de un numpy.ndarray:
>>> float2[-3:]
memmap([ 3016362.69195522, 535281.10514262, 4566560.44373946])
Utilice la notación de corte de secuencia para inspeccionar los últimos tres números.
Multiplique cada elemento de la matriz de flotadores por 0,5 e inspeccione los últimos tres elementos
nuevamente.
Importe el temporizador de medición de rendimiento de alta resolución (disponible desde Python 3.3).
Divide cada elemento por 3; el tiempo transcurrido para 10 millones de flotadores es inferior a 40
milisegundos.
Cargue los datos como un archivo asignado a la memoria en otra matriz; esto permite un procesamiento
eficiente de las porciones de la matriz incluso si no cabe por completo en la memoria.
Inspeccione los últimos tres elementos después de multiplicar cada elemento por 6.
Esto fue solo un aperitivo. NumPy y SciPy son bibliotecas formidables y son la base de otras herramientas
asombrosas, como las bibliotecas de análisis de datos Pandas y Blaze , que proporcionan tipos de matriz eficientes
que pueden contener datos no numéricos, así como funciones de importación/exportación compatibles con muchos
formatos diferentes ( ej., .csv, .xls, volcados de SQL, HDF5, etc.). Estos paquetes merecen libros enteros sobre
ellos. Este no es uno de esos libros.
Pero ninguna descripción general de las secuencias de Python estaría completa sin al menos una mirada rápida a
las matrices NumPy.
Después de analizar las secuencias planas (arreglos estándar y arreglos NumPy), ahora pasamos a un conjunto
completamente diferente de reemplazos para la lista simple y antigua: las colas.
métodos .append y .pop hacen que una lista se pueda usar como una pila o una cola (si usa .append y .pop(0),
obtiene el comportamiento LIFO). Pero insertar y quitar desde la izquierda de una lista (el extremo del índice
0) es costoso porque se debe cambiar toda la lista.
La clase collections.deque es una cola de dos extremos segura para subprocesos diseñada para insertar y
quitar rápidamente de ambos extremos. También es el camino a seguir si necesita mantener una lista de
"elementos vistos por última vez" o algo así, porque un deque puede estar acotado, es decir, creado con una
longitud máxima, y luego, cuando está lleno, se descarta. elementos del extremo opuesto cuando agrega otros
nuevos. El ejemplo 2-23 muestra algunas operaciones típicas realizadas en un deque.
El argumento maxlen opcional establece el número máximo de elementos permitidos en esta instancia
de deque; esto establece un atributo de instancia maxlen de solo lectura.
Girar con n > 0 toma elementos del extremo derecho y los antepone a la izquierda; cuando n < 0
elementos se toman de la izquierda y se agregan a la derecha.
Agregar a un deque que está lleno (len(d) == d.maxlen) descarta elementos del otro extremo; tenga
en cuenta en la siguiente línea que el 0 se elimina.
Agregar tres elementos a la derecha empuja hacia afuera el extremo izquierdo -1, 1 y 2.
Tenga en cuenta que extendleft(iter) funciona agregando cada elemento sucesivo del argumento iter
a la izquierda del deque, por lo tanto, la posición final de los elementos se invierte.
La Tabla 2-3 compara los métodos que son específicos de list y deque (eliminando los
que también aparecen en objeto).
Tenga en cuenta que deque implementa la mayoría de los métodos de lista y agrega algunos específicos a su
diseño, como popleft y rotar. Pero hay un costo oculto: quitar elementos de la
medio de un deque no es tan rápido. Está realmente optimizado para agregar y abrir desde
Los finales.
Las operaciones append y popleft son atómicas, por lo que es seguro usar deque como una cola LIFO
en aplicaciones multiproceso sin necesidad de utilizar bloqueos.
Tabla 2-3. Métodos implementados en list o deque (aquellos que también son implementados por
el objeto se omitió por brevedad)
lista de que
s.__añadir__(s2) • s + s2: concatenación
s.__iadd__(s2) •• s += s2: concatenación en el lugar
s.append(e) •• Agregar un elemento a la derecha (después del último)
s.__contiene__(e) • e en s
lista de que
s.__setitem__(p, e) ••
s[p] = e: coloque e en la posición p, sobrescribiendo el elemento existente
s.sort([clave], [reversa]) • Ordenar elementos en su lugar con clave de argumentos de palabra clave opcional y viceversa
cola
Esto proporciona las clases sincronizadas (es decir, seguras para subprocesos) Queue,
LifoQueue y PriorityQueue. Estos se utilizan para la comunicación segura entre subprocesos.
Las tres clases se pueden acotar proporcionando un argumento maxsize mayor que 0 al
constructor. Sin embargo, no descartan elementos para hacer espacio como lo hace deque . En
cambio, cuando la cola está llena, la inserción de un nuevo elemento se bloquea; es decir,
espera hasta que algún otro subproceso haga espacio al tomar un elemento de la cola, lo que
es útil para limitar el número de subprocesos activos.
multiprocesamiento
Implementa su propia Cola delimitada, muy similar a queue.Queue pero diseñada para la
comunicación entre procesos. También está disponible un multiprocessing.JoinableQueue
especializado para facilitar la gestión de tareas.
asyncio
Recientemente agregado a Python 3.4, asyncio proporciona Queue, LifoQueue,
PriorityQueue y JoinableQueue con API inspiradas en las clases contenidas en los
módulos de cola y multiprocesamiento , pero adaptadas para administrar tareas en
programación asíncrona.
heapq
A diferencia de los tres módulos anteriores, heapq no implementa una clase de cola, pero
proporciona funciones como heappush y heappop que le permiten usar una secuencia mutable
como cola de montón o cola de prioridad.
Esto finaliza nuestra descripción general de las alternativas al tipo de lista , y también nuestra
exploración de los tipos de secuencia en general, excepto por los detalles de las secuencias str y
binarias, que tienen su propio capítulo (Capítulo 4).
Las secuencias de Python a menudo se clasifican como mutables o inmutables, pero también es útil considerar
un eje diferente: secuencias planas y secuencias contenedoras. Los primeros son más compactos, rápidos y
fáciles de usar, pero se limitan a almacenar datos atómicos como números, caracteres y bytes. Las secuencias
de contenedores son más flexibles, pero pueden sorprenderlo cuando contienen objetos mutables, por lo que
debe tener cuidado de usarlos correctamente con estructuras de datos anidadas.
Las comprensiones de lista y las expresiones generadoras son notaciones poderosas para construir e inicializar
secuencias. Si aún no se siente cómodo con ellos, tómese el tiempo para dominar su uso básico. No es difícil, y
pronto te enganchará.
Las tuplas en Python juegan dos roles: como registros con campos sin nombre y como listas inmutables.
Cuando se utiliza una tupla como registro, el desempaquetado de tuplas es la forma más segura y legible de
*
acceder a los campos. La nueva sintaxis más
hacefácil
queignorar
el desempaquetado
algunos campos
de tuplas
y tratarsea
conaún
campos
mejoropcionales.
al hacer queLas
sea
tuplas con nombre no son tan nuevas, pero merecen más atención: al igual que las tuplas, tienen muy poca
sobrecarga por instancia, pero brindan un acceso conveniente a los campos por nombre y un útil ._asdict() para
exportar el registro como un OrderedDict.
La división de secuencias es una característica favorita de la sintaxis de Python, y es incluso más poderosa de
lo que muchos creen. La notación de corte multidimensional y puntos suspensivos (...) , como se usa en NumPy,
también puede ser compatible con secuencias definidas por el usuario. La asignación a segmentos es una forma
muy expresiva de editar secuencias mutables.
*
La concatenación repetida como en la secuencia n es conveniente y, con cuidado, puede usarse para inicializar
listas de listas que contienen elementos inmutables. La asignación aumentada con += y *= se comporta de
manera diferente para secuencias mutables e inmutables. En este último caso, estos operadores necesariamente
construyen nuevas secuencias. Pero si la secuencia de destino es mutable, generalmente se cambia de lugar,
pero no siempre, dependiendo de cómo se implemente la secuencia.
El método de ordenación y la función incorporada ordenada son fáciles de usar y flexibles, gracias al argumento
opcional clave que aceptan, con una función para calcular el criterio de ordenación. Por cierto, la tecla también
se puede usar con las funciones integradas min y max . Para mantener una secuencia ordenada en orden,
siempre inserte elementos en ella usando bisect.insort; para buscarlo de manera eficiente, use bisect.bisect.
Más allá de listas y tuplas, la biblioteca estándar de Python proporciona array.array. Aunque NumPy y SciPy no
son parte de la biblioteca estándar, si realiza algún tipo de procesamiento numérico en grandes conjuntos de
datos, estudiar incluso una pequeña parte de estas bibliotecas puede llevarle un largo camino.
Cerramos visitando el versátil y seguro subprocesos collections.deque, comparando su API con la de list en la
Tabla 2-3 y mencionando otras implementaciones de colas en la biblioteca estándar.
Otras lecturas
El capítulo 1, "Estructuras de datos" de Python Cookbook, 3.ª edición (O'Reilly) de David Beazley y
Brian K. Jones tiene muchas recetas centradas en secuencias, incluida la "Receta 1.11. Naming a
Slice”, del cual aprendí el truco de asignar divisiones a variables para mejorar la legibilidad, ilustrado
en nuestro Ejemplo 2-11.
La segunda edición de Python Cookbook se escribió para Python 2.4, pero gran parte de su código
funciona con Python 3 y muchas de las recetas de los capítulos 5 y 6 tratan sobre secuencias. El libro
fue editado por Alex Martelli, Anna Martelli Ravenscroft y David Ascher, e incluye contribuciones de
docenas de pitonistas. La tercera edición se reescribió desde cero y se enfoca más en la semántica del
lenguaje, particularmente en lo que ha cambiado en Python 3, mientras que el volumen anterior enfatiza
la pragmática (es decir, cómo aplicar el lenguaje a problemas del mundo real). Aunque algunas de las
soluciones de la segunda edición ya no son el mejor enfoque, sinceramente creo que vale la pena tener
a mano ambas ediciones de Python Cookbook .
El CÓMO de clasificación oficial de Python tiene varios ejemplos de trucos avanzados para usar sorted
y list.sort.
PEP 3132 — Extended Iterable Unpacking es la fuente canónica para leer sobre el nuevo uso de *extra
como objetivo en asignaciones paralelas. Si desea echar un vistazo a la evolución de Python, Missing
*-unpacking generalizations es un problema de seguimiento de errores que propone un uso aún más
amplio de la notación iterable de desempaquetado. PEP 448 — Generalizaciones adicionales de
desempaquetado surgieron de las discusiones en ese número. En el momento de escribir este artículo,
parece probable que los cambios propuestos se fusionen con Python, quizás en la versión 3.5.
La publicación de blog de Eli Bendersky “Less Copies in Python with the Buffer Protocol and
memoryviews” incluye un breve tutorial sobre memoryview.
Hay numerosos libros que cubren NumPy en el mercado, incluso algunos que no mencionan “NumPy”
en el título. Python para análisis de datos de Wes McKinney (O'Reilly) es uno de esos títulos.
A los científicos les encanta tanto la combinación de un indicador interactivo con el poder de NumPy y
SciPy que desarrollaron IPython, un reemplazo increíblemente poderoso para la consola de Python que
también proporciona una GUI, trazado de gráficos en línea integrado, soporte de programación
alfabetizada (texto intercalado con código) y renderizado a PDF. Las sesiones IPython interactivas y
multimedia pueden incluso compartirse a través de HTTP como cuadernos IPython. Vea capturas de
pantalla y videos en The IPython Notebook. IPython está tan de moda que en 2012 sus principales
desarrolladores, la mayoría de los cuales son investigadores de UC Berkeley, recibieron una subvención
de 1,15 millones de dólares de la Fundación Sloan para implementar mejoras durante el período
2013-2014.
En la biblioteca estándar de Python, 8.3. colecciones: los tipos de datos de contenedor incluyen
ejemplos breves y recetas prácticas que usan deque (y otras colecciones).
Lectura adicional | 59
Machine Translated by Google
Plataforma improvisada
La naturaleza de las
tuplas En 2012, presenté un póster sobre el lenguaje ABC en PyCon US. Antes de crear Python, Guido
había trabajado en el intérprete de ABC, así que vino a ver mi póster. Entre otras cosas, hablamos de los
compuestos ABC, que son claramente los predecesores de las tuplas de Python. Los compuestos también
admiten la asignación en paralelo y se utilizan como claves compuestas en los diccionarios (o tablas, en
el lenguaje ABC). Sin embargo, los compuestos no son secuencias. No son iterables y no puede recuperar
un campo por índice, y mucho menos dividirlos. O maneja el compuesto como un todo o extrae los campos
individuales mediante asignación paralela, eso es todo.
Le dije a Guido que estas limitaciones dejan muy claro el propósito principal de los compuestos: son solo
registros sin nombres de campo. Su respuesta: “Hacer que las tuplas se comportaran como secuencias
fue un truco”.
Esto ilustra el enfoque pragmático que hace que Python sea mucho mejor y más exitoso que ABC. Desde
la perspectiva del implementador del lenguaje, hacer que las tuplas se comporten como secuencias cuesta
poco. Como resultado, las tuplas pueden no ser tan “conceptualmente puras” como los compuestos, pero
tenemos muchas más formas de usarlas. ¡Incluso se pueden usar como listas inmutables, de todas las
cosas!
Es realmente útil tener listas inmutables en el lenguaje, incluso si su tipo no se llama lista congelada sino
que en realidad es una tupla que se comporta como una secuencia.
El uso de la sintaxis *extra para asignar múltiples elementos a un parámetro comenzó con las definiciones
de funciones hace mucho tiempo (tengo un libro sobre Python 1.4 de 1996 que cubre eso). A partir de
Python 1.6, el formulario *extra se puede usar en el contexto de las llamadas a funciones para descomprimir
un iterable en múltiples argumentos, una operación complementaria. Esto es elegante, tiene un sentido
intuitivo y hace que la función de aplicación sea redundante (ya no está).
Ahora, con Python 3, la notación *extra también funciona a la izquierda de las asignaciones paralelas para
captar los elementos sobrantes, mejorando lo que ya era una característica útil del lenguaje.
Con cada uno de estos cambios, el lenguaje se volvió más flexible, más consistente y más simple al mismo tiempo. “La
elegancia engendra simplicidad” es el lema de mi camiseta PyCon favorita de Chicago, 2009. Está decorada con una
pintura de Bruce Eckel que representa el hexagrama 22 del I Ching, ÿ (bì), “Adorno”, a veces traducido como “ Gracia”
o “Belleza”.
contenedoras Para resaltar los diferentes modelos de memoria de los tipos de secuencias, utilicé los términos
secuencia contenedora y secuencia plana. La palabra "contenedor" proviene de la documentación del modelo de datos:
Usé el término "secuencia de contenedores" para ser específico, porque hay contenedores en Python que no son
secuencias, como dict y set. Las secuencias de contenedor se pueden anidar porque pueden contener objetos de
cualquier tipo, incluido su propio tipo.
Por otro lado, las secuencias planas son tipos de secuencia que no se pueden anidar porque solo contienen tipos
atómicos simples como enteros, flotantes o caracteres.
Adopté el término secuencia plana porque necesitaba algo para contrastar con "secuencia de contenedor". No puedo
citar una referencia para respaldar el uso de secuencia plana en este contexto específico: como la categoría de tipos
de secuencia de Python que no son contenedores. En Wikipedia, este uso se etiquetaría como "investigación original".
Prefiero llamarlo “nuestro término”, con la esperanza de que lo encuentre útil y lo adopte también.
Los textos introductorios de Python enfatizan que las listas pueden contener objetos de tipos mixtos, pero en la práctica
esa característica no es muy útil: ponemos elementos en una lista para procesarlos más tarde, lo que implica que todos
los elementos deben admitir al menos alguna operación en común (es decir, todos deberían "graznar" ya sea que sean
genéticamente 100% patos o no). Por ejemplo, no puede ordenar una lista en Python 3 a menos que los elementos que
contiene sean comparables:
>>> l = [28, 14, '28', 5, '9', '1', 0, 6, '23', 19] >>> ordenado(l)
A diferencia de las listas, las tuplas suelen contener elementos de diferentes tipos. Eso es natural, considerando que
cada elemento de una tupla es realmente un campo, y cada tipo de campo es independiente de los demás.
La clave es brillante
El argumento opcional clave de list.sort, sorted, max y min es una gran idea. Otros lenguajes
lo obligan a proporcionar una función de comparación de dos argumentos como la función
obsoleta cmp(a, b) en Python 2. Usar la clave es más simple y más eficiente. Es más simple
porque solo define una función de un argumento que recupera o calcula cualquier criterio
que desee usar para ordenar sus objetos; esto es más fácil que escribir un argumento de dos
Lectura adicional | 61
Machine Translated by Google
función para devolver –1, 0, 1. También es más eficiente porque la función clave se invoca solo una vez
por elemento, mientras que la comparación de dos argumentos se llama cada vez que el algoritmo de
clasificación necesita comparar dos elementos. Por supuesto, Python también tiene que comparar las
claves durante la clasificación, pero esa comparación se realiza en código C optimizado y no en una función
de Python que haya escrito.
Por cierto, el uso de key nos permite ordenar una mezcla de números y cadenas similares a números. Solo
necesita decidir si desea tratar todos los elementos como enteros o cadenas:
>>> l = [28, 14, '28', 5, '9', '1', 0, 6, '23', 19] >>> ordenado(l,
clave=int) [0, '1 ', 5, 6, '9', 14, 19, '23', 28, '28'] >>> ordenado(l,
clave=cadena) [0, '1', 14, 19, '23', 28, '28', 5, 6, '9']
El algoritmo de clasificación utilizado en sorted y list.sort es Timsort, un algoritmo adaptativo que cambia
de estrategias de clasificación por inserción a estrategias de clasificación por fusión, según el orden de los
datos. Esto es eficiente porque los datos del mundo real tienden a tener series de elementos ordenados.
Hay un artículo de Wikipedia al respecto.
Timsort se implementó por primera vez en CPython, en 2002. Desde 2009, Timsort también se usa para
clasificar arreglos tanto en Java estándar como en Android, un hecho que se hizo ampliamente conocido
cuando Oracle usó parte del código relacionado con Timsort como evidencia de la infracción de Google de
Sun. propiedad intelectual. Ver Oracle v. Google - Presentaciones del día 14.
Timsort fue inventado por Tim Peters, un desarrollador central de Python tan prolífico que se cree que es
una IA, el Timbot. Puedes leer sobre esa teoría de la conspiración en Python Humor. Tim también escribió
El zen de Python: importa esto.
CAPÍTULO 3
Diccionarios y Juegos
Cualquier programa de Python en ejecución tiene muchos diccionarios activos al mismo tiempo, incluso si el
código del programa del usuario no usa explícitamente un diccionario.
— AM Kuchling
Capítulo 18, “Implementación del diccionario de Python
El tipo dict no solo se usa ampliamente en nuestros programas, sino que también es una parte fundamental
de la implementación de Python. Los espacios de nombres de módulos, los atributos de clase e instancia y
los argumentos de palabras clave de función son algunas de las construcciones fundamentales en las que
se implementan los diccionarios. Las funciones integradas viven en __builtins__.__dict__.
Debido a su papel crucial, los dictados de Python están altamente optimizados. Las tablas hash son los
motores detrás de los dictados de alto rendimiento de Python.
También cubrimos conjuntos en este capítulo porque también se implementan con tablas hash.
Saber cómo funciona una tabla hash es clave para aprovechar al máximo los diccionarios y conjuntos.
• Implicaciones de las tablas hash (limitaciones de tipo de clave, ordenación impredecible, etc.)
63
Machine Translated by Google
Figura 3-1. Diagrama de clases UML para MutableMapping y sus superclases de collections.abc
(las flechas de herencia apuntan de las subclases a las superclases; los nombres en cursiva son
clases abstractas y métodos abstractos)
>>> mi_dict = {}
>>> isinstance(my_dict, abc.Mapping)
Verdadero
Usar isinstance es mejor que verificar si un argumento de función es del tipo dict , porque
entonces se pueden usar tipos de mapeo alternativos.
Todos los tipos de mapeo en la biblioteca estándar usan el dict básico en su implementación, por
lo que comparten la limitación de que las claves deben ser hashable (los valores no necesitan
ser hashable, solo las claves).
¿Qué es hashable?
Aquí hay parte de la definición de hashable del Glosario de Python:
Un objeto es hashable si tiene un valor hash que nunca cambia durante su vida útil
(necesita un método __hash__() ) y se puede comparar con otros objetos (necesita un
método __eq__() ). Los objetos hashables que se comparan iguales deben tener el mismo
valor hash. […]
Los tipos atómicos inmutables (str, bytes, tipos numéricos) son todos modificables. Un conjunto
congelado siempre es hashable, porque sus elementos deben ser hashable por definición. Una tupla
es hashable solo si todos sus elementos son hashable. Véanse las tuplas tt, tl y tf:
Los tipos definidos por el usuario se pueden modificar de forma predeterminada porque su valor
de hash es su id() y no se comparan entre sí. Si un objeto implementa un __eq__ personalizado
que tiene en cuenta su estado interno, puede ser hash solo si todos sus atributos son inmutables.
Dadas estas reglas básicas, puede crear diccionarios de varias maneras. La página Tipos
incorporados en la Referencia de la biblioteca tiene este ejemplo para mostrar los diversos
medios para crear un dictado:
Verdadero
Además de la sintaxis literal y el constructor dict flexible , podemos usar dict comÿ
prensiones para construir diccionarios. Consulte la siguiente sección.
dict Comprensiones
>>> CÓDIGOS_MARCAR = [
... (86, 'China'),
... (91, 'India'),
... (1, 'Estados Unidos'),
... (62, 'Indonesia'),
... (55, 'Brasil'),
... (92, 'Pakistán'),
... (880, 'Bangladés'),
... (234, 'Nigeria'),
... (7, 'Rusia'),
... (81, 'Japón'),
... ]
>>> código_país = {país: código para código, país en DIAL_CODES} >>>
código_país
{'China': 86, 'India': 91, 'Bangladesh': 880, 'Estados Unidos': 1,
'Pakistán': 92, 'Japón': 81, 'Rusia': 7, 'Brasil': 55, 'Nigeria':
234, 'Indonesia': 62}
>>> {código: país.superior() para país, código en código_país.elementos() ... si el
código < 66}
{1: 'ESTADOS UNIDOS', 55: 'BRASIL', 62: 'INDONESIA', 7: 'RUSIA'}
Volviendo a invertir los pares, valores en mayúsculas y elementos filtrados por código < 66.
Si está acostumbrado a los liscomps, los dictcomps son el siguiente paso natural. Si no es así, la propagación de
la sintaxis de listcomp significa que ahora es más rentable que nunca dominarlo.
La API básica para mapeos es bastante rica. La Tabla 3-1 muestra los métodos implementados
by dict y dos de sus variaciones más útiles: defaultdict y OrderedDict, ambas
definido en el módulo de colecciones .
d.claro()
•• • Eliminar todos los elementos
d.__contiene__(k)
•• • tipo
d.copia()
•• • Copia superficial
d.__copia__()
• Soporte para copiar.copiar
d.default_factory
• Llamable invocado por __missing__ para establecer
valores faltantesa
d.__eliminar__(k)
•• • del d[k]: elimina el elemento con la tecla k
d.get(k, [predeterminado])
•• • Obtener elemento con clave k, devolver predeterminado o Ninguno
si falta
d.__getitem__(k)
•• • d[k]: obtiene el elemento con la tecla k
d.elementos()
•• • Obtener vista de elementos: pares (clave, valor)
d.__iter__()
•• • Obtener iterador sobre claves
d.teclas()
•• • Obtener vista sobre claves
d.__len__()
•• • len(d)—número de elementos
d.__desaparecido__(k)
• Llamadocuando__getitem__nopuedeencontrarlaclave
d.move_to_end(k, [último])
• Mover k primera o última posición (la última es Verdadera por
defecto)
d.popitem()
•• • Eliminar y devolver un valor arbitrario (key, val
ue) artículob
d.__invertido__()
• Obtener iterador para claves desde la última hasta la primera insertada
d.__setitem__(k, v)
•• • d[k] = v—poner v en k
d.update(m, [**kargs])
•• • Actualice d con elementos de mapeo o iterable de
(clave, valor) pares
d.valores()
•• • Obtener vista sobre los valores
una fábrica_predeterminada no es un método, sino un atributo de instancia invocable establecido por el usuario final cuando se crea una instancia de dictamen predeterminado.
b
OrderedDict.popitem() elimina el primer elemento insertado (FIFO); un último argumento opcional , si se establece en True, aparece el
último elemento (LIFO).
La forma en que update maneja su primer argumento m es un excelente ejemplo de tipificación pato:
primero verifica si m tiene un método de claves y, si lo tiene, asume que es un mapeo. De lo contrario, la
actualización recurre a iterar sobre m, asumiendo que sus elementos son pares (clave, valor) . El
constructor para la mayoría de las asignaciones de Python utiliza la lógica de actualización internamente,
lo que significa que se pueden inicializar desde otras asignaciones o desde cualquier objeto iterable que
produzca pares (clave, valor) .
Se establece un método de mapeo sutil por defecto. No siempre lo necesitamos, pero cuando lo
necesitamos, proporciona una aceleración significativa al evitar búsquedas de claves redundantes. Si no
te sientes cómodo utilizándolo, en el siguiente apartado se explica cómo, a través de un ejemplo práctico.
con la filosofía de fallo rápido , el acceso a dictados con d [k] genera un error cuando k no es una clave
existente. Todo Pythonista sabe que d.get(k, default) es una alternativa a d[k] siempre que un valor
predeterminado sea más conveniente que manejar KeyError. Sin embargo, al actualizar el valor encontrado
(si es mutable), usar __getitem__ o get es incómodo e ineficiente. Considere el Ejemplo 3-2, una secuencia
de comandos subóptima escrita solo para mostrar un caso en el que dict.get no es la mejor manera de
manejar una clave faltante.
El ejemplo 3-2 está adaptado de un ejemplo de Alex Martelli,1 que genera un índice como el del ejemplo
3-3.
Ejemplo 3-2. index0.py usa dict.get para obtener y actualizar una lista de ocurrencias de palabras del
índice (una mejor solución está en el Ejemplo 3-4)
importar
sistema importar re
WORD_RE = re.compilar('\w+')
index = {}
con open(sys.argv[1], encoding='utf-8') como fp: for
line_no, line in enumerate(fp, 1):
for match in WORD_RE.finditer(line): word
= match.group() column_no =
match.start()+1 location = (line_no,
column_no ) # esto es feo; codificado
así para hacer un punto ocurrencias = index.get(palabra,
[]) ocurrencias.append(ubicación) índice[palabra] =
ocurrencias
Ponga las ocurrencias modificadas en el dictado de índice ; esto implica una segunda búsqueda a
través del índice.
En el argumento key= de sorted , no estoy llamando a str.upper, solo paso una referencia a ese
método para que la función sorted pueda usarlo para normalizar las palabras para ordenar.2
Ejemplo 3-3. Salida parcial del Ejemplo 3-2 procesando el Zen de Python; cada línea muestra una palabra y
una lista de ocurrencias codificadas como pares: (línea-número, columna-número)
...
Las tres líneas que se ocupan de las ocurrencias en el Ejemplo 3-2 se pueden reemplazar por una sola línea
usando dict.setdefault. El ejemplo 3-4 está más cerca del ejemplo original de Alex Martelli.
Ejemplo 3-4. index.py usa dict.setdefault para obtener y actualizar una lista de ocurrencias de palabras del
índice en una sola línea; contraste con el ejemplo 3-2
importar
sistema importar re
WORD_RE = re.compilar('\w+')
índice = {}
2. Este es un ejemplo del uso de un método como una función de primera clase, el tema del Capítulo 5.
my_dict.setdefault(clave, []).append(nuevo_valor)
si la clave no está en
my_dict: my_dict[key]
= [] my_dict[key].append(new_value)
…excepto que el último código realiza al menos dos búsquedas de clave, tres si no se encuentra, mientras
que setdefault lo hace todo con una sola búsqueda.
3-5 usa collections.defaultdict para proporcionar otra solución elegante al problema del ejemplo 3-4. Se
configura un dictado predeterminado para crear elementos a pedido cada vez que se busca una clave
faltante.
Así es como funciona: cuando crea una instancia de un dictado predeterminado, proporciona un invocable
que se usa para producir un valor predeterminado cada vez que se pasa a __getitem__ un argumento
clave inexistente.
Por ejemplo, dado un dictado predeterminado vacío creado como dd = dictado predeterminado (lista), si
'nueva clave' no está en dd, la expresión dd ['nueva clave'] realiza los siguientes pasos:
El invocable que produce los valores predeterminados se mantiene en un atributo de instancia llamado
default_factory.
Ejemplo 3-5. index_default.py: usando una instancia de defaultdict en lugar del método setdefault
importar
sistema
importar volver a importar colecciones
WORD_RE = re.compilar('\w+')
Si no se proporciona default_factory , se genera el KeyError habitual para las claves que faltan.
El mecanismo que hace que funcione defaultdict al llamar a default_factory es en realidad el método
especial __missing__ , una función compatible con todos los tipos de mapeo estándar que analizamos
a continuación.
a la forma en que las asignaciones tratan con las claves faltantes se encuentra el método
__missing__ , acertadamente llamado. Este método no está definido en la clase dict base , pero dict
lo sabe: si crea una subclase de dict y proporciona un método __missing__ , el dict.__getitem__
estándar lo llamará cada vez que no se encuentre una clave, en lugar de generar KeyError.
Suponga que desea una asignación en la que las claves se conviertan en str cuando se busque. Un
caso de uso concreto es el proyecto Pingo.io , donde una placa programable con pines GPIO (p. ej.,
Raspberry Pi o Arduino) se representa mediante un objeto de placa con un atributo board.pins , que
es una asignación de ubicaciones físicas de pines para anclar objetos y la ubicación física puede ser
solo un número o una cadena como "A0" o "P9_12". Por consistencia, es deseable que todas las
teclas en board.pins sean cadenas, pero también es conveniente que buscar my_arduino.pin[13]
también funcione, para que los principiantes no se tropiecen cuando quieran hacer parpadear el LED
en el pin. 13 de sus Arduinos. El ejemplo 3-6 muestra cómo funcionaría tal mapeo.
Ejemplo 3-6. Al buscar una clave que no sea una cadena, StrKeyDict0 la convierte en str cuando no
se encuentra
Pruebas para la recuperación de elementos usando la notación `d[key]` ::
>>> d[4]
'cuatro'
>>> d[1]
Rastreo (última llamada más reciente):
...
Error de clave: '1'
>>> 2 en re
Verdadero >>> 1 en d
Falso
El ejemplo 3-7 implementa una clase StrKeyDict0 que pasa las pruebas anteriores.
Ejemplo 3-7. StrKeyDict0 convierte las claves que no son de cadena en str en la búsqueda (consulte las pruebas
en el Ejemplo 3-6)
clase StrKeyDict0(dict):
El método get delega a __getitem__ usando la notación self[key] ; que da la oportunidad a nuestros
__desaparecidos__ de actuar.
Busque una clave no modificada (la instancia puede contener claves que no sean str), luego busque una
str construida a partir de la clave.
Tómese un momento para considerar por qué la prueba isinstance(key, str) es necesaria en la implementación de
__missing__ .
Sin esa prueba, nuestro método __missing__ funcionaría bien para cualquier clave k (str o no str) siempre que str(k)
produjera una clave existente. Pero si str(k) no es una clave existente, tendríamos una recursividad infinita. La última
línea, self[str(key)] llamaría a __geti tem__ pasando esa tecla str , que a su vez llamaría a __missing__ de nuevo.
El método __contains__ también es necesario para un comportamiento consistente en este ejemplo, porque la
operación k en d lo llama, pero el método heredado de dict no recurre a invocar __missing__. Hay un detalle sutil en
nuestra implementación de __contains__: no buscamos la clave de la manera Pythonic habitual (k en my_dict)
porque str(key) en self llamaría recursivamente a __contains__. Evitamos esto buscando explícitamente la clave en
self.keys().
La comprobación de la clave no modificada (clave en self.keys()) es necesaria para que sea correcta porque
StrKeyDict0 no impone que todas las claves del diccionario deben ser del tipo str. Nuestro único objetivo con este
ejemplo simple es hacer que la búsqueda sea "más amigable" y no imponer tipos.
Hasta ahora hemos cubierto los tipos de mapeo dict y defaultdict , pero la biblioteca estándar viene con otras
implementaciones de mapeo, que discutiremos a continuación.
Variaciones de dict
En esta sección, resumimos los diversos tipos de mapeo incluidos en el módulo de colecciones de la
biblioteca estándar, además de defaultdict :
colecciones.OrderedDict
Mantiene las claves en el orden de inserción, lo que permite la iteración de elementos en un orden
predecible. El método popitem de un OrderedDict muestra el primer elemento de forma predeterminada,
pero si se llama como my_odict.popitem(last=True), muestra el último elemento agregado.
collections.ChainMap Contiene
una lista de asignaciones que se pueden buscar como una sola. La búsqueda se realiza en cada mapeo
en orden y tiene éxito si se encuentra la clave en cualquiera de ellos. Esto es útil para los intérpretes de
idiomas con ámbitos anidados, donde cada asignación representa un contexto de ámbito. La sección
"Objetos de ChainMap" de los documentos de las colecciones tiene varios ejemplos del uso de
ChainMap , incluido este fragmento inspirado en las reglas básicas de búsqueda de variables en Python:
importar
componentes incorporados pylookup = ChainMap (locals(), globales(), vars(incorporados))
colecciones.Contador
Una asignación que contiene un número entero para cada clave. La actualización de una clave existente
se suma a su cuenta. Esto se puede usar para contar instancias de objetos hashable (las claves) o
como un conjunto múltiple, un conjunto que puede contener varias ocurrencias de cada elemento. Encimera
implementa los operadores + y - para combinar conteos y otros métodos útiles como most_common([n]),
que devuelve una lista ordenada de tuplas con los n elementos más comunes y sus conteos; ver la
documentación. Aquí está Counter utilizado para contar letras en palabras:
Contador({'a': 10, 'z': 3, 'b': 2, 'r': 2, 'c': 1, 'd': 1}) >>> ct.most_common(2)
[ ('a', 10), ('z', 3)]
collections.UserDict Una
implementación Python pura de un mapeo que funciona como un dictado estándar.
Mientras que OrderedDict, ChainMap y Counter vienen listos para usar, UserDict está diseñado para
subclasificarse, como haremos a continuación.
Variaciones de dict | 75
Machine Translated by Google
Subclasificación de UserDict
Casi siempre es más fácil crear un nuevo tipo de asignación extendiendo UserDict en lugar de dict. Su valor se
puede apreciar cuando extendemos nuestro StrKeyDict0 del Ejemplo 3-7 para asegurarnos de que cualquier
clave agregada al mapeo se almacene como str.
La razón principal por la que es preferible subclasificar desde UserDict en lugar de dict es que el integrado tiene
algunos accesos directos de implementación que terminan obligándonos a anular métodos que podemos
heredar de UserDict sin problemas.3
Tenga en cuenta que UserDict no se hereda de dict, sino que tiene una instancia interna de dict , llamada data,
que contiene los elementos reales. Esto evita la recurrencia no deseada al codificar métodos especiales como
__setitem__ y simplifica la codificación de __contains__, en comparación con el ejemplo 3-7.
Gracias a UserDict, StrKeyDict (Ejemplo 3-8) es en realidad más corto que StrKeyDict0 (Ejemplo 3-7), pero
hace más: almacena todas las claves como str, evitando sorpresas desagradables si la instancia se crea o
actualiza con datos que contienen claves que no son cadenas. .
Ejemplo 3-8. StrKeyDict siempre convierte las claves que no son cadenas en cadenas, al insertarlas, actualizarlas
y buscarlas.
importar colecciones
clase StrKeyDict(colecciones.UserDict):
__contiene__ es más simple: podemos suponer que todas las claves almacenadas son str y podemos
verificar self.data en lugar de invocar self.keys() como hicimos en StrKeyDict0.
3. El problema exacto con la subclasificación de dict y otras funciones integradas se cubre en "Subclases de tipos integrados es
Difícil” en la página 348.
__setitem__ convierte cualquier clave en una cadena. Este método es más fácil de sobrescribir
cuando podemos delegar en el atributo self.data .
Debido a que UserDict subclase MutableMapping, los métodos restantes que hacen de StrKeyDict
una asignación completa se heredan de UserDict, MutableMapping o Mapping. Estos últimos tienen
varios métodos concretos útiles, a pesar de ser clases base abstractas (ABC). Cabe destacar los
siguientes métodos:
MutableMapping.update Este
poderoso método se puede llamar directamente, pero __init__ también lo usa para cargar la
instancia desde otras asignaciones, desde iterables de pares (clave, valor) y argumentos de
palabras clave. Debido a que usa self[key] = value para agregar elementos, termina llamando a
nuestra implementación de __setitem__.
Mapping.get
En StrKeyDict0 (Ejemplo 3-7), tuvimos que codificar nuestro propio get para obtener
resultados consistentes con __getitem__, pero en el Ejemplo 3-8 heredamos Mapping.get,
que se implementa exactamente como StrKeyDict0.get (ver código fuente de Python).
Sabemos que hay varios tipos de secuencias inmutables, pero ¿qué tal un diccionario inmutable?
Bueno, no hay uno real en la biblioteca estándar, pero hay un sustituto disponible.
sigue leyendo
Asignaciones inmutables
Los tipos de asignación proporcionados por la biblioteca estándar son todos mutables, pero es
posible que deba garantizar que un usuario no pueda cambiar una asignación por error. Se
puede encontrar un caso de uso concreto, de nuevo, en el proyecto Pingo.io que describí en “El
método __missing__” en la página 72: el mapeo board.pins representa los pines GPIO físicos
en el dispositivo. Como tal, es bueno evitar actualizaciones inadvertidas de board.pins porque
el hardware no se puede cambiar a través del software, por lo que cualquier cambio en el mapeo
lo haría inconsistente con la realidad física del dispositivo.
Asignaciones inmutables | 77
Machine Translated by Google
Desde Python 3.3, el módulo de tipos proporciona una clase contenedora llamada MappingProxy Type, que, dada
una asignación, devuelve una instancia de mappingproxy que es una vista de solo lectura pero dinámica de la
asignación original. Esto significa que las actualizaciones de la asignación original se pueden ver en el proxy de
asignación, pero no se pueden realizar cambios a través de él. Vea el Ejemplo 3-9 para una breve demostración.
Ejemplo 3-9. MappingProxyType crea una instancia de mappingproxy de solo lectura a partir de un dict
'A'
>>> d_proxy[2] = 'x'
Rastreo (llamadas recientes más última):
Archivo "<stdin>", línea 1, en <módulo>
TypeError: el objeto 'mappingproxy' no admite la asignación de elementos >>> d[2] = 'B'
>>> d_proxy mappingproxy({1: 'A', 2: 'B'}) >>> d_proxy[2]
'B'
>>>
Así es como esto podría usarse en la práctica en el escenario de Pingo.io: el constructor en una subclase Board
concreta llenaría un mapeo privado con los objetos pin y lo expondría a los clientes de la API a través de un
atributo .pins público implementado como un mapeo proxy. De esa forma, los clientes no podrían agregar, eliminar o
cambiar pines por accidente.4
Ahora que hemos cubierto la mayoría de los tipos de mapeo en la biblioteca estándar y cuándo usarlos, pasaremos
a los tipos establecidos.
4. En realidad, no estamos usando MappingProxyType en Pingo.io porque es nuevo en Python 3.3 y necesitamos compatibilidad
con Python 2.7 en este momento.
Teoría de conjuntos
Los conjuntos son una adición relativamente nueva en la historia de Python, y algo infrautilizados.
El tipo de conjunto y su hermano inmutable frozenset aparecieron por primera vez en un módulo en Python 2.3
y se promovieron a integrados en Python 2.6.
En este libro, la palabra “set” se usa para referirse tanto a set como a set
congelado. Cuando se habla específicamente de la clase set , su nombre
aparece en la fuente de ancho constante utilizada para el código fuente: set.
Un conjunto es una colección de objetos únicos. Un caso de uso básico es eliminar la duplicación:
Los elementos del conjunto deben ser hashable. El tipo set no es hashable, pero frozenset sí lo es, por lo que
puede tener elementos frozenset dentro de un conjunto.
Además de garantizar la unicidad, los tipos de conjunto implementan las operaciones de conjunto esenciales
como operadores infijos, por lo que, dados dos conjuntos a y b, a | b devuelve su unión, a & b calcula la
intersección y a - b la diferencia. El uso inteligente de las operaciones de configuración puede reducir tanto el
número de líneas como el tiempo de ejecución de los programas de Python, al mismo tiempo que hace que el
código sea más fácil de leer y razonar, eliminando bucles y mucha lógica condicional.
Por ejemplo, imagine que tiene un gran conjunto de direcciones de correo electrónico (el pajar) y un conjunto
más pequeño de direcciones (las agujas) y necesita contar cuántas agujas hay en el pajar. Gracias a establecer
la intersección (el operador & ) puede codificar eso en una línea simple (vea el Ejemplo 3-10).
Ejemplo 3-10. Cuente las apariciones de agujas en un pajar, ambas del conjunto de tipos
Sin el operador de intersección, tendría que escribir el Ejemplo 3-11 para realizar la misma tarea que el Ejemplo
3-10.
Ejemplo 3-11. Cuente las ocurrencias de agujas en un pajar (mismo resultado final que el Ejemplo 3-10)
encontrado
= 0 para n en agujas:
si n en pajar: encontrado
+= 1
Teoría de conjuntos | 79
Machine Translated by Google
El ejemplo 3-10 se ejecuta un poco más rápido que el ejemplo 3-11. Por otro lado, el Ejemplo 3-11
funciona para cualquier objeto iterable agujas y pajar, mientras que el Ejemplo 3-10 requiere que
ambos sean conjuntos. Pero, si no tiene conjuntos a mano, siempre puede construirlos sobre la
marcha, como se muestra en el Ejemplo 3-12.
Ejemplo 3-12. Cuente las ocurrencias de agujas en un pajar; estas líneas funcionan para cualquier tipo
iterable
# otra forma:
encontrado = len(conjunto(agujas).intersección(pajar))
Por supuesto, hay un costo adicional involucrado en la construcción de los juegos del ejemplo 3-12,
pero si las agujas o el pajar ya son un juego, las alternativas del ejemplo 3-12 pueden ser más baratas
que las del ejemplo 3-11.
Cualquiera de los ejemplos anteriores es capaz de buscar 1.000 valores en una pila de heno de
10.000.000 de elementos en poco más de 3 milisegundos, lo que equivale a unos 3 microsegundos
por aguja.
Además de la prueba de membresía extremadamente rápida (gracias a la tabla hash subyacente), los
tipos integrados set y frozenset brindan una rica selección de operaciones para crear nuevos conjuntos
o, en el caso de set, para cambiar los existentes. Discutiremos las operaciones en breve, pero primero
una nota sobre la sintaxis.
establecer literales
La sintaxis de los literales de conjuntos ({1}, {1, 2}, etc.) se ve exactamente como la notación
matemática, con una excepción importante: no hay notación literal para el conjunto vacío, por lo que
debemos recordar escribir set( ).
Sintaxis
peculiaridad No olvides: para crear un conjunto vacío, debes
usar el constructor sin argumentos: set(). Si escribe {}, está
creando un dict vacío; esto no ha cambiado.
>>> s = {1}
>>> tipo(s)
<clase 'conjunto'>
>>> s
{1}
>>> s.pop() 1
>>> s
establecer()
La sintaxis de conjunto literal como {1, 2, 3} es más rápida y más legible que llamar al
constructor (p. ej., set([1, 2, 3])). La última forma es más lenta porque, para evaluarla,
Python tiene que buscar el nombre del conjunto para buscar el constructor, luego construir una lista y finalmente
pasarlo al constructor. Por el contrario, para procesar un literal como {1, 2, 3}, Python ejecuta
un código de bytes BUILD_SET especializado .
Eche un vistazo al código de bytes para las dos operaciones, como la salida de dis.dis (el disasÿ
función de sembrador):
No existe una sintaxis especial para representar literales de conjuntos congelados: deben ser creados por
llamando al constructor. La representación de cadena estándar en Python 3 parece una
Llamada al constructor de frozenset . Tenga en cuenta el resultado en la sesión de la consola:
Hablando de sintaxis, la forma familiar de los listcomps también se adaptó para crear conjuntos.
Establecer comprensiones
Se agregaron comprensiones de conjuntos (setcomps) en Python 2.7, junto con los dictcomps
que vimos en “dict Comprehensions” en la página 66. El ejemplo 3-13 es un ejemplo simple.
Teoría de conjuntos | 81
Machine Translated by Google
Ejemplo 3-13. Cree un conjunto de caracteres Latin-1 que tengan la palabra "SIGN" en sus nombres Unicode
Cree un conjunto de caracteres con códigos del 32 al 255 que tengan la palabra 'SIGN' en sus
nombres.
Dejando a un lado los asuntos de sintaxis, ahora revisemos la rica variedad de operaciones proporcionadas por
conjuntos
Operaciones con
conjuntos La figura 3-2 ofrece una descripción general de los métodos que puede esperar de conjuntos mutables e
inmutables. Muchos de ellos son métodos especiales para la sobrecarga de operadores. La tabla 3-2 muestra los
operadores de conjuntos matemáticos que tienen operadores o métodos correspondientes en Python. Tenga en
cuenta que algunos operadores y métodos realizan cambios en el lugar en el conjunto de destino (por ejemplo, &=,
difference_update, etc.). Tales operaciones no tienen sentido en el mundo ideal de los conjuntos matemáticos y no se
implementan en los conjuntos congelados.
Figura 3-2. Diagrama de clases UML para MutableSet y sus superclases de collections.abc (los nombres en cursiva
son clases abstractas y métodos abstractos; los métodos de operador inverso se omiten por brevedad)
Los operadores infijos de la tabla 3-2 requieren que ambos operandos sean conjuntos,
pero todos los demás métodos toman uno o más argumentos iterables. Para
ejemplo, para producir la unión de cuatro colecciones, a, b, c y d,
puede llamar a.union(b, c, d), donde a debe ser un conjunto, pero b, c,
yd pueden ser iterables de cualquier tipo .
Tabla 3-2. Operaciones matemáticas de conjuntos: estos métodos producen un nuevo conjunto o
fecha el objetivo establecido en su lugar, si es mutable
Matemáticas
Operador Python Método Descripción
símbolo
s.intersection(it, …) Intersección de s y todos los conjuntos construidos a partir de iterables it, etc.
SÿZ Unión de s y z
s|z s.__o__(z)
s.update(it, …) s actualizado con la unión de s y todos los conjuntos creados a partir de iterables
eso, etc
ence (eso)
s.metric_differ actualizado con diferencia simétrica de arena y todos los conjuntos construidos
Teoría de conjuntos | 83
Machine Translated by Google
Mientras escribo esto, hay un informe de error de Python (problema 8743) que
dice: “Los operadores set() (or, and, sub, xor, y su in situ
contrapartes) requieren que el parámetro también sea una instancia de
set().”, con el efecto secundario no deseado de que estos operadores no funcionan
con subclases collections.abc.Set . El error ya está corregido en
trunk para Python 2.7 y 3.4, y debería ser historia para cuando
tu lees esto.
La Tabla 3-3 enumera los predicados establecidos: operadores y métodos que devuelven Verdadero o Falso.
Además de los operadores y métodos derivados de la teoría matemática de conjuntos, los tipos de conjuntos
implementar otros métodos de uso práctico, resumidos en la Tabla 3-4.
conjunto congeladoconjunto
• Agregar elemento e a s
añadir (e)
•
claro() Eliminar todos los elementos de s
••
s.copiar() copia superficial de s
__len__() • • lente)
•
pop() Eliminar y devolver un elemento de s, generando KeyError si s está vacío
Ahora cambiamos de marcha para analizar cómo se implementan los diccionarios y conjuntos con hash
mesas. Después de leer el resto de este capítulo, ya no le sorprenderá la
comportamiento aparentemente impredecible exhibido a veces por dict, set y sus hermanos.
ningún objeto de Python como clave de dictado o elemento de conjunto ? • ¿Por qué el
orden de las teclas de dictado o los elementos establecidos depende del orden de inserción y puede cambiar
durante la vida útil de la estructura?
• ¿Por qué es malo agregar elementos a un dictado o conjunto mientras se itera a través de él?
Para motivar el estudio de las tablas hash, comenzamos mostrando el asombroso rendimiento de dict and set con
una prueba simple que involucra millones de elementos.
experiencia, todos los pitonistas saben que los dictados y conjuntos son rápidos. Lo confirmaremos con un
experimento controlado.
Para ver cómo el tamaño de un dictado, conjunto o lista afecta el rendimiento de la búsqueda usando el operador
in , generé una matriz de 10 millones de flotantes de doble precisión distintos, el "pajar". Luego generé una matriz
de agujas: 1,000 flotadores, con 500 recogidos del pajar y 500 verificados para no estar en él.
Para el punto de referencia de dict , usé dict.fromkeys() para crear un dict llamado haystack con 1,000 flotantes.
Esta fue la configuración para la prueba de dictado . El código real que registré con el módulo timeit es el Ejemplo
3-14 (como el Ejemplo 3-11).
encontrado
= 0 para n en
agujas: si n en
pajar: encontrado += 1
El punto de referencia se repitió otras cuatro veces, aumentando cada vez diez veces el tamaño del pajar, para
alcanzar un tamaño de 10.000.000 en la última prueba. El resultado de la prueba de rendimiento de dict se
encuentra en la Tabla 3-5.
Tabla 3-5. Tiempo total para usar en el operador para buscar 1,000 agujas en dictados de pajar
de cinco tamaños en una computadora portátil Core i7 con Python 3.4.0 (las pruebas cronometraron el ciclo en
Ejemplo 3-14)
En términos concretos, para comprobar la presencia de 1000 claves de punto flotante en un diccionario
con 1000 elementos, el tiempo de procesamiento en mi computadora portátil fue de 0,000202 s, y la misma búsqueda
en un dict con 10,000,000 artículos tomó 0.000337s. En otras palabras, el tiempo por búsqueda en
el pajar con 10 millones de artículos fue de 0,337 µs por cada aguja, sí, eso es aproximadamente uno
tercio de microsegundo por aguja.
Para comparar, repetí el benchmark, con los mismos pajares de tamaño creciente, pero
almacenar el pajar como un conjunto o como una lista. Para las pruebas establecidas , además de cronometrar el
bucle en el Ejemplo 3-14, también cronometré el one-liner en el Ejemplo 3-15, que produce el
Mismo resultado: contar el número de elementos de las agujas que también están en el pajar.
Ejemplo 3-15. Use la intersección establecida para contar las agujas que ocurren en el pajar
La Tabla 3-6 muestra las pruebas una al lado de la otra. Los mejores tiempos están en la columna "set & time",
que muestra los resultados para el conjunto y el operador utilizando el código del Ejemplo 3-15. los
los peores tiempos están, como se esperaba, en la columna "list time", porque no hay una tabla hash
para admitir búsquedas con el operador in en una lista, por lo que se debe realizar un escaneo completo, lo que resulta
en tiempos que crecen linealmente con el tamaño del pajar.
Tabla 3-6. Tiempo total de uso del operador para buscar 1000 claves en montones de heno de 5
tamaños, almacenados como dictados, conjuntos y listas en una computadora portátil Core i7 que ejecuta Python 3.4.0 (pruebas
cronometró el bucle en el Ejemplo 3-14 excepto el set&, que usa el Ejemplo 3-15)
largo del pajar Factor dict time Factor set time Factor set& time Lista de factores tiempo Factor
10,000 10x 0,000140s 0,69x 0,000147s 1,03x 0,000092s 1,06x 0,086586s 8,20x
100,000 100x 0.000228s 1.13x 0.000241s 1.69x 0.000163s 1.87x 0.871560s 82.57x
1,000,000 1.000x 0,000290s 1,44x 0,000332s 2,32x 0,000250s 2,87x 9,189616s 870,56x
10,000,000 10,000x 0.000337s 1.67x 0.000387s 2.71x 0.000314s 3.61x 97.948056s 9,278.90x
Si su programa realiza algún tipo de E/S, el tiempo de búsqueda de claves en dictados o conjuntos es
insignificante, independientemente del tamaño del dictado o conjunto (siempre que quepa en la RAM).
Consulte el código utilizado para generar la Tabla 3-6 y la discusión que lo acompaña en el Apéndice A, Ejemplo A-1.
Ahora que tenemos evidencia concreta de la velocidad de dictados y conjuntos, exploremos cómo se logra.
La discusión de las funciones internas de la tabla hash explica, por ejemplo, por qué el orden de las claves
es aparentemente aleatorio e inestable.
Una tabla hash es una matriz dispersa (es decir, una matriz que siempre tiene celdas vacías). En los textos
de estructura de datos estándar, las celdas de una tabla hash a menudo se denominan "cubos". En una tabla
hash de dictado, hay un depósito para cada elemento y contiene dos campos: una referencia a la clave y una
referencia al valor del elemento. Debido a que todos los cubos tienen el mismo tamaño, el acceso a un cubo
individual se realiza por compensación.
Python intenta mantener vacío al menos 1/3 de los cubos; si la tabla hash se llena demasiado, se copia en
una nueva ubicación con espacio para más cubos.
Para colocar un elemento en una tabla hash, el primer paso es calcular el valor hash de la clave del elemento,
lo que se hace con la función integrada hash() , que se explica a continuación.
Hashes e igualdad La
función integrada hash() funciona directamente con tipos integrados y recurre a llamar a __hash__ para tipos
definidos por el usuario. Si dos objetos se comparan iguales, sus valores hash también deben ser iguales; de
lo contrario, el algoritmo de la tabla hash no funciona. Por ejemplo, dado que 1 == 1.0 es verdadero, hash(1)
== hash(1.0) también debe ser verdadero, aunque la representación interna de un int y un float son muy
diferentes.6
5. El código fuente del módulo dictobject.c de CPython es rico en comentarios. Véase también la referencia de la
Hermoso libro de códigos en “Lecturas adicionales” en la página 94.
6. Debido a que acabamos de mencionar int, aquí hay un detalle de implementación de CPython: el valor hash de un int que cabe en una
palabra de máquina es el valor del int mismo.
Además, para ser efectivos como índices de tablas hash, los valores hash deben dispersarse alrededor del índice.
espacio tanto como sea posible. Esto significa que, idealmente, los objetos que son similares pero no iguales
debe tener valores hash que difieran ampliamente. El ejemplo 3-16 es la salida de un script para
comparar los patrones de bits de los valores hash. Observe cómo los valores hash de 1 y 1.0 son iguales,
pero los de 1.0001, 1.0002 y 1.0003 son muy diferentes.
Ejemplo 3-16. Comparación de patrones de bits hash de 1, 1.0001, 1.0002 y 1.0003 en un 32-
compilación de bits de Python (los bits que son diferentes en los hashes arriba y abajo están resaltados).
ted con! y la columna de la derecha muestra el número de bits que difieren)
El código para producir el Ejemplo 3-16 se encuentra en el Apéndice A. La mayor parte trata sobre el formato
la salida, pero se enumera como Ejemplo A-3 para completar.
Con esta comprensión básica de los hashes de objetos, estamos listos para sumergirnos en el algoritmo.
que hace que las tablas hash funcionen.
Sin embargo, si search_key y found_key no coinciden, se trata de una colisión hash. Esto sucede porque
una función hash asigna objetos arbitrarios a una pequeña cantidad de bits y, además, la tabla hash está
indexada con un subconjunto de esos bits. Para resolver la colisión, el algoritmo toma diferentes bits en
el hash, los masajea de una manera particular y usa el resultado como compensación para buscar un
depósito diferente.7 Si está vacío, se genera KeyError ; si no, las claves coinciden y se devuelve el valor
del elemento, o se repite el proceso de resolución de colisiones. Consulte la Figura 3-3 para ver un
diagrama de este algoritmo.
Figura 3-3. Diagrama de flujo para recuperar un elemento de un dictado; dada una clave, este
procedimiento devuelve un valor o genera KeyError
El proceso para insertar o actualizar un elemento es el mismo, excepto que cuando se encuentra un
depósito vacío, el nuevo elemento se coloca allí y cuando se encuentra un depósito con una clave
coincidente, el valor de ese depósito se sobrescribe con el nuevo valor. .
Además, al insertar elementos, Python puede determinar que la tabla hash está demasiado llena y
reconstruirla en una nueva ubicación con más espacio. A medida que crece la tabla hash, también lo
hace la cantidad de bits hash utilizados como compensaciones de depósito, y esto mantiene baja la tasa
de colisiones.
7. La función de C que baraja los bits hash en caso de colisión tiene un nombre curioso: perturb. Para obtener todos los
detalles, consulte dictobject.c en el código fuente de CPython.
Esta implementación puede parecer mucho trabajo, pero incluso con millones de elementos en un dictado,
muchas búsquedas ocurren sin colisiones, y el número promedio de colisiones por búsqueda es entre uno y
dos. Bajo un uso normal, incluso las claves más desafortunadas se pueden encontrar después de que se
resuelvan algunas colisiones.
Conociendo los aspectos internos de la implementación de dict , podemos explicar las fortalezas y limitaciones
de esta estructura de datos y todas las demás derivadas de ella en Python. Ahora estamos listos para
considerar por qué los dictados de Python se comportan como lo hacen.
siguientes subsecciones, analizaremos las limitaciones y los beneficios que la implementación de la tabla hash
subyacente aporta al uso de dict .
1. Admite la función hash() a través de un método hash() que siempre devuelve el mismo valor durante la
vida útil del objeto.
Los tipos definidos por el usuario se pueden modificar de forma predeterminada porque su valor de hash es su
id() y no se comparan entre sí.
memoria significativa Debido a que un dictado usa una tabla hash internamente, y las tablas hash deben ser
escasas para funcionar, no son eficientes en cuanto al espacio. Por ejemplo, si está manejando una gran
cantidad de registros, tiene sentido almacenarlos en una lista de tuplas o tuplas con nombre en lugar de usar
una lista de diccionarios en estilo JSON, con un dict por registro. Reemplazar los dictados con tuplas reduce el
uso de la memoria de dos maneras: eliminando la sobrecarga de una tabla hash por registro y no almacenando
los nombres de los campos nuevamente con cada registro.
Para los tipos definidos por el usuario, el atributo de clase __slots__ cambia el almacenamiento de
atributos de instancia de un dict a una tupla en cada instancia. Esto se tratará en “Ahorro de espacio con
el atributo de clase __slots__” en la página 264 (Capítulo 9).
Tenga en cuenta que estamos hablando de optimizaciones de espacio. Si está tratando con unos pocos
millones de objetos y su máquina tiene gigabytes de RAM, debe posponer tales optimizaciones hasta que
realmente estén garantizadas. La optimización es el altar donde se sacrifica la mantenibilidad.
La búsqueda de claves
es muy rápida. La implementación de dict es un ejemplo de intercambio de espacio por tiempo: los
diccionarios tienen una sobrecarga de memoria significativa, pero brindan un acceso rápido
independientemente del tamaño del diccionario, siempre que quepa en la memoria. Como muestra la
Tabla 3-5 , cuando aumentamos el tamaño de un dictado de 1000 a 10ÿ000ÿ000 elementos, el tiempo de
búsqueda aumentó en un factor de 2,8, de 0,000163 s a 0,000456 s. La última cifra significa que
podríamos buscar más de 2 millones de claves por segundo en un dict con 10 millones de elementos.
de inserción Cuando ocurre una colisión hash, la segunda clave termina en una posición que normalmente
no ocuparía si se hubiera insertado primero. Entonces, un dict construido como dict([(key1, value1), (key2,
value2)]) se compara igual a dict([(key2, value2), (key1, value1)]), pero su orden de claves puede no ser
el lo mismo si los hash de key1 y key2 chocan.
El ejemplo 3-17 demuestra el efecto de cargar tres dictados con los mismos datos, solo que en diferente
orden. Los diccionarios resultantes se comparan todos iguales, incluso si su orden no es el mismo.
Ejemplo 3-17. dialcodes.py llena tres diccionarios con los mismos datos ordenados de diferentes maneras
d1 = dict(DIAL_CODES)
print('d1:', d1.keys()) d2 =
dict(ordenado(DIAL_CODES))
print('d2:', d2.keys()) d3 =
dict(ordenado(DIAL_CODES, key=lambda x:x[1]))
print('d3:', d3.keys()) afirmar d1 == d2 y d2 == d3
d1: construido a partir de las tuplas en orden descendente de población del país.
d2: lleno de tuplas ordenadas por código de marcación. d3: cargado con tuplas
Los diccionarios se comparan igual, porque contienen los mismos pares clave:valor .
d1: teclas_dict([880, 1, 86, 55, 7, 234, 91, 92, 62, 81]) d2:
teclas_dict([880, 1, 91, 86, 81, 55, 234, 7, 92, 62 ]) d3:
teclas_dict([880, 81, 1, 86, 55, 7, 234, 91, 92, 62])
claves existentes Cada vez que agrega un nuevo elemento a un dictado, el intérprete de Python puede decidir
que la tabla hash de ese diccionario necesita crecer. Esto implica crear una nueva tabla hash más grande y
agregar todos los elementos actuales a la nueva tabla. Durante este proceso, pueden ocurrir colisiones hash
nuevas (pero diferentes), con el resultado de que es probable que las claves se ordenen de manera diferente en
la nueva tabla hash. Todo esto depende de la implementación, por lo que no puede predecir de manera confiable
cuándo sucederá. Si está iterando sobre las claves del diccionario y cambiándolas al mismo tiempo, es posible
que su ciclo no explore todos los elementos como se esperaba, ni siquiera los elementos que ya estaban en el
diccionario antes de agregarlos.
Esta es la razón por la que modificar el contenido de un dict mientras se itera es una mala idea. Si necesita
escanear y agregar elementos a un diccionario, hágalo en dos pasos: lea el dictado de principio a fin y recopile las
adiciones necesarias en un segundo dictado. Luego actualice el primero con él.
Ahora podemos aplicar lo que sabemos sobre las tablas hash a los conjuntos.
tipos set y frozenset también se implementan con una tabla hash, excepto que cada depósito contiene solo una
referencia al elemento (como si fuera una clave en un dictado, pero sin un valor que lo acompañe) . De hecho,
antes de agregar set al idioma, a menudo usábamos diccionarios con valores ficticios solo para realizar pruebas
rápidas de membresía en las claves.
Todo lo dicho en “Consecuencias prácticas de cómo funciona dict” en la página 90 sobre cómo la tabla hash
subyacente determina el comportamiento de un dict se aplica a un conjunto. Sin repetir el apartado anterior,
podemos resumirlo por conjuntos en unas pocas palabras:
Dos métodos poderosos disponibles en la mayoría de las asignaciones son setdefault y update. El método
setdefault se usa para actualizar elementos que contienen valores mutables, por ejemplo, en un dict de valores
de lista , para evitar búsquedas redundantes de la misma clave. El método de actualización permite la inserción
masiva o la sobrescritura de elementos de cualquier otra asignación, de iterables que proporcionan pares
(clave, valor) y de argumentos de palabras clave. Los constructores de mapeo también usan la actualización
internamente, lo que permite que las instancias se inicialicen a partir de mapeos, iterables o argumentos de
palabras clave.
Un gancho inteligente en la API de mapeo es el método __missing__ , que le permite personalizar lo que
sucede cuando no se encuentra una clave.
El módulo collections.abc proporciona las clases base abstractas Mapping y MutableMapping para referencia y
verificación de tipos. El poco conocido MappingProxyType del módulo de tipos crea asignaciones inmutables.
También hay ABC para Set y Mutable Set.
La implementación de la tabla hash subyacente a dict and set es extremadamente rápida. Comprender
su lógica explica por qué los elementos aparentemente están desordenados e incluso pueden
reordenarse a nuestras espaldas. Hay un precio a pagar por toda esta velocidad, y el precio está en la memoria.
Otras lecturas
En la biblioteca estándar de Python, 8.3. colecciones: los tipos de datos de contenedor incluyen
ejemplos y recetas prácticas con varios tipos de mapeo. El código fuente de Python para el módulo Lib/
collections/ init.py es una gran referencia para cualquiera que quiera crear un nuevo tipo de mapeo o
asimilar la lógica de los existentes.
Escrito por AM Kuchling, un colaborador principal de Python y autor de muchas páginas de los
documentos y procedimientos oficiales de Python, el Capítulo 18, “Implementación del diccionario de
Python: Ser todo para todas las personas, en el libro Beautiful Code (O'Reilly) incluye una explicación
detallada del funcionamiento interno del dict de Python. Además, hay muchos comentarios en el código
fuente del módulo dictobject.cCPython . La presentación de Brandon Craig Rhodes, The Mighty
Dictionary , es excelente y muestra cómo funcionan las tablas hash usando muchas diapositivas con...
¡tablas!
La justificación para agregar conjuntos al lenguaje se documenta en PEP 218: Agregar un tipo de objeto
de conjunto incorporado. Cuando se aprobó el PEP 218, no se adoptó ninguna sintaxis literal especial
para conjuntos. Los literales establecidos se crearon para Python 3 y se adaptaron a Python 2.7, junto
con las comprensiones dict y set . PEP 274 — Dict Comprehensions es el certificado de nacimiento de
los dictcomps. No pude encontrar un PEP para setcomps; aparentemente fueron adoptados porque se
llevan bien con sus hermanos, una muy buena razón.
Plataforma improvisada
Mi amigo Geraldo Cohen comentó una vez que Python es "simple y correcto".
El tipo dict es un ejemplo de simplicidad y corrección. Está altamente optimizado para hacer una
cosa bien: recuperar claves arbitrarias. Es lo suficientemente rápido y robusto como para usarse
en todo el intérprete de Python. Si necesita un pedido predecible, use OrderedDict. Ese no es un
requisito en la mayoría de los usos de las asignaciones, por lo que tiene sentido mantener la
implementación central simple y ofrecer variaciones en la biblioteca estándar.
Contrasta con PHP, donde las matrices se describen así en el manual oficial de PHP:
Una matriz en PHP es en realidad un mapa ordenado. Un mapa es un tipo que asocia valores a
claves. Este tipo está optimizado para varios usos diferentes; se puede tratar como una matriz, una lista
(vector), tabla hash (una implementación de un mapa), diccionario, colección, pila, cola y
probablemente más.
A partir de esa descripción, no sé cuál es el costo real de usar el híbrido list/Ordered Dict de PHP.
El objetivo de este capítulo y del anterior de este libro era mostrar los tipos de colección de Python
optimizados para usos particulares. Señalé que más allá de la lista y el dictado confiables , existen
alternativas especializadas para diferentes casos de uso.
Antes de encontrar Python, había hecho programación web usando Perl, PHP y JavaScript.
Realmente disfruté tener una sintaxis literal para las asignaciones en estos lenguajes, y la extraño
mucho cada vez que tengo que usar Java o C. Una buena sintaxis literal para las asignaciones facilita
la configuración, las implementaciones basadas en tablas y el almacenamiento de datos para
prototipos y pruebas. La falta de este empujó a la comunidad Java a adoptar el XML detallado y
excesivamente complejo como formato de datos.
JSON se propuso como "La alternativa sin grasa a XML" y se convirtió en un gran éxito, reemplazando
a XML en muchos contextos. Una sintaxis concisa para listas y diccionarios constituye un excelente
formato de intercambio de datos.
PHP y Ruby imitaron la sintaxis hash de Perl, usando => para vincular claves a valores.
JavaScript siguió el ejemplo de Python y utiliza :. Por supuesto, JSON proviene de JavaScript, pero
también resulta ser un subconjunto casi exacto de la sintaxis de Python. JSON es compatible con
Python excepto por la ortografía de los valores verdadero, falso y nulo. La sintaxis que todo el mundo
usa ahora para intercambiar datos es la sintaxis de dictado y lista de Python .
Sencillo y correcto.
Lectura adicional | 95
Machine Translated by Google
Machine Translated by Google
CAPÍTULO 4
Python 3 introdujo una clara distinción entre cadenas de texto humano y secuencias de bytes sin formato. La
conversión implícita de secuencias de bytes a texto Unicode es cosa del pasado.
Este capítulo trata sobre las cadenas Unicode, las secuencias binarias y las codificaciones utilizadas para
realizar conversiones entre ellas.
Dependiendo de su contexto de programación Python, una comprensión más profunda de Unicode puede o
no ser de vital importancia para usted. Al final, la mayoría de los temas cubiertos en este capítulo no afectan
a los programadores que trabajan solo con texto ASCII. Pero incluso si ese es su caso, no hay forma de
escapar de la división entre str y byte . Como beneficio adicional, encontrará que los tipos de secuencias
binarias especializadas brindan características que el tipo str de Python 2 "para todo uso" no tiene.
1. Diapositiva 12 de PyCon 2014 charla "Codificación de caracteres y Unicode en Python" (diapositivas, video).
97
Machine Translated by Google
fuerza bruta • Ordenación adecuada de texto Unicode con la configuración regional y la biblioteca PyUCA •
Problemas de carácter
El concepto de "cadena" es bastante simple: una cadena es una secuencia de caracteres. El problema radica en la
definición de “carácter”.
En 2015, la mejor definición de "carácter" que tenemos es un carácter Unicode. En consecuencia, los elementos
que obtiene de una cadena de Python 3 son caracteres Unicode, al igual que los elementos de un objeto Unicode
en Python 2, y no los bytes sin procesar que obtiene de una cadena de Python 2.
El estándar Unicode separa explícitamente la identidad de los caracteres de las representaciones de bytes
específicas:
• La identidad de un carácter, su punto de código, es un número del 0 al 1.114.111 (base 10), que se muestra en
el estándar Unicode como 4 a 6 dígitos hexadecimales con un prefijo “U+”. Por ejemplo, el punto de código
para la letra A es U+0041, el símbolo del euro es U+20AC y el símbolo musical clave G se asigna al punto de
código U+1D11E. Alrededor del 10 % de los puntos de código válidos tienen caracteres asignados en Unicode
6.3, el estándar utilizado en Python 3.4. • Los bytes reales que representan un carácter dependen de la
codificación en uso. Una codificación es un algoritmo que convierte puntos de código en secuencias de bytes
y viceversa.
El punto de código para A (U+0041) se codifica como el byte único \x41 en la codificación UTF-8 o como los
bytes \x41\x00 en la codificación UTF-16LE. Como otro ejemplo, el símbolo del euro (U+20AC) se convierte
en tres bytes en UTF-8—\xe2\x82\xac—pero en UTF-16LE se codifica en dos bytes: \xac\x20.
>>> s = 'café'
>>> len(s) # 4
>>> b =
s.encode('utf8') # >>> b
b'caf\xc3\xa9' # >>> len(b) # 5
cinco bytes (el punto de código para “é” está codificado como dos bytes en UTF-8).
Aunque Python 3 str es más o menos el tipo Unicode de Python 2 con un nuevo nombre, los bytes de
Python 3 no son simplemente el antiguo str renombrado, y también existe el tipo bytearray estrechamente
relacionado . Por lo tanto, vale la pena echar un vistazo a los tipos de secuencias binarias antes de avanzar
a los problemas de codificación/descodificación.
Esenciales de byte
Los nuevos tipos de secuencias binarias son diferentes a Python 2 str en muchos aspectos. Lo primero
que debe saber es que hay dos tipos integrados básicos para secuencias binarias: el tipo de bytes
inmutables introducido en Python 3 y el bytearray mutable, agregado en Python 2.6. (Python 2.6 también
introdujo bytes, pero es solo un alias para el tipo str y no se comporta como el tipo de bytes de Python 3).
Cada elemento en bytes o bytearray es un número entero de 0 a 255, y no una cadena de un carácter
como en Python 2 str. Sin embargo, una porción de una secuencia binaria siempre produce una secuencia
binaria del mismo tipo, incluidas las porciones de longitud 1. Consulte el Ejemplo 4-2.
Ejemplo 4-2. Una secuencia de cinco bytes como bytes y como bytearray
Esenciales de bytes | 99
Machine Translated by Google
bytearray(b'caf\xc3\xa9') >>>
cafe_arr[-1:] bytearray(b'\xa9')
los bytes se pueden construir a partir de una cadena, dada una codificación.
Las porciones de bytes también son bytes, incluso porciones de un solo byte.
No existe una sintaxis literal para bytearray: se muestran como bytearray() con un bytes literal como
argumento.
Aunque las secuencias binarias son realmente secuencias de números enteros, su notación literal refleja el
hecho de que el texto ASCII suele estar incrustado en ellas. Por lo tanto, se utilizan tres pantallas diferentes,
dependiendo del valor de cada byte:
• Para bytes en el rango ASCII imprimible, desde el espacio hasta ~, el propio carácter ASCII
se usa
• Para los bytes correspondientes a tabulación, nueva línea, retorno de carro y \, se utilizan las secuencias de escape \t, \n, \r
y \\ .
• Para cualquier otro valor de byte, se utiliza una secuencia de escape hexadecimal (p. ej., \x00 es el
byte nulo).
Es por eso que en el Ejemplo 4-2 ves b'caf\xc3\xa9': los primeros tres bytes b'caf' están en el rango ASCII
imprimible, los dos últimos no lo están.
Tanto bytes como bytearray admiten todos los métodos str , excepto los que dan formato (format, format_map) y algunos otros
que dependen de datos Unicode, incluidos case fold, isdecimal, isidentifier, isnumeric, isprintable y encode. Esto significa que
puede usar métodos de cadena familiares como extremos con, reemplazar, quitar, traducir, superior y docenas de otros con
Además, las funciones de expresiones regulares en el módulo re también funcionan en secuencias binarias, si
la expresión regular se compila a partir de una secuencia binaria en lugar de una str. El operador % no funciona
con secuencias binarias en Python 3.0 a 3.4, pero debería ser compatible.
portado en la versión 3.5 de acuerdo con PEP 461 — Agregar % de formato a bytes y byteÿ
formación.
Las secuencias binarias tienen un método de clase que str no tiene, llamado fromhex, que crea una secuencia
binaria analizando pares de dígitos hexadecimales opcionalmente separados por espacios:
Las otras formas de construir instancias de bytes o bytearray son llamando a sus constructores con:
a 255. • Un solo entero, para crear una secuencia binaria de ese tamaño inicializada con bytes nulos.
(Esta firma quedará obsoleta en Python 3.5 y se eliminará en Python 3.6. Consulte PEP 467: mejoras
menores de API para secuencias binarias). • Un objeto que implementa el protocolo de búfer (p. ej., bytes,
bytearray, vista de memoria, array.array); esto copia los bytes del objeto fuente a la secuencia binaria recién
creada.
Construir una secuencia binaria a partir de un objeto similar a un búfer es una operación de bajo nivel que
puede implicar la conversión de tipos. Vea una demostración en el Ejemplo 4-3.
Ejemplo 4-3. Inicializar bytes de los datos sin procesar de una matriz
b'\xfe\xff\xff\xff\x00\x00\x01\x00\x02\x00'
El código de tipo 'h' crea una matriz de enteros cortos (16 bits). octetos
Estos son los 10 bytes que representan los cinco enteros cortos.
La creación de un objeto bytes o bytearray desde cualquier fuente similar a un búfer siempre copiará los bytes.
Por el contrario, los objetos de vista de memoria le permiten compartir memoria entre estructuras de datos
binarios. Para extraer información estructurada de secuencias binarias, el módulo struct es invaluable. Lo
veremos funcionar junto con bytes y memoryview en la siguiente sección.
módulo struct proporciona funciones para analizar bytes empaquetados en una tupla de campos de
diferentes tipos y realizar la conversión opuesta, de una tupla a bytes empaquetados. struct se usa con
objetos bytes, bytearray y memoryview .
Como hemos visto en "Vistas de memoria" en la página 51, la clase de vista de memoria no le permite
crear ni almacenar secuencias de bytes, pero proporciona acceso de memoria compartida a segmentos de
datos de otras secuencias binarias, matrices empaquetadas y búferes como Python Imaging. Imágenes de
biblioteca (PIL),2 sin copiar los bytes.
El ejemplo 4-4 muestra el uso de memoryview y struct juntos para extraer el ancho y el alto de una imagen
GIF.
Ejemplo 4-4. Uso de memoryview y struct para inspeccionar un encabezado de imagen GIF
formato de estructura : < little-endian; 3s3s dos secuencias de 3 bytes; HH dos enteros de 16 bits.
la memoria... ...luego otra vista de memoria cortando la primera; aquí no se copian bytes.
Descomprima la vista de memoria en una tupla de: tipo, versión, ancho y alto.
Elimine las referencias para liberar la memoria asociada con las instancias de vista de memoria .
Tenga en cuenta que cortar una vista de memoria devuelve una nueva vista de memoria, sin copiar bytes
(Leonardo Rochael, uno de los revisores técnicos, señaló que ocurriría incluso menos copia de bytes si
usara el módulo mmap para abrir la imagen como un archivo asignado a la memoria .
No cubriré mmap en este libro, pero si lee y cambia archivos binarios con frecuencia, aprender más sobre mmap
(la compatibilidad con archivos mapeados en memoria será muy fructífero).
No profundizaremos en memoryview o el módulo struct en este libro, pero si trabaja con datos binarios, encontrará
que vale la pena estudiar sus documentos: Tipos integrados »
Vistas de memoria y estructura: interpreta los bytes como datos binarios empaquetados.
Después de esta breve exploración de los tipos de secuencias binarias en Python, veamos cómo se convierten a/
desde cadenas.
Codificadores/Decodificadores básicos
La distribución de Python incluye más de 100 códecs (codificador/descodificador) para la conversión de texto a
byte y viceversa. Cada códec tiene un nombre, como 'utf_8', y a menudo alias, como 'utf8', 'utf-8' y 'U8', que
puede usar como argumento de codificación en funciones como open(), str.encode (), bytes.decode(), etc. El
ejemplo 4-5 muestra el mismo texto codificado como tres secuencias de bytes diferentes.
Ejemplo 4-5. La cadena “El Niño” codificada con tres códecs que producen secuencias de bytes muy diferentes
La figura 4-1 muestra una variedad de códecs que generan bytes a partir de caracteres como la letra "A" hasta el
símbolo musical G-clef. Tenga en cuenta que las últimas tres codificaciones son codificaciones multibyte de
longitud variable.
Figura 4-1. Doce caracteres, sus puntos de código y su representación de bytes (en hexadecimal) en siete
codificaciones diferentes (los asteriscos indican que el carácter no se puede representar en esa
codificación)
Todos esos asteriscos en la Figura 4-1 dejan en claro que algunas codificaciones, como ASCII e incluso
la GB2312 multibyte, no pueden representar todos los caracteres Unicode. Sin embargo, las codificaciones
UTF están diseñadas para manejar todos los puntos de código Unicode.
Las codificaciones que se muestran en la Figura 4-1 se eligieron como una muestra
representativa: latin1 , también conocido como iso8859_1 Importante porque es la base para
otras codificaciones, como cp1252 y Unicode (observe cómo los valores de bytes latin1 aparecen en
los bytes cp1252 e incluso en los bytes puntos de código).
cp1252
Un superconjunto latin1 de Microsoft, que agrega símbolos útiles como comillas y el € (euro); algunas
aplicaciones de Windows lo llaman "ANSI", pero nunca fue un estándar ANSI real.
cp437
El conjunto de caracteres original de IBM PC, con caracteres de dibujo de cuadro. Incompatible con
latin1 , que apareció más tarde.
gb2312
Estándar heredado para codificar los ideogramas chinos simplificados utilizados en China continental;
una de varias codificaciones multibyte ampliamente implementadas para idiomas asiáticos.
utf-8 La
codificación de 8 bits más común en la Web, con diferencia;3 compatible con versiones anteriores de ASCII
(el texto ASCII puro es UTF-8 válido).
utf-16le
Una forma del esquema de codificación UTF-16 de 16 bits; todas las codificaciones UTF-16 admiten
puntos de código más allá de U+FFFF a través de secuencias de escape denominadas "pares sustitutos".
Una vez completada esta descripción general de las codificaciones comunes, pasamos a manejar los problemas
en las operaciones de codificación y decodificación.
Aunque existe una excepción UnicodeError genérica , el error informado casi siempre es más específico: un
UnicodeEncodeError (al convertir str en secuencias binarias) o un UnicodeDecodeError (al leer secuencias binarias
en str). La carga de módulos de Python también puede generar un SyntaxError cuando la codificación de origen
es inesperada.
Le mostraremos cómo manejar todos estos errores en las siguientes secciones.
los códecs que no son UTF manejan solo un pequeño subconjunto de los caracteres Unicode. Al convertir texto a
bytes, si un carácter no está definido en la codificación de destino, se generará UnicodeEn codeError , a menos
que se proporcione un manejo especial al pasar un argumento de errores al método o función de codificación. El
comportamiento de los controladores de errores se muestra en el Ejemplo 4-6.
3. A partir de septiembre de 2014, W3Techs: Uso de codificaciones de caracteres para sitios web afirma que el 81,4 % de los sitios usan
UTF-8, mientras que Construido con: Estadísticas de uso de codificación estima un 79,4 %.
'cp437' no puede codificar la 'ã' ("a" con tilde). El controlador de errores predeterminado,
"estricto", genera UnicodeEncodeError.
Al codificar, error='replace' sustituye los caracteres no codificables por '?'; se pierden datos,
pero los usuarios sabrán que algo anda mal. 'xmlcharrefreplace' reemplaza los caracteres no
los bytes contienen un carácter ASCII válido, y no todas las secuencias de bytes son UTF-8 o
UTF-16 válidas; por lo tanto, cuando asume una de estas codificaciones al convertir una secuencia
binaria en texto, obtendrá un UnicodeDecodeError si se encuentran bytes inesperados.
Por otro lado, muchas codificaciones heredadas de 8 bits como 'cp1252', 'iso8859_1' y 'koi8_r'
pueden decodificar cualquier flujo de bytes, incluido el ruido aleatorio, sin generar errores. Por lo
tanto, si su programa asume la codificación incorrecta de 8 bits, decodificará silenciosamente la
basura.
El ejemplo 4-7 ilustra cómo el uso del códec incorrecto puede producir gremlins o un Unico
decodeError.
'Montreal'
Estos bytes son los caracteres de “Montréal” codificados como latin1; '\xe9' es el byte para
“é”.
ISO-8859-7 está diseñado para griego, por lo que el byte '\xe9' se malinterpreta y no se emite
ningún error.
Usando el manejo de errores de 'reemplazo' , \xe9 se reemplaza por "ÿ" (punto de código U
+FFFD), el CARÁCTER DE REEMPLAZO oficial de Unicode destinado a representar
caracteres desconocidos.
de origen predeterminada para Python 3, al igual que ASCII era la codificación predeterminada para
Python 2 (a partir de 2.5). Si carga un módulo .py que contiene datos que no son UTF-8 y sin declaración
de codificación, recibe un mensaje como este:
SyntaxError: Código no UTF-8 que comienza con '\xe1' en el archivo ola.py en la línea 1, pero no
se declara codificación; ver http://python.org/dev/peps/pep-0263/ para más detalles
Debido a que UTF-8 se implementa ampliamente en los sistemas GNU/Linux y OSX, un escenario
probable es abrir un archivo .py creado en Windows con cp1252. Tenga en cuenta que este error ocurre
incluso en Python para Windows, porque la codificación predeterminada para Python 3 es UTF-8 en
todas las plataformas.
Para solucionar este problema, agregue un comentario de codificación mágica en la parte superior del archivo, como se muestra
en el Ejemplo 4-8.
# codificación: cp1252
print('¡Olá, Mundo!')
A algunas personas no les gusta la idea. El argumento más común para apegarse a los identificadores ASCII es
facilitar que todos lean y editen el código. Ese argumento pierde el punto: desea que su código fuente sea legible
y editable por su público objetivo, y es posible que no sea "todo el mundo". Si el código pertenece a una
corporación multinacional o es de código abierto y desea colaboradores de todo el mundo, los identificadores
deben estar en inglés y luego todo lo que necesita es ASCII.
Pero si es profesor en Brasil, a sus alumnos les resultará más fácil leer el código que utiliza nombres de variables
y funciones en portugués, correctamente escritos. Y no tendrán dificultad para teclear las cedillas y las vocales
acentuadas en sus teclados localizados.
Ahora que Python puede analizar nombres Unicode y UTF-8 es la codificación de origen
predeterminada, no veo el sentido de codificar identificadores en portugués sin acentos, como
solíamos hacer en Python 2 por necesidad, a menos que necesite el código para ejecutarse en
Python. 2 también. Si los nombres están en portugués, omitir los acentos no hará que el código
sea más legible para nadie.
Este es mi punto de vista como brasileño de habla portuguesa, pero creo que se aplica a través de
fronteras y culturas: elija el idioma humano que haga que el código sea más fácil de leer para el
equipo, luego use los caracteres necesarios para una ortografía correcta.
Suponga que tiene un archivo de texto, ya sea código fuente o poesía, pero no conoce su codificación.
¿Cómo se detecta la codificación real? La siguiente sección responde eso con una recomendación de
biblioteca.
encuentra la codificación de una secuencia de bytes? Respuesta corta: no puedes. Debes ser informado.
Algunos protocolos de comunicación y formatos de archivo, como HTTP y XML, contienen encabezados
que nos dicen explícitamente cómo se codifica el contenido. Puede estar seguro de que algunos flujos
de bytes no son ASCII porque contienen valores de bytes superiores a 127, y la forma en que se
construyen UTF-8 y UTF-16 también limita las posibles secuencias de bytes. Pero incluso entonces,
nunca puede estar 100% seguro de que un archivo binario es ASCII o UTF-8 solo porque ciertos patrones
de bits no están allí.
Sin embargo, teniendo en cuenta que los lenguajes humanos también tienen sus reglas y restricciones,
una vez que asume que un flujo de bytes es texto sin formato humano , es posible detectar su codificación
mediante heurística y estadísticas. Por ejemplo, si los bytes b'\x00' son comunes, es probable que sea
una codificación de 16 o 32 bits, y no un esquema de 8 bits, porque los caracteres nulos en texto sin
formato son errores; cuando la secuencia de bytes b'\x20\x00' aparece con frecuencia, es probable que
sea el carácter de espacio (U+0020) en una codificación UTF-16LE, en lugar del oscuro carácter U +2000
EN QUAD , sea lo que sea.
Así es como funciona el paquete Chardet: el detector universal de codificación de caracteres para
identificar una de las 30 codificaciones admitidas. Chardet es una biblioteca de Python que puede usar
en sus programas, pero también incluye una utilidad de línea de comandos, chardetect. Esto es lo que
informa sobre el archivo fuente de este capítulo:
Aunque las secuencias binarias de texto codificado no suelen llevar indicaciones explícitas de su
codificación, los formatos UTF pueden anteponer una marca de orden de bytes al contenido textual. Eso
se explica a continuación.
Los bytes son b'\xff\xfe'. Esa es una BOM (marca de orden de bytes) que indica el orden de bytes "little
endian" de la CPU Intel donde se realizó la codificación.
En una máquina little-endian, para cada punto de código, el byte menos significativo viene primero: la
letra 'E', punto de código U+0045 (decimal 69), se codifica en las compensaciones de bytes 2 y 3 como
69 y 0:
>>> lista(u16)
[255, 254, 69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111, 0]
Para evitar confusiones, la codificación UTF-16 antepone el texto a codificar con el carácter especial
ZERO WIDTH NO-BREAK SPACE (U+FEFF), que es invisible. En un sistema little endian, se codifica
como b'\xff\xfe' (decimal 255, 254). Debido a que, por diseño, no hay ningún carácter U+FFFE, la
secuencia de bytes b'\xff\xfe' debe significar el ESPACIO SIN INTERRUPCIÓN DE ANCHO CERO en
una codificación little-endian, por lo que el códec sabe qué orden de bytes usar.
Hay una variante de UTF-16, UTF-16LE, que es explícitamente little-endian y otra explícitamente big-
endian, UTF-16BE. Si los usa, no se genera un BOM:
Si está presente, se supone que la BOM se filtra con el códec UTF-16, de modo que solo obtenga el
contenido de texto real del archivo sin el ESPACIO SIN CORTE DE ANCHO CERO inicial. El estándar
dice que si un archivo es UTF-16 y no tiene BOM, se debe asumir que es UTF-16BE (big-endian). Sin
embargo, la arquitectura Intel x86 es little-endian, por lo que hay mucho UTF-16 little-endian sin BOM en
la naturaleza.
Todo este tema del endianness solo afecta a las codificaciones que usan palabras de más de un byte,
como UTF-16 y UTF-32. Una gran ventaja de UTF-8 es que produce la misma secuencia de bytes
independientemente de la máquina, por lo que no se necesita una lista de materiales. Sin embargo,
algunas aplicaciones de Windows (en particular, el Bloc de notas) agregan la BOM a los archivos UTF-8
de todos modos, y Excel depende de la BOM para detectar un archivo UTF-8; de lo contrario, asume el contenido.
está codificado con una página de códigos de Windows. El carácter U+FEFF codificado en UTF-8 es la
secuencia de tres bytes b'\xef\xbb\xbf'. Entonces, si un archivo comienza con esos tres bytes, es probable que
sea un archivo UTF-8 con una lista de materiales. Sin embargo, Python no asume automáticamente que un
archivo es UTF-8 solo porque comienza con b'\xef\xbb\xbf'.
La mejor práctica para manejar texto es el “sándwich Unicode” (Figura 4-2).4 Esto significa que los bytes deben
decodificarse a str tan pronto como sea posible en la entrada (por ejemplo, al abrir un archivo para leer). La
“carne” del emparedado es la lógica comercial de su programa, donde el manejo de texto se realiza
exclusivamente en objetos str . Nunca debe estar codificando o decodificando en medio de otro procesamiento.
En la salida, los str se codifican en bytes lo más tarde posible. La mayoría de los marcos web funcionan así, y
rara vez tocamos bytes cuando los usamos. En Django, por ejemplo, sus vistas deberían generar Unicode str;
El propio Django se encarga de codificar la respuesta a bytes, utilizando UTF-8 por defecto.
Figura 4-2. Sándwich Unicode: mejores prácticas actuales para el procesamiento de texto
Python 3 hace que sea más fácil seguir los consejos del sándwich Unicode, porque el código integrado abierto
hace la decodificación necesaria al leer y codificar al escribir archivos en modo de texto, por lo que todo lo que
obtienes de my_file.read() y lo pasas a my_file. write(text) son objetos str.5
4. Vi por primera vez el término "sándwich Unicode" en la excelente charla "Pragmatic Unicode" de Ned Batchelder en la PyCon de EE . UU.
2012.
5. Los usuarios de Python 2.6 o 2.7 deben usar io.open() para obtener la decodificación/codificación automática al leer/escribir.
Por lo tanto, usar archivos de texto es simple. Pero si confía en las codificaciones predeterminadas, será
mordido.
Ejemplo 4-9. Un problema de codificación de la plataforma (si intenta esto en su máquina, es posible que
vea el problema o no)
El error: especifiqué la codificación UTF-8 al escribir el archivo, pero no lo hice al leerlo, por lo que Python
asumió la codificación predeterminada del sistema (Windows 1252) y los bytes finales del archivo se
decodificaron como caracteres 'é' en lugar de 'mi'.
Ejecuté el Ejemplo 4-9 en una máquina con Windows 7. Las mismas declaraciones que se ejecutan en
GNU/Linux o Mac OSX recientes funcionan perfectamente porque su codificación predeterminada es
UTF-8, lo que da la falsa impresión de que todo está bien. Si se omitió el argumento de codificación al
abrir el archivo para escribir, se usaría la codificación predeterminada de la configuración regional y
leeríamos el archivo correctamente usando la misma codificación. Pero entonces este script generaría
archivos con diferentes contenidos de bytes dependiendo de la plataforma o incluso dependiendo de la
configuración regional en la misma plataforma, creando problemas de compatibilidad.
Un detalle curioso del ejemplo 4-9 es que la función escribir en la primera declaración informa que se
escribieron cuatro caracteres, pero en la línea siguiente se leen cinco caracteres.
El ejemplo 4-10 es una versión extendida del ejemplo 4-9, que explica ese y otros detalles.
Ejemplo 4-10. Una inspección más detallada del Ejemplo 4-9 que se ejecuta en Windows revela el error y
cómo solucionarlo
>>> fp2
<_io.TextIOWrapper nombre='cafe.txt' modo='r' codificación='cp1252'>
>>> fp2.codificación
'cp1252'
>>> fp2.leer()
'café'
>>> fp3 = open('cafe.txt', encoding='utf_8') >>> fp3
os.stat informa que el archivo contiene 5 bytes; UTF-8 codifica 'é' como 2 bytes, 0xc3
y 0xa9.
En la codificación de Windows cp1252 , el byte 0xc3 es una “Ô (A con tilde) y 0xa9
es el signo de derechos de autor.
El problema del Ejemplo 4-10 tiene que ver con confiar en una configuración predeterminada al abrir un archivo de
texto. Hay varias fuentes para dichos valores predeterminados, como se muestra en la siguiente sección.
manicomio Varias configuraciones afectan los valores predeterminados de codificación para E/S en Python. Consulte
el script default_encodings.py en el Ejemplo 4-11.
"""
expresiones =
locale.getpreferredencoding()
type(my_file) my_file.encoding
sys.stdout.isatty() sys.stdout.encoding
sys.stdin.isatty() sys.stdin.encoding
sys.stderr.isatty() sys.stderr.encoding
sys. getdefaultencoding()
sys.getfilesystemencoding()
"""
El resultado del Ejemplo 4-11 en GNU/Linux (Ubuntu 14.04) y OSX (Mavericks 10.9) es idéntico, y muestra que UTF-8
se usa en todas partes en estos sistemas:
$ python3 default_encodings.py
locale.getpreferredencoding() -> 'UTF-8'
tipo (mi_archivo) -> <clase '_io.TextIOWrapper'>
mi_archivo.codificación -> 'UTF-8'
sys.stdout.isatty() -> True
sys.stdout.encoding -> 'UTF-8'
sys.stdin.isatty() -> True
sys.stdin.encoding -> 'UTF-8'
sys.stderr.isatty( ) -> True
sys.stderr.encoding -> 'UTF-8'
sys.getdefaultencoding() -> 'utf-8'
sys.getfilesystemencoding() -> 'utf-8'
Ejemplo 4-12. Codificaciones predeterminadas en Windows 7 (SP 1) cmd.exe localizado para Brasil;
PowerShell da el mismo resultado
Z:\>chcp
Página de código activo: 850 Z:
\>python default_encodings.py
locale.getpreferredencoding() -> 'cp1252'
tipo(mi_archivo) -> <clase '_io.TextIOWrapper'>
mi_archivo.codificación -> 'cp1252'
sys.stdout.isatty() -> True
sys.stdout.encoding -> 'cp850'
sys.stdin.isatty() -> True
sys.stdin.encoding -> 'cp850'
sys.stderr.isatty() -> True
sys.stderr.encoding -> 'cp850'
sys.getdefaultencoding() -> 'utf-8'
sys.getfilesystemencoding() -> 'mbcs'
• Si omite el argumento de codificación al abrir un archivo, el valor predeterminado viene dado por
locale.getpreferredencoding() ('cp1252' en el Ejemplo 4-12).
• sys.getdefaultencoding() es usado internamente por Python para convertir datos binarios a/desde
str; esto sucede con menos frecuencia en Python 3, pero aún sucede.6 No se admite cambiar
esta configuración.7 • sys.getfilesystemencoding() se usa para codificar/decodificar nombres de
archivo (no contenido de archivo). Se usa cuando open() obtiene un argumento str para el nombre
del archivo; si el nombre de archivo se proporciona como un argumento de bytes , se pasa sin
cambios a la API del sistema operativo. El Python Unicode HOWTO dice: “en Windows, Python
usa el nombre mbcs para referirse a la codificación configurada actualmente”. El acrónimo MBCS
significa Conjunto de caracteres de varios bytes, que para Microsoft son codificaciones heredadas
de ancho variable como gb2312 o Shift_JIS , pero no UTF-8. (Sobre este tema, una respuesta útil
en Stack- Overflow es "Diferencia entre MBCS y UTF-8 en Windows".)
locale.getpreferredencoding(do_setlocale=True)
Devuelve la codificación utilizada para los datos de texto, según las preferencias del usuario. Las preferencias
del usuario se expresan de manera diferente en diferentes sistemas y es posible que no estén disponibles
programáticamente en algunos sistemas, por lo que esta función solo devuelve una suposición. […]
Por lo tanto, el mejor consejo sobre la codificación predeterminada es: no confíe en ellos.
Si sigue los consejos del sándwich Unicode y siempre es explícito sobre las codificaciones en sus
programas, evitará muchos dolores de cabeza. Desafortunadamente, Unicode es
6. Mientras investigaba este tema, no encontré una lista de situaciones en las que Python 3 convierte internamente bytes a
str. El desarrollador central de Python, Antoine Pitrou, dice en la lista comp.python.devel que las funciones internas de
CPython que dependen de tales conversiones “no se usan mucho en py3k”.
doloroso incluso si obtiene sus bytes correctamente convertidos a str. Las siguientes dos secciones cubren
temas que son simples en ASCII-land, pero se vuelven bastante complejos en el planeta Unicode:
normalización de texto (es decir, convertir texto a una representación uniforme para comparaciones) y
clasificación.
Las comparaciones de cadenas se complican por el hecho de que Unicode tiene caracteres combinados:
signos diacríticos y otras marcas que se adjuntan al carácter anterior y aparecen como uno solo cuando se
imprimen.
Por ejemplo, la palabra “café” puede estar compuesta de dos formas, utilizando cuatro o cinco puntos de
código, pero el resultado es exactamente el mismo:
>>> s1 = 'café'
>>> s2 = 'café\u0301'
>>> s1, s2 ('café', 'café')
>>> len(s1), len(s2) (4,
5) >>> s1 == s2
Falso
El punto de código U+0301 es el ACENTO AGUDO COMBINADO. Usarlo después de "e" se convierte en "é".
En el estándar Unicode, las secuencias como 'é' y 'e\u0301' se denominan “equivalentes canónicos” y se
supone que las aplicaciones las tratan como iguales. Pero Python ve dos secuencias diferentes de puntos de
código y las considera no iguales.
La forma de normalización C (NFC) compone los puntos de código para producir la cadena equivalente más
corta, mientras que NFD se descompone, expandiendo los caracteres compuestos en caracteres base y
separando los caracteres combinados. Ambas normalizaciones hacen que las comparaciones funcionen
como se esperaba:
Verdadero
Los teclados occidentales suelen generar caracteres compuestos, por lo que el texto escrito por los
usuarios estará en NFC de forma predeterminada. Sin embargo, para estar seguro, puede ser bueno
desinfectar las cadenas con ize normal ('NFC', user_text) antes de guardar. NFC también es la forma
de normalización recomendada por el W3C en Character Model for the World Wide Web: String
Matching and Searching.
NFC normaliza algunos caracteres individuales en otro carácter único. El símbolo de la unidad de
resistencia eléctrica ohm (ÿ) se normaliza a la omega mayúscula griega. Son visualmente idénticos,
pero se comparan desiguales por lo que es fundamental normalizar para evitar sorpresas:
'SIGNO DE
OHMIOS' >>> ohm_c = normalizar('NFC',
ohm) >>> nombre(ohm_c)
'LETRA GRIEGA MAYÚSCULA
OMEGA' >>> ohm == ohm_c
Falso
>>> normalizar('NFC', ohm) == normalizar('NFC', ohm_c)
Verdadero
En los acrónimos de las otras dos formas de normalización, NFKC y NFKD, la letra K significa
"compatibilidad". Estas son formas más fuertes de normalización, que afectan a los llamados
"caracteres de compatibilidad". Aunque uno de los objetivos de Unicode es tener un único
punto de código "canónico" para cada carácter, algunos caracteres aparecen más de una vez
por motivos de compatibilidad con los estándares preexistentes. Por ejemplo, el microsigno,
'µ' (U+00B5), se agregó a Unicode para admitir la conversión de ida y vuelta a latin1, aunque el
mismo carácter forma parte del alfabeto griego con punto de código U+03BC (LETRA
MINÚSCULA GRIEGA MU). Entonces, el signo micro se considera un "carácter de compatibilidad".
En los formularios NFKC y NFKD, cada carácter de compatibilidad se reemplaza por una
"descomposición de compatibilidad" de uno o más caracteres que se consideran una
representación "preferida", incluso si hay alguna pérdida de formato; idealmente, el formato
debe ser la responsabilidad del marcado externo, no forma parte de Unicode. A modo de
ejemplo, la descomposición de compatibilidad de la mitad de la fracción '½' (U+00BD) es la
secuencia de tres caracteres '1/2', y la descomposición de compatibilidad del microsigno
'µ' (U+00B5) es la baja ÿ ercase mu 'ÿ' (U+03BC).8
8. Curiosamente, el signo micro se considera un "carácter de compatibilidad" pero el símbolo de ohm no lo es. El resultado
final es que NFC no toca el signo micro sino que cambia el símbolo de ohmios a omega mayúscula, mientras que
NFKC y NFKD cambian tanto el ohmio como el micro por otros caracteres.
Aunque '1ÿ2' es un sustituto razonable de '½', y el signo micro es en realidad un mu griego en minúsculas, convertir '4²'
en '42' cambia el significado. Una aplicación podría almacenar '4²' como '4<sup>2</sup>', pero la función de
normalización no sabe nada sobre formateo. Por lo tanto, NFKC o NFKD pueden perder o distorsionar la información,
pero pueden producir representaciones intermedias convenientes para buscar e indexar: los usuarios pueden estar
contentos de que una búsqueda de '1ÿ2 pulgada' también encuentre documentos que contengan '½ pulgada'.
Al preparar el texto para la búsqueda o la indexación, otra operación es útil: el plegado de casos, nuestro próximo tema.
Plegado de
mayúsculas y minúsculas Básicamente, el plegado de mayúsculas y minúsculas consiste en convertir todo el texto a
minúsculas, con algunas transformaciones adicionales. Es compatible con el método str.casefold() (nuevo en Python 3.3).
Para cualquier cadena que contenga solo caracteres latinos1 , s.casefold() produce el mismo resultado que s.lower(),
con solo dos excepciones: el microsigno 'µ' se cambia a la minúscula griega mu (que tiene el mismo aspecto en la
mayoría de los casos). fonts) y el alemán Eszett o “s sostenido” (ß) se convierte en “ss”:
A partir de Python 3.4, hay 116 puntos de código para los que str.casefold() y str.low er() devuelven resultados
diferentes. Eso es el 0,11% de un total de 110.122 caracteres con nombre en Unicode 6.3.
Como es habitual con cualquier cosa relacionada con Unicode, el plegado de mayúsculas y minúsculas es un problema complicado
con muchos casos especiales lingüísticos, pero el equipo central de Python hizo un esfuerzo para proporcionar una solución que,
En las próximas dos secciones, pondremos en práctica nuestro conocimiento de normalización para desarrollar
funciones de utilidad.
hemos visto, NFC y NFD son seguros de usar y permiten comparaciones sensatas entre cadenas Unicode.
NFC es la mejor forma normalizada para la mayoría de las aplicaciones. str.case fold() es el camino a seguir
para las comparaciones que no distinguen entre mayúsculas y minúsculas.
Si trabaja con texto en muchos idiomas, un par de funciones como nfc_equal y fold_equal en el Ejemplo 4-13
son adiciones útiles a su caja de herramientas.
Falso
>>> nfc_equal(s1, s2)
Verdadero
Falso
>>> nfc_equal(s3, s4)
Falso
>>> doblar_igual(s3, s4)
"""
Más allá de la normalización Unicode y el plegado de cajas, que son parte del estándar Unicode, a veces tiene sentido
aplicar transformaciones más profundas, como cambiar 'café' por 'cafe'. Veremos cuándo y cómo en la siguiente
sección.
secreta de la Búsqueda de Google implica muchos trucos, pero uno de ellos aparentemente es ignorar los signos
diacríticos (p. ej., acentos, cedillas, etc.), al menos en algunos contextos. Eliminar los signos diacríticos no es una
forma adecuada de normalización porque a menudo cambia el significado de las palabras y puede producir falsos
positivos durante la búsqueda. Pero ayuda a lidiar con algunos hechos de la vida: las personas a veces son perezosas
o ignorantes sobre el uso correcto de los signos diacríticos, y las reglas ortográficas cambian con el tiempo, lo que
significa que los acentos van y vienen en el idioma vivo.
calibres
Aparte de la búsqueda, deshacerse de los signos diacríticos también hace que las URL sean más legibles, al menos
en los idiomas basados en el latín. Eche un vistazo a la URL del artículo de Wikipedia sobre la ciudad de São Paulo:
http://en.wikipedia.org/wiki/S%C3%A3o_Paulo
La parte %C3%A3 es la representación UTF-8 con escape de URL de la única letra “ã” (“a” con tilde). Lo siguiente es
mucho más amigable, incluso si no es la ortografía correcta:
http://en.wikipedia.org/wiki/Sao_Paulo
Para eliminar todos los signos diacríticos de una cadena, puede usar una función como la del Ejemplo 4-14.
Ejemplo 4-14. Función para eliminar todas las marcas de combinación (módulo sanitize.py)
importar cadena de
importación de datos unicode
def shave_marks(txt):
"""Eliminar todos los signos diacríticos"""
norm_txt = unicodedata.normalize('NFD', txt) shaved
= ''.join(c for c in norm_txt if not
unicodedata.combining(c)) return
unicodedata.normalize('NFC', afeitado)
>>> pedido = '“Herr Voß: • ½ taza de café con leche Œtker™ • tazón de açaí.”' >>>
shave_marks(pedido)
'“Herr Voß: • ½ taza de café con leche Œtker™ • tazón de acai.”'
>>> Griego = 'ÿÿÿÿÿÿÿ, Zéfiro' >>>
marcas_de_afeitado(Griego)
'ÿÿÿÿÿÿÿ, Zefiro'
La función shave_marks del Ejemplo 4-14 funciona bien, pero tal vez va demasiado lejos. A menudo, la razón
para eliminar los signos diacríticos es cambiar el texto latino a ASCII puro, pero shave_marks también cambia
los caracteres no latinos, como las letras griegas, que nunca se convertirán en ASCII solo por perder sus acentos.
Por lo tanto, tiene sentido analizar cada carácter base y eliminar las marcas adjuntas solo si el carácter base es
una letra del alfabeto latino. Esto es lo que hace el Ejemplo 4-16 .
Ejemplo 4-16. Función para eliminar las marcas de combinación de los caracteres latinos (se omiten las
declaraciones de importación ya que esto es parte del módulo sanitize.py del Ejemplo 4-14)
def shave_marks_latin(txt):
"""Eliminar todos los signos diacríticos de los caracteres base latinos"""
norm_txt = unicodedata.normalize('NFD', txt) latin_base = Falsos
guardianes = [] para c en norm_txt: if unicodedata.combining(c ) y
base_latina:
guardianes.append(c)
# si no está combinando caracteres, es un nuevo carácter base
si no es unicodedata.combining(c):
latin_base = c en string.ascii_letters
afeitado = ''.join(guardianes)
devolver unicodedata.normalize('NFC', afeitado)
Un paso aún más radical sería reemplazar los símbolos comunes en los textos occidentales (p. ej.,
comillas, guiones largos, viñetas, etc.) en equivalentes ASCII . Esto es lo que la función
asciize lo hace en el ejemplo 4-17.
Ejemplo 4-17. Transforme algunos símbolos tipográficos occidentales en ASCII (este recorte
pet también es parte de sanitize.py del Ejemplo 4-14)
multi_map.update(single_map)
def dewinize(txt):
"""Reemplazar símbolos Win1252 con caracteres o secuencias ASCII"""
devolver txt.translate(multi_mapa)
def asciize(txt):
no_marks = shave_marks_latin(dewinize(txt))
no_marks = no_marks.replace('ß', 'ss') return
unicodedata.normalize('NFKC', no_marks)
dewinize no afecta el texto ASCII o latin1 , solo las adiciones de Microsoft a latin1 en cp1252.
Reemplace el Eszett con "ss" (no estamos usando case fold aquí porque queremos preservar el
caso).
Aplique la normalización NFKC para componer caracteres con sus puntos de código de compatibilidad.
>>> pedido = '“Herr Voß: • ½ taza de café con leche Œtker™ • tazón de açaí.”' >>>
dewinize(pedido)
'"Herr Voß: - ½ taza de café con leche OEtker(TM) - tazón de açaí."' >>>
asciize(pedir)
'"Herr Voss: - 1ÿ2 taza de café con leche OEtker(TM) - tazón de acai".'
Los diferentes idiomas tienen sus propias reglas para eliminar signos diacríticos.
Por ejemplo, los alemanes cambian la 'ü' por 'ue'. Nuestra función asciize no es
tan refinada, por lo que puede o no ser adecuada para su idioma. Sin embargo,
funciona aceptablemente para el portugués.
En resumen, las funciones de sanitize.py van mucho más allá de la normalización estándar y realizan una
cirugía profunda en el texto, con buenas posibilidades de cambiar su significado. Solo usted puede decidir si
ir tan lejos, conociendo el idioma de destino, sus usuarios y cómo se utilizará el texto transformado.
Python ordena secuencias de cualquier tipo comparando los elementos de cada secuencia uno por uno.
Para cadenas, esto significa comparar los puntos de código. Desafortunadamente, esto produce resultados
inaceptables para cualquiera que use caracteres que no sean ASCII.
Las reglas de clasificación varían según el lugar, pero en portugués y en muchos idiomas que usan el alfabeto latino,
los acentos y las cedillas rara vez hacen una diferencia al clasificar.9 Por lo tanto, "cajá" se clasifica como "caja" y
debe ir antes de "caju".
La forma estándar de ordenar texto que no es ASCII en Python es usar la función locale.strxfrm que, de acuerdo
con los documentos del módulo de configuración regional , "transforma una cadena en una que se puede usar en
comparaciones con reconocimiento de configuración regional".
Para habilitar locale.strxfrm, primero debe establecer una configuración regional adecuada para su aplicación y rezar
para que el sistema operativo la admita. En GNU/Linux (Ubuntu 14.04) con la configuración regional pt_BR , funciona
la secuencia de comandos del Ejemplo 4-19 .
Por lo tanto, debe llamar a setlocale(LC_COLLATE, «your_locale») antes de usar locale.strxfrm como clave al
ordenar.
• Debido a que la configuración local es global, no se recomienda llamar a setlocale en una biblioteca. Su
aplicación o marco debe establecer la configuración regional cuando se inicia el proceso y no debe cambiarla
después.
• La configuración regional debe estar instalada en el sistema operativo; de lo contrario, setlocale genera una configuración regional. Error:
• Debe saber cómo se escribe el nombre de la configuración regional. Están bastante estandarizados en los
derivados de Unix como 'language_code.encoding', pero en Windows la sintaxis es más complicada: Language
Name-Language Variant_Region Name.code page>. Tenga en cuenta que las partes Nombre del idioma,
Variante del idioma y Nombre de la región pueden tener espacios dentro de ellas, pero las partes posteriores
a la primera tienen un prefijo especial.
9. Los signos diacríticos afectan la clasificación solo en el raro caso de que sean la única diferencia entre dos palabras, en ese
caso, la palabra con un diacrítico se ordena después de la palabra simple.
caracteres diferentes: un guión, un carácter de subrayado y un punto. Todas las partes parecen ser
opcionales excepto el nombre del idioma. Por ejemplo: Inglés_Estados Unidos. 850 significa Nombre de
idioma "Inglés", región "Estados Unidos" y página de códigos "850".
Los nombres de idiomas y regiones que comprende Windows se enumeran en el artículo de MSDN
Constantes y cadenas de identificadores de idioma, mientras que Identificadores de página de códigos
enumera los números para la última parte.10
• La configuración regional debe ser implementada correctamente por los creadores del sistema operativo.
Tuve éxito en Ubuntu 14.04, pero no en OSX (Mavericks 10.9). En dos Mac diferentes, la llamada
setlocale(LC_COLLATE, 'pt_BR.UTF-8') devuelve la cadena 'pt_BR.UTF-8' sin quejas. Pero sorted(fruits,
key=locale.strxfrm) produjo el mismo resultado incorrecto que sorted(fruits) . También probé las
configuraciones regionales fr_FR, es_ES y de_DE en OSX, pero locale.strxfrm nunca hizo su trabajo.11
Entonces, la solución de biblioteca estándar para la clasificación internacionalizada funciona, pero parece ser
compatible solo con GNU/Linux (quizás también con Windows, si es un experto). Incluso entonces, depende
de la configuración local, lo que genera dolores de cabeza en la implementación.
Afortunadamente, existe una solución más sencilla: la biblioteca PyUCA, disponible en PyPI.
prolífico colaborador de Django, debe haber sentido el dolor y creó PyUCA, una implementación en Python
puro del Algoritmo de intercalación Unicode (UCA).
El ejemplo 4-20 muestra lo fácil que es usarlo.
Esto es amigable y simplemente funciona. Lo probé en GNU/Linux, OSX y Windows. Solo se admite Python
3.X en este momento.
10. Gracias a Leonardo Rachael que fue más allá de sus deberes como revisor técnico e investigó estos detalles de
Windows, aunque él mismo es un usuario de GNU/Linux.
11. Una vez más, no pude encontrar una solución, pero encontré a otras personas reportando el mismo problema. Alex
Martelli, uno de los revisores técnicos, no tuvo problemas para usar setlocale y locale.strxfrm en su Mac con OSX 10.9.
En resumen: su kilometraje puede variar.
el cuadro, utiliza allkeys.txt, que se incluye con el proyecto. Esa es solo una copia de la tabla de elementos de
intercalación Unicode predeterminada de Unicode 6.3.0.
Por cierto, esa tabla es una de las muchas que componen la base de datos Unicode, nuestro próximo tema.
El módulo unicodedata tiene funciones que devuelven metadatos de caracteres; por ejemplo, su nombre oficial
en el estándar, si es un carácter de combinación (por ejemplo, diacrítico como una tilde de combinación) y el
valor numérico del símbolo para humanos (no su punto de código).
El ejemplo 4-21 muestra el uso de unicodedata.name() y unicodedata.numeric() junto con los métodos .isdecimal()
y .isnumeric() de str.
Ejemplo 4-21. Demostración de los metadatos de caracteres numéricos de la base de datos Unicode (las
llamadas describen cada columna en la salida)
importar unicodedata
importar re
re_digit = re.compile(r'\d')
muestra = '1\xbc\xb2\u0969\u136b\u216b\u2466\u2480\u3285'
para char en la
muestra: print('U+%04x' %
ord(char), char.center(6),
're_dig' if re_digit.match(char) else '-', 'isdig' if
char.isdigit() else '-', 'isnum' if char.isnumeric() else
'-', format(unicodedata.numeric(char), '5.2f'),
unicodedata.name(char), sep='\t')
Figura 4-3. Nueve caracteres numéricos y metadatos sobre ellos; re_dig significa que el carácter
coincide con la expresión regular r'\d';
La Figura 4-3 muestra que la expresión regular r'\d' coincide con el dígito "1" y el dígito Devanagari
3, pero no con otros caracteres que la función isdi git considera dígitos . El módulo re no es tan
experto en Unicode como podría ser. El nuevo módulo regex disponible en PyPI fue diseñado para
reemplazar eventualmente a re y proporciona una mejor compatibilidad con Unicode.12 Volveremos
al módulo re en la siguiente sección.
A lo largo de este capítulo, hemos usado varias funciones UnicodeData , pero hay muchas más que
no cubrimos. Consulte la documentación de la biblioteca estándar para el módulo unicodedata .
Concluiremos nuestro recorrido de str versus bytes con un vistazo rápido a una nueva tendencia:
las API de modo dual que ofrecen funciones que aceptan argumentos str o bytes con un manejo
especial según el tipo.
12. Aunque no fue mejor que volver a identificar dígitos en esta muestra en particular.
Ejemplo 4-22. ramanujan.py: compara el comportamiento de una cadena simple y una expresión regular de bytes
siones
importar re
re_numbers_str = re.compilar(r'\d+')
re_words_str = re.compilar(r'\w+')
re_numbers_bytes = re.compilar(rb'\d+')
re_words_bytes = re.compilar(rb'\w+')
bytes_texto = cadena_texto.encode('utf_8')
Texto Unicode para buscar, que contiene los dígitos tamiles para 1729 (la línea lógica
continúa hasta el símbolo del paréntesis derecho).
Esta cadena se une a la anterior en tiempo de compilación (ver “2.4.2. Literal de cadena
concatenación” en The Python Language Reference).
Se necesita una cadena de bytes para buscar con las expresiones regulares de bytes .
El patrón de bytes rb'\d+' coincide solo con los bytes ASCII para dígitos.
El patrón str r'\w+' coincide con las letras, los superíndices, los dígitos Tamil y ASCII.
El patrón de bytes rb'\w+' coincide solo con los bytes ASCII para letras y dígitos.
El ejemplo 4-22 es un ejemplo trivial para aclarar un punto: puede usar expresiones regulares en str y bytes, pero
en el segundo caso, los bytes fuera del rango ASCII se tratan como caracteres que no son dígitos ni palabras.
Para las expresiones regulares str , hay un indicador re.ASCII que hace que \w, \W, \b, \B, \d, \D, \s y
\S realicen coincidencias solo de ASCII. Consulte la documentación del módulo re para obtener
detalles completos.
GNU/Linux no es experto en Unicode, por lo que en el mundo real puede encontrar nombres de archivos hechos
de secuencias de bytes que no son válidos en ningún esquema de codificación sensible y no se pueden decodificar
a str. Los servidores de archivos con clientes que utilizan una variedad de sistemas operativos son particularmente
propensos a este problema.
Para solucionar este problema, todas las funciones del módulo os que aceptan nombres de archivos o rutas toman
argumentos como str o bytes. Si se llama a una de estas funciones con un argumento str , el argumento se
convertirá automáticamente usando el códec nombrado por sys.getfilesystemencoding(), y la respuesta del sistema
operativo se decodificará con el mismo códec. Esto es casi siempre lo que desea, de acuerdo con las mejores
prácticas del sándwich Unicode.
Pero si debe lidiar con (y quizás corregir) nombres de archivo que no se pueden manejar de esa manera, puede
pasar argumentos de bytes a las funciones os para obtener valores de retorno de bytes . Este
La característica le permite manejar cualquier archivo o nombre de ruta, sin importar cuántos gremlins pueda
encontrar. Vea el Ejemplo 4-23.
Dado un argumento de byte , listdir devuelve los nombres de archivo como bytes: b'\xcf\x80' es la
codificación UTF-8 de la letra griega pi).
Para ayudar con el manejo manual de secuencias str o bytes que son nombres de archivos o rutas, el módulo
os proporciona funciones especiales de codificación y decodificación:
fsencode(nombre de archivo)
Codifica el nombre del archivo (puede ser str o bytes) a bytes usando el códec nombrado por
sys.getfilesystemencoding() si el nombre del archivo es del tipo str, de lo contrario, devuelve los bytes del
nombre del archivo sin cambios.
En las plataformas derivadas de Unix, estas funciones utilizan el controlador de errores de escape suplente
(consulte la barra lateral a continuación) para evitar la obstrucción de bytes inesperados. En Windows, se
utiliza el controlador de errores estricto .
La idea de este controlador de errores es reemplazar cada byte no decodificable con un punto de código en el rango
Unicode de U+DC00 a U+DCFF que se encuentra en el llamado "Área sustituta baja" del estándar: un espacio de
código sin caracteres. asignado, reservado para uso interno en aplicaciones. Al codificar, dichos puntos de código
se vuelven a convertir a los valores de byte que reemplazaron. Vea el Ejemplo 4-24.
>>> os.listdir('.')
['abc.txt', 'dígitos-de-ÿ.txt']
>>> os.listdir(b'.')
[b'abc.txt', b'dígitos-de-\xcf\x80.txt'] >>>
pi_name_bytes = os.listdir(b'.')[1 ] >>>
pi_name_str = pi_name_bytes.decode('ascii', 'surrogateescape') >>> pi_name_str
'dígitos-de-\udccf\udc80.txt' >>> pi_name_str.encode('ascii', 'surrogateescape') b
'dígitos-de-\xcf\x80.txt'
Cada byte que no es ASCII se reemplaza por un punto de código sustituto: '\xcf\x80' se convierte en
'\udccf\udc80'.
Vuelva a codificar en bytes ASCII: cada punto de código sustituto se reemplaza por el byte que
reemplazó.
Esto finaliza nuestra exploración de str y bytes. Si todavía estás conmigo, ¡felicidades!
Después de una breve descripción general de los tipos de datos de secuencia binaria (bytes, bytearray y
memoryview), pasamos a codificar y decodificar, con una muestra de códecs importantes, seguidos de enfoques
para prevenir o tratar los infames UnicodeEncodeError, UnicodeDecodeError y SyntaxError. causado por una
codificación incorrecta en los archivos fuente de Python.
Mientras que en el tema del código fuente, presenté mi posición sobre el debate sobre los identificadores
que no son ASCII: si los mantenedores del código base quieren usar un lenguaje humano que tiene
caracteres que no son ASCII, los identificadores deberían hacer lo mismo, a menos que el código necesite
para ejecutarse en Python 2 también. Pero si el proyecto tiene como objetivo atraer una base de
contribuyentes internacionales, los identificadores deben estar hechos de palabras en inglés y luego ASCII es suficiente.
como la única sugerencia de codificación que se encuentra comúnmente en archivos UTF-16 y UTF-32, a veces
también en archivos UTF-8.
En la siguiente sección, demostramos cómo abrir archivos de texto, una tarea fácil excepto por un
escollo: el argumento de la palabra clave encoding= no es obligatorio cuando abre un archivo de
texto, pero debería serlo. Si no especifica la codificación, termina con un programa que logra
generar "texto sin formato" que es incompatible entre plataformas, debido a codificaciones
predeterminadas en conflicto. Luego expusimos las diferentes configuraciones de codificación que
Python usa como predeterminadas y cómo detectarlas: locale.getpreferredencoding(), sys.getfilesys
temencoding(), sys.getdefaultencoding() y las codificaciones para los archivos de E/S estándar (p.
ej., sys.stdout.codificación). Una triste realidad para los usuarios de Windows es que estas
configuraciones a menudo tienen valores distintos dentro de la misma máquina, y los valores son
incompatibles entre sí; Los usuarios de GNU/Linux y OSX, por el contrario, viven en un lugar más
feliz donde UTF-8 es el valor predeterminado prácticamente en todas partes.
Las comparaciones de texto son sorprendentemente complicadas porque Unicode proporciona múltiples formas
de representar algunos caracteres, por lo que la normalización es un requisito previo para la coincidencia de
texto. Además de explicar la normalización y el plegado de casos, presentamos algunas funciones de utilidad
que puede adaptar a sus necesidades, incluidas transformaciones drásticas como la eliminación de todos los
acentos. Luego vimos cómo ordenar el texto Unicode correctamente aprovechando el módulo de configuración
regional estándar , con algunas advertencias, y una alternativa que no depende de configuraciones locales
complicadas: el paquete PyUCA externo.
Finalmente, echamos un vistazo a la base de datos Unicode (una fuente de metadatos sobre cada carácter) y
terminamos con una breve discusión de las API de modo dual (por ejemplo, los módulos re y os , donde algunas
funciones se pueden llamar con argumentos str o bytes , provocando resultados diferentes pero apropiados).
Otras lecturas
Ned Batchelder's 2012 PyCon US talk "Pragmatic Unicode - or - How Do I Stop the Pain?" fue sobresaliente.
Ned es tan profesional que proporciona una transcripción completa de la charla junto con las diapositivas y el
video. Esther Nam y Travis Fischer dieron una excelente charla de PyCon 2014 "Codificación de caracteres y
Unicode en Python: Cómo (ÿ°ÿ°)ÿÿ ÿÿÿ con dignidad" (diapositivas, video), de la cual cité el breve y dulce de
este capítulo epígrafe: “Los humanos usan texto. Las computadoras hablan bytes”. Lennart Regebro, uno de los
revisores técnicos de este libro, presenta su "Modelo mental útil de Unicode (UMMU)" en la breve publicación
"Unicode sin confusión: ¿Qué es Unicode?". Unicode es un estándar complejo, por lo que UMMU de Lennart es
un punto de partida realmente útil.
El CÓMO oficial de Unicode en los documentos de Python aborda el tema desde varios ángulos diferentes,
desde una buena introducción histórica hasta detalles de sintaxis, códecs, expresiones regulares, nombres de
archivo y mejores prácticas para E/S compatible con Unicode (es decir, el sándwich Unicode), con muchos
enlaces de referencia adicionales de cada sección. Capítulo 4, “Cuerdas”, de Mark
El increíble libro de Pilgrim Dive into Python 3 también proporciona una muy buena introducción a la
compatibilidad con Unicode en Python 3. En el mismo libro, el Capítulo 15 describe cómo se transfirió la
biblioteca Chardet de Python 2 a Python 3, un valioso caso de estudio dado que el cambio de la antigua
str a los nuevos bytes es la causa de la mayoría de los problemas de migración, y esa es una
preocupación central en una biblioteca diseñada para detectar codificaciones.
Si conoce Python 2 pero es nuevo en Python 3, What's New in Python 3.0 de Guido van Rossum tiene
15 viñetas que resumen lo que cambió, con muchos enlaces. Guido comienza con la declaración
contundente: "Todo lo que creía saber sobre los datos binarios y Unicode ha cambiado". La publicación
de blog de Armin Ronacher "La guía actualizada de Unicode en Python" es profunda y destaca algunas
de las trampas de Unicode en Python 3 (Armin no es un gran admirador de Python 3).
El capítulo 2, "Cadenas y texto", de Python Cookbook, tercera edición (O'Reilly), de David Beazley y
Brian K. Jones, tiene varias recetas que tratan sobre la normalización de Unicode, la desinfección del
texto y la realización de operaciones orientadas al texto en byte. secuencias. El Capítulo 5 cubre archivos
y E/S, e incluye la “Receta 5.17. Escribir bytes en un archivo de texto”, lo que demuestra que debajo de
cualquier archivo de texto siempre hay una secuencia binaria a la que se puede acceder directamente
cuando sea necesario. Más adelante en el libro de cocina, el módulo struct se usa en la “Receta 6.11.
Lectura y escritura de matrices binarias de estructuras.
El blog Python Notes de Nick Coghlan tiene dos publicaciones muy relevantes para este capítulo:
"Protocolos binarios compatibles con Python 3 y ASCII" y "Procesamiento de archivos de texto en Python 3".
Muy recomendable.
Las secuencias binarias están a punto de obtener nuevos constructores y métodos en Python 3.5, con
una de las firmas de constructores actuales en desuso (ver PEP 467 — Mejoras menores de API para
secuencias binarias). Python 3.5 también debería ver la implementación de PEP 461: agregar % de
formato a bytes y bytearray.
Una lista de codificaciones compatibles con Python está disponible en Codificaciones estándar en la
documentación del módulo de códecs . Si necesita obtener esa lista mediante programación, vea cómo
se hace en el script /Tools/ unicode/ listcodecs.py que viene con el código fuente de CPython.
"Changing the Python Default Encoding Considered Harmful" de Martijn Faassen y "sys.setdefaultencoding
Is Evil" de Tarek Ziadé explican por qué la codificación predeterminada que obtiene de
sys.getdefaultencoding() nunca debe cambiarse, incluso si descubre cómo.
Los libros Explicación de Unicode de Jukka K. Korpela (O'Reilly) y Desmitificación de Unicode de Richard
Gillam (Addison-Wesley) no son específicos de Python, pero fueron muy útiles cuando estudié los
conceptos de Unicode. Programación con Unicode de Victor Stinner es un libro gratuito y autoeditado
(Creative Commons BY-SA) que cubre Unicode en general, así como herramientas y API en el contexto
de los principales sistemas operativos y algunos lenguajes de programación, incluido Python.
Las páginas del W3C Case Folding: An Introduction and Character Model for the World Wide
Web: String Matching and Searching cubren los conceptos de normalización, siendo el primero
una introducción suave y el segundo un borrador de trabajo escrito en lenguaje estándar seco:
el mismo tono de el Anexo estándar Unicode #15 — Formularios de normalización Unicode.
Las Preguntas frecuentes/Normalización de Unicode.org son más fáciles de leer, al igual que
las Preguntas frecuentes sobre NFC de Mark Davis, autor de varios algoritmos Unicode y
presidente del Consorcio Unicode en el momento de escribir este artículo.
Plataforma improvisada
Para cualquier persona que trabaje diariamente con texto que no esté en inglés, "texto sin formato" no implica
"ASCII". El Glosario de Unicode define texto sin formato como este:
Texto codificado por computadora que consta únicamente de una secuencia de puntos de
código de un estándar dado, sin otra información estructural o de formato.
Esa definición empieza muy bien, pero no estoy de acuerdo con la parte que sigue a la coma. HTML es un
gran ejemplo de un formato de texto sin formato que contiene información estructural y de formato. Pero sigue
siendo texto sin formato porque cada byte en dicho archivo está ahí para representar un carácter de texto,
generalmente usando UTF-8. No hay bytes sin significado de texto, como puede encontrar en un
documento .png o .xls donde la mayoría de los bytes representan valores binarios empaquetados como valores
RGB y números de coma flotante. En texto sin formato, los números se representan como secuencias de
caracteres de dígitos.
Estoy escribiendo este libro en un formato de texto sin formato llamado, irónicamente, AsciiDoc, que es parte
de la cadena de herramientas de la excelente plataforma de publicación de libros Atlas de O'Reilly. Los
archivos fuente de AsciiDoc son texto sin formato, pero son UTF-8, no ASCII. De lo contrario, escribir este
capítulo hubiera sido realmente doloroso. A pesar del nombre, AsciiDoc es simplemente genial.
El mundo de Unicode está en constante expansión y, en los bordes, el soporte de herramientas no siempre
está ahí. Es por eso que tuve que usar imágenes para las Figuras 4-1, 4-3 y 4-4: no todos los caracteres que
quería mostrar estaban disponibles en las fuentes utilizadas para renderizar el libro. Por otro lado, los
terminales Ubuntu 14.04 y OSX 10.9 los muestran perfectamente, incluidos los caracteres japoneses de la
palabra “mojibake”: ÿÿÿÿ.
Adivinanzas Unicode
Calificadores imprecisos como "a menudo", "la mayoría" y "generalmente" parecen aparecer cada vez que
escribo sobre la normalización de Unicode. Lamento la falta de un consejo más definitivo, pero hay tantas
excepciones a las reglas en Unicode que es difícil ser absolutamente positivo.
Por ejemplo, el µ (signo micro) se considera un "carácter de compatibilidad", pero los símbolos ÿ (ohm) y Å
(Ångström) no lo son. La diferencia tiene consecuencias prácticas: la normalización NFC, recomendada para
la coincidencia de texto, reemplaza ÿ (ohm) por ÿ (mayúsculas Grek omega) y Å (Ångström) por Å (A mayúscula
con un anillo arriba).
Pero como “carácter de compatibilidad”, el µ (microsigno) no se reemplaza por el visualmente
ÿ idéntico (mu minúscula griega), excepto cuando se aplican las normalizaciones NFKC o NFKD más fuertes,
y estas transformaciones son con pérdida.
Entiendo que el µ (microsigno) está en Unicode porque aparece en la codificación latin1 y reemplazarlo con el
griego mu interrumpiría la conversión de ida y vuelta. Después de todo, es por eso que el microsigno es un
"carácter de compatibilidad". Pero si los símbolos de ohm y Ångström no están en Unicode por razones de
compatibilidad, ¿por qué tenerlos? Ya hay puntos de código para la LETRA MAYÚSCULA GRIEGA OMEGA
y LA LETRA A MAYÚSCULA LATINA CON ANILLO ARRIBA, que tienen el mismo aspecto y los reemplazan
en la normalización NFC.
Imagínate.
Mi opinión después de muchas horas estudiando Unicode: es enormemente complejo y está lleno de casos
especiales, lo que refleja la maravillosa variedad de lenguajes humanos y la política de los estándares de la
industria.
Los documentos oficiales de Python evitan el problema de cómo se almacenan en la memoria los puntos de
código de una cadena . Esto es, después de todo, un detalle de implementación. En teoría, no importa:
cualquiera que sea la representación interna, cada str debe codificarse en bytes en la salida.
En la memoria, Python 3 almacena cada str como una secuencia de puntos de código usando un número fijo
de bytes por punto de código, para permitir un acceso directo eficiente a cualquier carácter o porción.
Antes de Python 3.3, CPython podía compilarse para usar 16 o 32 bits por punto de código en RAM; el primero
era de “construcción estrecha” y el segundo de “construcción ancha”. Para saber cuál tiene, verifique el valor
de sys.maxunicode: 65535 implica una "construcción estrecha" que no puede manejar puntos de código por
encima de U+FFFF de forma transparente. Una “construcción amplia” no tiene esta limitación, pero consume
mucha memoria: 4 bytes por carácter, incluso cuando la gran mayoría de los puntos de código para ideogramas
chinos caben en 2 bytes. Ninguna opción era excelente, por lo que tenía que elegir según sus necesidades.
Desde Python 3.3, al crear un nuevo objeto str , el intérprete verifica los caracteres que contiene y elige el
diseño de memoria más económico que sea adecuado para esa str en particular: si solo hay caracteres en el
rango latin1 , esa str usará solo un byte por punto de código. De lo contrario, se pueden usar 2 o 4 bytes por
punto de código, según el str.
Esta es una simplificación; para obtener todos los detalles, consulte PEP 393 — Representación de cadenas
flexibles.
La representación de cadena flexible es similar a la forma en que funciona el tipo int en Python 3: si el número
entero cabe en una palabra de máquina, se almacena en una palabra de máquina. De lo contrario, el intérprete
cambia a una representación de longitud variable como la del tipo largo de Python 2 . Es agradable ver la
difusión de buenas ideas.
PARTE III
CAPÍTULO 5
Nunca he considerado que Python esté fuertemente influenciado por lenguajes funcionales, sin
importar lo que la gente diga o piense. Estaba mucho más familiarizado con los lenguajes
imperativos como C y Algol 68 y, aunque había creado funciones como objetos de primera clase,
no veía a Python como un lenguaje de programación funcional.1
—Guido van Rossum
Python BDFL
Las funciones en Python son objetos de primera clase. Los teóricos del lenguaje de programación definen un "objeto de primera
Los enteros, las cadenas y los diccionarios son otros ejemplos de objetos de primera clase en Python, nada especial aquí. Pero si
llegaste a Python desde un lenguaje donde las funciones no son ciudadanos de primera clase, este capítulo y el resto de la Parte III
del libro se enfoca en las implicaciones y aplicaciones prácticas de tratar las funciones como objetos.
1. “Orígenes de las características funcionales de Python ”, del blog The History of Python de Guido.
139
Machine Translated by Google
Ejemplo 5-1. Cree y pruebe una función, luego lea su __doc__ y verifique su tipo
Esta es una sesión de consola, por lo que estamos creando una función en "tiempo de
El atributo __doc__ se utiliza para generar el texto de ayuda de un objeto. En la consola interactiva de Python,
el comando ayuda (factorial) mostrará una pantalla como la de la Figura 5-1.
Figura 5-1. Pantalla de ayuda para la función factorial; el texto es del atributo __doc__ del objeto de función
El ejemplo 5-2 muestra la naturaleza de “primera clase” de un objeto función. Podemos asignarle un hecho
variable y llamarlo por ese nombre. También podemos pasar factorial como argumento a map. La función map
devuelve un iterable donde cada elemento es el resultado de la aplicación del primer argumento (una función) a
elementos sucesivos del segundo argumento (un iterable), range(10) en este ejemplo.
Ejemplo 5-2. Use la función con un nombre diferente y pase la función como argumento
>>> hecho = factorial
>>> hecho <función
factorial en 0x...> >>> hecho(5)
120
>>> mapa(factorial, rango(11))
<objeto de mapa en 0x...> >>>
lista(mapa(hecho, rango(11))) [1, 1,
2, 6, 24, 120, 720 , 5040, 40320, 362880, 3628800]
Tener funciones de primera clase permite programar en un estilo funcional. Uno de los sellos distintivos de
la programación funcional es el uso de funciones de orden superior, nuestro próximo tema.
Por ejemplo, para ordenar una lista de palabras por longitud, simplemente pase la función len como clave,
como en el Ejemplo 5-3.
>>> frutas = ['fresa', 'higo', 'manzana', 'cereza', 'frambuesa', 'plátano'] >>> sorted(frutas,
clave=len) ['higo', 'manzana', 'cereza', 'plátano', 'frambuesa', 'fresa']
>>>
Cualquier función de un argumento se puede utilizar como clave. Por ejemplo, para crear un diccionario de
rimas, puede ser útil ordenar cada palabra escrita al revés. En el ejemplo 5-4, observe que las palabras de
la lista no cambian en absoluto; solo se usa su ortografía invertida como criterio de clasificación, de modo
que las bayas aparecen juntas.
función con un conjunto dinámico de argumentos, puede simplemente escribir fn(*args, **key
palabras) en lugar de aplicar (fn, args, kwargs).
Las funciones map, filter y reduce de orden superior todavía existen, pero mejor alterÿ
los nativos están disponibles para la mayoría de sus casos de uso, como se muestra en la siguiente sección.
Ejemplo 5-5. Listas de factoriales producidos con mapa y filtro en comparación con alternativas
codificado como listas de comprensión
>>>
Lista de factoriales de números impares hasta el 5!, usando tanto mapa como filtro.
En Python 3, mapee y filtre los generadores de retorno, una forma de iterador, por lo que su directo
replace ahora es una expresión generadora (en Python 2, estas funciones devolvían listas,
por lo tanto, su alternativa más cercana es un listcomp).
4950
>>> suma(rango(100))
4950
>>>
Importe agregar para evitar crear una función solo para sumar dos números.
La idea común de suma y reducción es aplicar alguna operación a elementos sucesivos en una
secuencia, acumulando resultados anteriores, reduciendo así una secuencia de valores a un solo
valor.
Devuelve True si todos los elementos del iterable son verdaderos; all([]) devuelve Verdadero.
cualquiera (iterable)
Devuelve True si algún elemento del iterable es verdadero; cualquiera([]) devuelve Falso.
Ofrezco una explicación más completa de reduce en “Vector Take #4: Hashing and a Faster ==” en
la página 288, donde un ejemplo continuo proporciona un contexto significativo para el uso de esta
función. Las funciones de reducción se resumen más adelante en el libro cuando los iterables están
enfocados, en “Funciones de reducción iterables” en la página 434.
Para usar una función de orden superior, a veces es conveniente crear una función pequeña y única.
Por eso existen las funciones anónimas. Los cubriremos a continuación.
Funciones anónimas
La palabra clave lambda crea una función anónima dentro de una expresión de Python.
Sin embargo, la sintaxis simple de Python limita el cuerpo de las funciones lambda a expresiones
puras. En otras palabras, el cuerpo de una lambda no puede realizar asignaciones ni usar ninguna
otra declaración de Python, como while, try, etc.
El mejor uso de las funciones anónimas es en el contexto de una lista de argumentos. Por ejemplo,
el Ejemplo 5-7 es el ejemplo de índice de rima del Ejemplo 5-4 reescrito con lambda, sin definir una
función inversa .
Ejemplo 5-7. Ordenar una lista de palabras por su ortografía inversa usando lambda
>>> frutas = ['fresa', 'higo', 'manzana', 'cereza', 'frambuesa', 'plátano'] >>> sorted(frutas, clave=palabra
lambda : palabra[::-1])
Fuera del contexto limitado de argumentos para funciones de orden superior, las funciones anónimas rara vez son
útiles en Python. Las restricciones sintácticas tienden a hacer que las lambdas no triviales sean ilegibles o inviables.
Si encuentra un fragmento de código difícil de entender debido a una lambda, Fredrik Lundh
sugiere este procedimiento de refactorización:
4. Elimina el comentario.
Estos pasos se citan del CÓMO de programación funcional, una lectura obligada.
La sintaxis lambda es simplemente azúcar sintáctica: una expresión lambda crea un objeto de función al igual que
la declaración def . Ese es solo uno de varios tipos de objetos a los que se puede llamar en Python.
La siguiente sección repasa todos ellos.
puede llamar: Funciones definidas por el usuario Creadas con instrucciones def o
expresiones lambda .
Funciones integradas
Una función implementada en C (para CPython), como len o time.strftime.
Métodos incorporados
Métodos
Clases
Cuando se invoca, una clase ejecuta su método __nuevo__ para crear una instancia, luego __en él__ para
inicializarla y, finalmente, la instancia se devuelve a la persona que llama. Debido a que no hay un nuevo
operador en Python, llamar a una clase es como llamar a una función. (Por lo general, llamar a una clase crea
una instancia de la misma clase, pero otros comportamientos son posibles anulando __nuevo__. Veremos un
ejemplo de esto en “Creación flexible de objetos con __nuevo__” en la página 592).
Instancias de clase
Si una clase define un método __call__ , entonces sus instancias pueden invocarse como funciones.
Ver "Tipos invocables definidos por el usuario" en la página 145.
Funciones generadoras
Funciones o métodos que utilizan la palabra clave yield . Cuando se las llama, las funciones generadoras
devuelven un objeto generador.
Las funciones de generador son diferentes a otras llamadas en muchos aspectos. El capítulo 14 está dedicado a
ellos. También se pueden usar como corrutinas, que se tratan en el Capítulo 16.
Ahora pasamos a crear instancias de clase que funcionan como objetos invocables.
Las funciones de Python no solo son objetos reales, sino que también se pueden hacer que los objetos de Python
arbitrarios se comporten como funciones. Implementar un método de instancia __call__ es todo lo que se necesita.
El ejemplo 5-8 implementa una clase BingoCage . Una instancia se crea a partir de cualquier iterable y almacena
una lista interna de elementos en orden aleatorio. Al llamar a la instancia, aparece un elemento.
Ejemplo 5-8. bingocall.py: BingoCage hace una cosa: elige elementos de una lista barajada
importar al azar
Clase BingoCage:
def __call__(self):
return self.pick()
__init__ acepta cualquier iterable; la creación de una copia local evita efectos secundarios
inesperados en cualquier lista que se pase como argumento. Se garantiza que la reproducción
Aquí hay una demostración simple del Ejemplo 5-8. Tenga en cuenta cómo se puede invocar una instancia de
bingo como una función, y el invocable (...) incorporado lo reconoce como un objeto invocable:
>>> bingo()
0 >>>
invocable(bingo)
Verdadero
Una clase que implementa __call__ es una manera fácil de crear objetos similares a funciones que tienen algún
estado interno que debe mantenerse entre invocaciones, como los elementos restantes en BingoCage. Un
ejemplo es un decorador. Los decoradores deben ser funciones, pero a veces es conveniente poder "recordar"
algo entre las llamadas del decorador (p. ej., para memorizar, almacenar en caché los resultados de cálculos
costosos para su uso posterior).
Un enfoque totalmente diferente para crear funciones con estado interno es usar cierres.
Los cerramientos, así como los decoradores, son objeto del Capítulo 7.
Ahora pasamos a otro aspecto del manejo de funciones como objetos: la introspección en tiempo de ejecución.
Introspección de funciones
Los objetos de función tienen muchos atributos más allá de __doc__. Vea lo que revela la función dir sobre
nuestro factorial:
>>> dir(factorial)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__',
'__delattr__', '__dict__', '__dir__', '__doc__', '__eq__ ',
>>>
La mayoría de estos atributos son comunes a los objetos de Python en general. En esta sección, cubrimos
aquellos que son especialmente relevantes para tratar funciones como objetos, comenzando con __dict__.
Al igual que las instancias de una clase sencilla definida por el usuario, una función utiliza el atributo __dict__
para almacenar los atributos de usuario que se le asignan. Esto es útil como una forma primitiva de anotación.
Asignar atributos arbitrarios a funciones no es una práctica muy común en general, pero Django es un marco
que lo usa. Consulte, por ejemplo, los atributos short_description, boolean y allow_tags descritos en la
documentación del sitio de administración de Django . En los documentos de Django, este ejemplo muestra
cómo adjuntar una descripción corta a un método para determinar la descripción que aparecerá en las listas
de registros en el administrador de Django cuando se use ese método:
def upper_case_name(obj):
return ("%s %s" % (obj.first_name, obj.last_name)).upper()
upper_case_name.short_description = 'Nombre del cliente'
Ahora centrémonos en los atributos que son específicos de las funciones y que no se encuentran en un objeto
genérico definido por el usuario de Python. Calcular la diferencia de dos conjuntos rápidamente nos da una
lista de los atributos específicos de la función (vea el Ejemplo 5-9).
>>>
Usando la diferencia establecida, genere una lista ordenada de los atributos que existen en una
función pero no en una instancia de una clase simple.
__llamar__ method-wrapper Implementación del operador (); también conocido como el protocolo de objeto invocable
__cierre__ tupla El cierre de la función, es decir, enlaces para variables libres (a menudo es Ninguno)
__obtener__ method-wrapper Implementación del protocolo de descriptor de solo lectura (consulte el Capítulo 20)
__kwdefaults__ dictado Valores predeterminados para los parámetros formales de solo palabra clave
__qualname__ calle El nombre calificado de la función, por ejemplo, Random.choice (ver PEP-3155)
Una de las mejores características de las funciones de Python es el manejo de parámetros extremadamente flexible.
mecanismo, mejorado con argumentos de solo palabras clave en Python 3. Estrechamente relacionados están
el uso de * y ** para "explotar" iterables y asignaciones en argumentos separados cuando
llamamos función. Para ver estas características en acción, consulte el código del Ejemplo 5-10 y
pruebas que muestran su uso en el ejemplo 5-11.
Ejemplo 5-10. la etiqueta genera HTML; se usa un argumento de solo palabra clave cls para pasar
Atributos de "clase" como solución porque la clase es una palabra clave en Python
La función de etiqueta se puede invocar de muchas formas, como muestra el ejemplo 5-11 .
Ejemplo 5-11. Algunas de las muchas formas de llamar a la función de etiqueta del Ejemplo 5-10
>>> etiqueta('br')
'<br />'
>>> etiqueta('p', 'hola')
'<p>hola</p>'
>>> print(etiqueta('p', 'hola', 'mundo'))
<p>hola</p>
<p>mundo</p>
>>> etiqueta('p', 'hola', id=33) '<p
id="33">hola</p>'
>>> print(tag('p', 'hola', 'mundo', cls='barra lateral')) <p
class="barra lateral">hola</p>
<p class="sidebar">mundo</p>
>>> etiqueta(contenido='prueba', nombre="img")
'<img contenido="prueba" />'
>>> mi_etiqueta = {'nombre': 'img', 'título': 'Sunset Boulevard',
... 'src': 'puesta de sol.jpg', 'cls': 'enmarcado'}
>>> tag(**my_tag)
'<img class="framed" src="sunset.jpg" title="Sunset Boulevard" />'
Un solo argumento posicional produce una etiqueta vacía con ese nombre.
Cualquier número de argumentos después del primero son capturados por *content como una tupla.
Los argumentos de palabras clave que no se nombran explícitamente en la firma de la etiqueta son capturados por
**attrs como dictado .
Incluso el primer argumento posicional se puede pasar como una palabra clave cuando se llama a la etiqueta .
Prefijar el dictamen my_tag con ** pasa todos sus elementos como argumentos separados,
que luego se unen a los parámetros nombrados, con el resto capturado por
** atributos
Los argumentos de solo palabras clave son una característica nueva en Python 3. En el Ejemplo 5-10, el cls
El parámetro solo se puede dar como un argumento de palabra clave; nunca capturará datos sin nombre.
argumentos posicionales. Para especificar argumentos de solo palabras clave al definir una función,
nómbralos después del argumento con el prefijo *. Si no desea admitir variables
argumentos posicionales pero aún desea argumentos de solo palabras clave, coloque un * por sí mismo en el
firma, así:
Tenga en cuenta que los argumentos de solo palabras clave no necesitan tener un valor predeterminado:
pueden ser obligatorios, como b en el ejemplo anterior.
Pasamos ahora a la introspección de los parámetros de la función, comenzando con un ejemplo motivador
de un marco web y siguiendo con técnicas de introspección.
Ejemplo 5-12. Bobo sabe que hola requiere un argumento de persona y lo recupera de la solicitud HTTP
importar bobo
@bobo.query('/')
def hola(persona):
return '¡Hola %s!' % persona
El decorador bobo.query integra una función simple como hola con la maquinaria de manejo de solicitudes
del marco. Cubriremos a los decoradores en el Capítulo 7; ese no es el punto de este ejemplo aquí. El
punto es que Bobo introspecciona la función hola y descubre que necesita un parámetro llamado persona
para funcionar, y recuperará un parámetro con ese nombre de la solicitud y lo pasará a hola, por lo que el
programador no necesita tocar el objeto de solicitud. en absoluto.
Si instala Bobo y dirige su servidor de desarrollo al script del Ejemplo 5-12 (p. ej., bobo -f hello.py), un
acceso a la URL http://localhost:8080/ generará el mensaje "Formulario faltante". persona variable” con un
código HTTP 403. Esto sucede porque Bobo entiende que se requiere el argumento de la persona para
llamar hola, pero no se encontró ese nombre en la solicitud. El ejemplo 5-13 es una sesión de shell que
usa curl para mostrar este comportamiento.
Ejemplo 5-13. Bobo emite una respuesta prohibida 403 si faltan argumentos de función en la solicitud; curl
-i se usa para volcar los encabezados a la salida estándar
<html>
<head><title>Parámetro faltante</title></head>
<body>Persona variable de formulario faltante</body> </
html>
Sin embargo, si obtiene http://localhost:8080/?person=Jim, la respuesta será la cadena '¡Hola, Jim!'. Vea el
Ejemplo 5-14.
¡Hola Jim!
¿Cómo sabe Bobo qué nombres de parámetros requiere la función y si tienen valores predeterminados o
no?
Dentro de un objeto de función, el atributo __defaults__ contiene una tupla con los valores predeterminados
de los argumentos posicionales y de palabras clave. Los valores predeterminados para los argumentos de
solo palabras clave aparecen en __kwdefaults__. Los nombres de los argumentos, sin embargo, se
encuentran dentro del atributo __code__ , que es una referencia a un objeto de código con muchos atributos propios.
Para demostrar el uso de estos atributos, inspeccionaremos la función clip en un módulo clip.py, listado en
el Ejemplo 5-15.
Ejemplo 5-15. Función para acortar una cadena recortando en un espacio cerca de la longitud deseada
end = Ninguno
si len(texto) > max_len:
space_before = text.rfind(' ', 0, max_len) if space_before >=
0: end = space_before else: space_after = text.rfind(' ',
max_len) if space_after >= 0: end = space_after if end
es Ninguno: # no se encontraron espacios end = len(texto)
return text[:end].rstrip()
(80,)
>>> clip.__code__ # doctest: +ELLIPSIS <código
de clip de objeto en 0x...> >>>
clip.__code__.co_varnames ('texto', 'max_len',
'end', 'space_before', 'espacio_después') >>> clip.__code__.co_argcount
2
Como puede ver, este no es el arreglo de información más conveniente. Los nombres de los argumentos
aparecen en __code__.co_varnames, pero eso también incluye los nombres de las variables locales
creadas en el cuerpo de la función. Por lo tanto, los nombres de los argumentos son las primeras N
cadenas, donde N viene dado por __code__.co_argcount que, por cierto, no incluye ningún argumento
variable con el prefijo * o **. Los valores predeterminados se identifican __defaults__
solo por su posición
, por lo que
en lapara
tupla
vincular cada uno con el argumento respectivo, debe escanear del último al primero. En el ejemplo,
tenemos dos argumentos, text y max_len, y uno predeterminado, 80, por lo que debe pertenecer al último
argumento, max_len. Esto es incómodo.
Esto es mucho mejor. inspect.signature devuelve un objeto inspect.Signature , que tiene un atributo de
parámetros que le permite leer una asignación ordenada de nombres a objetos inspect.Parameter . Cada
instancia de Parámetro tiene atributos como nombre, defecto y tipo. El valor especial inspect._empty
denota parámetros sin valor predeterminado, lo que tiene sentido considerando que Ninguno es un valor
predeterminado válido y popular.
El atributo kind contiene uno de los cinco valores posibles de la clase _ParameterKind :
POSITIONAL_OR_KEYWORD
Un parámetro que se puede pasar como argumento posicional o de palabra clave (la mayoría de los
parámetros de funciones de Python son de este tipo).
VAR_POSITIONAL
Una tupla de parámetros posicionales.
VAR_KEYWORD
KEYWORD_ONLY
Un parámetro solo de palabra clave (nuevo en Python 3).
POSITIONAL_ONLY
Un parámetro solo posicional; actualmente no es compatible con la sintaxis de declaración de función de
Python, pero ejemplificado por funciones existentes implementadas en C, como divmod , que no aceptan
parámetros pasados por palabra clave.
Además de name, default y kind, los objetos inspect.Parameter tienen un atributo de anotación que normalmente
es inspect._empty pero que puede contener metadatos de firma de función proporcionados a través de la nueva
sintaxis de anotaciones en Python 3 (las anotaciones se tratan en la siguiente sección).
Ejemplo 5-18. Vincular la firma de función de la función de etiqueta en el ejemplo 5-10 a un dict de
argumentos
>>> import
inspeccionar >>> sig =
inspeccionar.firma(etiqueta) >>> mi_etiqueta = {'nombre': 'img', 'título':
... 'Sunset Boulevard', 'src': 'sunset.jpg', ' cls': 'enmarcado'}
>>> bind_args = sig.bind(**my_tag) >>>
bound_args <inspeccionar.BoundArguments
object at 0x...> >>> for name, value
inbound_args.arguments.items (): print(name, '= ', valor)
...
...
name = img
cls = framed
attrs = {'title': 'Sunset Boulevard', 'src': 'sunset.jpg'} >>> del
my_tag['name'] >>> bound_args = sig.bind(**my_tag )
Iterar sobre los elementos enbound_args.arguments, que es un OrderedDict, para mostrar los
nombres y valores de los argumentos.
Este ejemplo muestra cómo el modelo de datos de Python, con la ayuda de inspeccionar, expone la
misma maquinaria que usa el intérprete para vincular argumentos a parámetros formales en llamadas
a funciones.
Los marcos y herramientas como los IDE pueden usar esta información para validar el código. Otra
característica de Python 3, las anotaciones de funciones, mejora los posibles usos de esto, como veremos.
ver siguiente
Anotaciones de funciones
Python 3 proporciona sintaxis para adjuntar metadatos a los parámetros de una declaración de función
y su valor de retorno. El ejemplo 5-19 es una versión anotada del ejemplo 5-15. Las únicas diferencias
están en la primera línea.
Cada argumento en la declaración de la función puede tener una expresión de anotación precedida
por :. Si hay un valor predeterminado, la anotación va entre el nombre del argumento y el signo = . Para
anotar el valor devuelto, agregue -> y otra expresión entre ) y : al final de la declaración de la función.
Las expresiones pueden ser de cualquier tipo. los
los tipos más comunes que se usan en las anotaciones son clases, como str o int, o cadenas, como 'int >
0', como se ve en la anotación para max_len en el ejemplo 5-19.
El elemento con la clave 'retorno' contiene la anotación de valor de retorno marcada con -> en la
declaración de la función en el Ejemplo 5-19.
Lo único que hace Python con las anotaciones es almacenarlas en el atributo __anotaciones__ de la
función. Nada más: no se realiza ninguna comprobación, ejecución, validación ni ninguna otra acción. En
otras palabras, las anotaciones no tienen significado para el intérprete de Python. Son solo metadatos
que pueden usar herramientas, como IDE, marcos y decoradores. Al momento de escribir esto, no existen
herramientas que usen estos metadatos en la biblioteca estándar, excepto que inspect.signature() sabe
cómo extraer las anotaciones, como muestra el Ejemplo 5-20 .
La función de firma devuelve un objeto Signature , que tiene un atributo return_annotation y un diccionario
de parámetros que asigna nombres de parámetros a objetos Parameter .
Cada objeto de parámetro tiene su propio atributo de anotación . Así es como funciona el Ejemplo 5-20 .
En el futuro, marcos como Bobo podrían admitir anotaciones para automatizar aún más el procesamiento
de solicitudes. Por ejemplo, un argumento anotado como precio:flotante puede convertirse automáticamente
de una cadena de consulta al flotante esperado por la función; una anotación de cadena como cantidad:
'int > 0' podría analizarse para realizar la conversión y validación de un parámetro.
El mayor impacto de las anotaciones de funciones probablemente no sea la configuración dinámica como
Bobo, sino la provisión de información de tipo opcional para la verificación de tipo estático en herramientas
como IDE y linters.
Después de esta inmersión profunda en la anatomía de las funciones, el resto de este capítulo
cubre los paquetes más útiles de la biblioteca estándar que admiten la programación funcional.
def fact(n):
return reduce(lambda a, b: a*b, range(1, n+1))
Para evitarle la molestia de escribir funciones anónimas triviales como lambda a, b: a*b, el módulo
de operador proporciona funciones equivalentes para docenas de operadores aritméticos. Con él,
podemos reescribir el Ejemplo 5-21 como Ejemplo 5-22.
def fact(n):
return reduce(mul, range(1, n+1))
Otro grupo de lambdas de un solo truco que el operador reemplaza son funciones para seleccionar
elementos de secuencias o leer atributos de objetos: itemgetter y attrgetter en realidad crean
funciones personalizadas para hacer eso.
El ejemplo 5-23 muestra un uso común de itemgetter: ordenar una lista de tuplas por el valor de
un campo. En el ejemplo, las ciudades se imprimen ordenadas por código de país (campo 1).
Esencialmente, itemgetter(1) hace lo mismo que los campos lambda: campos[1]: crea una función
que, dada una colección, devuelve el elemento en el índice 1.
Ejemplo 5-23. Demostración de itemgetter para ordenar una lista de tuplas (datos del Ejemplo 2-8)
>>> metro_data =
... [ ('Tokio', 'JP', 36.933, (35.689722, 139.691667)), ('Delhi
... NCR', 'IN', 21.935, (28.613889, 77.208889)), ('Ciudad de
... México', ' MX', 20.142, (19.433333, -99.133333)), ('Nueva York-
... Newark', 'US', 20.104, (40.808611, -74.020386)), ('Sao Paulo', 'BR',
... 19.649, (-23.547778 , -46.635833)),
... ]
>>>
>>> from operator import itemgetter >>>
for city in sorted(metro_data, key=itemgetter(1)): print(city)
...
...
('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833))
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889))
('Tokio', 'JP', 36.933, (35.689722, 139.691667))
('Ciudad de México', 'MX', 20.142, (19.433333, -99.133333))
('Nueva York-Newark', 'EE. UU.', 20.104, (40.808611, -74.020386))
Si pasa múltiples argumentos de índice a itemgetter, la función que crea devolverá tuplas con los valores
extraídos:
Debido a que itemgetter usa el operador [] , admite no solo secuencias, sino también asignaciones y
cualquier clase que implemente __getitem__.
Un hermano de itemgetter es attrgetter, que crea funciones para extraer atributos de objetos por nombre.
Si le pasa a attrgetter varios nombres de atributos como argumentos, también devuelve una tupla de
valores. Además, si algún nombre de argumento contiene un . (punto), attrget ter navega a través de
objetos anidados para recuperar el atributo. Estos comportamientos se muestran en el ejemplo 5-24. Esta
no es la sesión de consola más corta porque necesitamos construir una estructura anidada para mostrar
el manejo de atributos punteados por attrgetter.
Ejemplo 5-24. Demostración de attrgetter para procesar una lista previamente definida de tuplas
nombradas llamada metro_data (la misma lista que aparece en el Ejemplo 5-23)
Cree la lista metro_areas con instancias de Metropolis ; tenga en cuenta el desempaquetado de la tupla
anidada para extraer (lat, long) y utilícelos para construir el LatLong para el atributo coord de Metropolis.
Aquí hay una lista parcial de funciones definidas en operator (nombres que comienzan con _ se omiten,
porque en su mayoría son detalles de implementación):
La mayoría de los 52 nombres enumerados son evidentes. El grupo de nombres con el prefijo i
y el nombre de otro operador, por ejemplo, iadd, iand , etc., corresponden a los operadores de
asignación aumentada, por ejemplo, +=, &=, etc. Estos cambian su primer argumento en su lugar, si
es mutable; si no, la función funciona como la que no tiene el prefijo i : simplemente devuelve el resultado de
la operación.
De las funciones de operador restantes, methodcaller es la última que cubriremos. Es algo similar a attrgetter
y itemgetter en que crea una función sobre la marcha. La función que crea llama a un método por su nombre
en el objeto dado como argumento, como se muestra en el Ejemplo 5-25.
Ejemplo 5-25. Demostración de methodcaller: la segunda prueba muestra el enlace de argumentos adicionales
'EL MOMENTO HA
LLEGADO' >>> hipenato = llamadométodo('reemplazar', ' ', '-') >>>
hipenato(s)
'El tiempo ha llegado'
La primera prueba en el ejemplo 5-25 está ahí solo para mostrar el método llamador en funcionamiento, pero
si necesita usar str.upper como una función, puede simplemente llamarlo en la clase str y pasar una cadena
como argumento, así:
>>> str.superior(es)
'EL TIEMPO HA LLEGADO'
La segunda prueba en el ejemplo 5-25 muestra que methodcaller también puede hacer una aplicación parcial
para congelar algunos argumentos, como lo hace la función functools.partial . Ese es nuestro próximo tema.
functools reúne un puñado de funciones de orden superior. La más conocida de ellas es probablemente reduce,
que se trató en “Reemplazos modernos para mapear, filtrar y reducir” en la página 142. De las funciones
restantes en functools, la más útil es parcial y su variación, método parcial.
functools.partial es una función de orden superior que permite la aplicación parcial de una función. Dada una
función, una aplicación parcial produce un nuevo invocable con algunos de los argumentos de la función
original fijos. Esto es útil para adaptar una función que toma uno o más argumentos a una API que requiere
una devolución de llamada con menos argumentos.
El ejemplo 5-26 es una demostración trivial.
Ejemplo 5-26. Usar parcial para usar una función de dos argumentos donde se requiere un invocable de un
argumento
>>> triple(7) 21
Cree una nueva función triple a partir de mul, vinculando el primer argumento posicional a 3.
Pruébalo.
Utilice triple con mapa; mul no funcionaría con map en este ejemplo.
Un ejemplo más útil involucra la función unicode.normalize que vimos en "Normalizar Unicode para comparaciones más
sanas" en la página 117. Si trabaja con texto de muchos idiomas, puede aplicar unicode.normalize('NFC', s ) a cualquier
cadena antes de compararla o almacenarla. Si lo hace con frecuencia, es útil tener una función nfc para hacerlo, como en
el Ejemplo 5-27.
Ejemplo 5-27. Construyendo una conveniente función de normalización de Unicode con parcial
>>> s2 = 'café\u0301'
>>> s1, s2 ('café', 'café')
>>> s1 == s2
Falso
>>> nfc(s1) == nfc(s2)
Verdadero
parcial toma un invocable como primer argumento, seguido de un número arbitrario de argumentos posicionales y de
palabras clave para enlazar.
El ejemplo 5-28 muestra el uso de parcial con la función de etiqueta del ejemplo 5-10, para congelar un argumento
posicional y un argumento de palabra clave.
Ejemplo 5-28. Demostración de aplicación parcial a la etiqueta de función del Ejemplo 5-10
>>> imagen.palabras
clave {'cls': 'pic-frame'}
Cree una función de imagen a partir de la etiqueta fijando el primer argumento posicional
con 'img' y el argumento de la palabra clave cls con 'pic-frame'. la imagen funciona como
tiene atributos que brindan acceso a la función original y los argumentos fijos.
La función functools.partialmethod (nueva en Python 3.4) hace el mismo trabajo que la función
parcial, pero está diseñada para funcionar con métodos.
Una función de functools impresionante es lru_cache, que realiza la memorización, una forma de
optimización automática que funciona almacenando los resultados de las llamadas a funciones
para evitar recálculos costosos. Lo cubriremos en el Capítulo 7, donde se explican los decoradores,
junto con otras funciones de orden superior diseñadas para usarse como decoradores: singledis
patch y wraps.
Los callables vienen en siete sabores diferentes en Python, desde las funciones simples creadas
con lambda hasta instancias de clases que implementan __call__. Todos ellos pueden ser
detectados por el callable() incorporado. Cada invocable admite la misma sintaxis enriquecida para
declarar parámetros formales, incluidos parámetros y anotaciones solo de palabras clave, ambas
características nuevas introducidas con Python 3.
2. El código fuente de functools.py revela que la clase functools.partial está implementada en C y se usa de forma
predeterminada. Si eso no está disponible, una implementación de Python puro de parcial está disponible desde
Python 3.4.en el módulo functools.
Las funciones de Python y sus anotaciones tienen un amplio conjunto de atributos que se pueden leer
con la ayuda del módulo de inspección , que incluye el método Signature.bind para aplicar las reglas
flexibles que utiliza Python para vincular argumentos reales a parámetros declarados.
ters.
Por último, cubrimos algunas funciones del módulo operator y functools.parti al, que facilitan la
programación funcional al minimizar la necesidad de la sintaxis lambda funcionalmente desafiada.
Otras lecturas
Los siguientes dos capítulos continúan nuestra exploración de la programación con objetos de función.
El Capítulo 6 muestra cómo las funciones de primera clase pueden simplificar algunos patrones de diseño
clásicos orientados a objetos, mientras que el Capítulo 7 se sumerge en los decoradores de funciones, un
tipo especial de función de orden superior, y el mecanismo de cierre que los hace funcionar.
El capítulo 7 de Python Cookbook, tercera edición (O'Reilly), de David Beazley y Brian K. Jones, es un
complemento excelente para el capítulo actual y el capítulo 7 de este libro, ya que cubre principalmente
los mismos conceptos con un enfoque diferente. .
En Referencia del lenguaje Python, “3.2. La jerarquía de tipos estándar presenta los siete tipos a los que
se puede llamar, junto con todos los demás tipos integrados.
Las funciones exclusivas de Python-3 analizadas en este capítulo tienen sus propios PEP: PEP 3102 —
Argumentos de solo palabras clave y PEP 3107 — Anotaciones de función.
Para obtener más información sobre el uso actual (a partir de mediados de 2014) de las anotaciones, vale
la pena leer dos preguntas de Stack Overflow: "¿Cuáles son los buenos usos para las 'anotaciones de
función' de Python3?" tiene una respuesta práctica y comentarios perspicaces de Raymond Hettinger, y
la respuesta a "¿De qué sirven las anotaciones de funciones de Python?" citas extensas de Guido van
Rossum.
Vale la pena leer PEP 362 — Function Signature Object si tiene la intención de usar el módulo de
inspección que implementa esa característica.
fn.py es un paquete para admitir la programación funcional en Python 2 y 3. Según su autor, Alexey
Kachayev, fn.py proporciona "implementación de funciones faltantes para disfrutar de FP" en Python.
Incluye un decorador @recur.tco que implementa la optimización de llamadas de cola para recursividad
ilimitada en Python, entre muchas otras funciones, estructuras de datos y recetas.
Bobo de Jim Fulton fue probablemente el primer marco web que mereció ser llamado orientado
a objetos. Si estaba intrigado por él y quiere aprender más sobre su reescritura moderna,
comience en su Introducción. Un poco de la historia temprana de Bobo aparece en un
comentario de Phillip J. Eby en una discusión en el blog de Joel Spolsky.
Plataforma improvisada
Sobre Bobo
Le debo mi carrera en Python a Bobo. Lo usé en mi primer proyecto web de Python en 1998. Descubrí
Bobo mientras buscaba una forma orientada a objetos de codificar aplicaciones web, después de
probar las alternativas de Perl y Java.
En 1997, Bobo había sido pionero en el concepto de publicación de objetos: mapeo directo de URL a
una jerarquía de objetos, sin necesidad de configurar rutas. Me enganché cuando vi la belleza de esto.
Bobo también presentó el manejo automático de consultas HTTP basado en el análisis de las firmas
de los métodos o funciones utilizadas para manejar las solicitudes.
Bobo fue creado por Jim Fulton, conocido como "El Papa Zope" gracias a su papel principal en el
desarrollo del marco Zope, la base de Plone CMS, SchoolÿTool, ERP5 y otros proyectos Python a gran
escala. Jim también es el creador de ZODB, la base de datos de objetos de Zope, una base de datos
de objetos transaccionales que proporciona ACID (atomicidad, consistencia, aislamiento y durabilidad),
diseñada para facilitar su uso desde Python.
Desde entonces, Jim ha reescrito Bobo desde cero para admitir WSGI y Python moderno (incluido
Python 3). En el momento de escribir este artículo, Bobo usa la biblioteca six para realizar la
introspección de funciones, a fin de ser compatible con Python 2 y Python 3 a pesar de los cambios en
los objetos de función y las API relacionadas.
Alrededor del año 2000, estaba en un entrenamiento en los Estados Unidos cuando Guido van Rossum
pasó por el salón de clases (él no era el instructor). En la sesión de preguntas y respuestas que siguió,
alguien le preguntó qué características de Python se tomaron prestadas de otros lenguajes. Su
respuesta: “Todo lo que es bueno en Python fue robado de otros lenguajes”.
Los “paradigmas” del lenguaje de programación son un legado moribundo y tedioso de una era pasada.
Los diseñadores de lenguajes modernos no les respetan, así que, ¿por qué nuestros cursos se adhieren
servilmente a ellos?
¿Qué más hacer con un lenguaje como Python, Ruby o Perl? Sus diseñadores no tienen paciencia para las
sutilezas de estas jerarquías linneanas; toman prestadas las características que desean, creando mezclas
que desafían por completo la caracterización.
Krishnamurthi sostiene que en lugar de tratar de clasificar las lenguas en alguna taxonomía, es
más útil considerarlas como agregados de características.
Incluso si no era el objetivo de Guido, dotar a Python de funciones de primera clase abrió la puerta
a la programación funcional. En su publicación “Orígenes de las características funcionales de
Python ”, dice que mapear, filtrar y reducir fueron la motivación para agregar lambda a Python en
primer lugar. Todas estas características fueron aportadas juntas por Amrit Prem para Python 1.0
en 1994 (según Misc/HISTORY en el código fuente de CPython).
lambda, map, filter y reduce aparecieron por primera vez en Lisp, el lenguaje funcional original.
Sin embargo, Lisp no limita lo que se puede hacer dentro de una lambda, porque todo en Lisp es
una expresión. Python usa una sintaxis orientada a declaraciones en la que las expresiones no
pueden contener declaraciones, y muchas construcciones del lenguaje son declaraciones, incluido
try/catch, que es lo que extraño con más frecuencia cuando escribo lambdas. Este es el precio a
pagar por la sintaxis altamente legible de Python.3 Lisp tiene muchas fortalezas, pero la legibilidad
no es una de ellas.
Además de la sintaxis de función anónima limitada, el mayor obstáculo para una adopción más
amplia de lenguajes de programación funcional en Python es la falta de eliminación de la
recursividad de cola, una optimización que permite el cálculo eficiente de la memoria de una
función que realiza una llamada recursiva en la "cola" de su cuerpo En otra publicación de blog,
“Eliminación de recurrencia de cola”, Guido da varias razones por las que tal optimización no es adecuada para Python.
Esa publicación es una gran lectura por los argumentos técnicos, pero aún más porque las tres
primeras y más importantes razones dadas son problemas de usabilidad. No es casualidad que
Python sea un placer de usar, aprender y enseñar. Guido lo hizo así.
Así que ahí lo tienes: Python es, por diseño, no un lenguaje funcional, lo que sea que eso signifique.
Python solo toma prestadas algunas buenas ideas de los lenguajes funcionales.
allá de las restricciones de sintaxis específicas de Python, las funciones anónimas tienen un serio
inconveniente en todos los idiomas: no tienen nombre.
Solo estoy medio bromeando aquí. Los seguimientos de pila son más fáciles de leer cuando las funciones tienen nombres.
Las funciones anónimas son un atajo útil, las personas se divierten codificando con ellas, pero a
veces se dejan llevar, especialmente si el lenguaje y el entorno fomentan el anidamiento profundo
de funciones anónimas, como JavaScript en Node.js. Muchas funciones anónimas anidadas
dificultan la depuración y el manejo de errores. Programación asíncrona
3. También existe el problema de la sangría perdida al pegar código en los foros web, pero estoy divagando.
en Python está más estructurado, quizás porque la lambda limitada lo exige. Prometo escribir más
sobre la programación asíncrona en el futuro, pero este tema debe posponerse al Capítulo 18. Por
cierto, las promesas, los futuros y los diferidos son conceptos que se utilizan en las API asíncronas
modernas. Junto con las rutinas, proporcionan un escape del llamado "infierno de devolución de
llamada". Veremos cómo funciona la programación asincrónica sin devolución de llamada en “De
las devoluciones de llamada a futuros y corrutinas” en la página 562.
CAPÍTULO 6
—Ralph Johnson
Coautor del clásico Design Patterns
Aunque los patrones de diseño son independientes del idioma, eso no significa que todos los
patrones se apliquen a todos los idiomas. En su presentación de 1996, “Patrones de diseño en
lenguajes dinámicos”, Peter Norvig afirma que 16 de los 23 patrones en el libro original de patrones
de diseño de Gamma et al. volverse "invisible o más simple" en un lenguaje dinámico (diapositiva
9). Estaba hablando de Lisp y Dylan, pero muchas de las características dinámicas relevantes
también están presentes en Python.
La elección del lenguaje de programación es importante porque influye en el punto de vista de uno.
Nuestros patrones asumen características de lenguaje de nivel Smalltalk/C++, y esa elección determina
qué se puede y qué no se puede implementar fácilmente. Si asumimos lenguajes procedimentales,
podríamos haber incluido patrones de diseño llamados "Herencia", "Encapsulación" y "Polimorfismo".
De manera similar, algunos de nuestros patrones son compatibles directamente con los lenguajes
orientados a objetos menos comunes. CLOS tiene múltiples métodos, por ejemplo, que reducen la
necesidad de un patrón como Visitor.2 En particular, en el contexto de lenguajes con funciones de
primera clase, Norvig sugiere repensar los patrones de estrategia, comando, método de plantilla y visitante. La idea
general es: puede reemplazar instancias de alguna clase participante en estos patrones con funciones simples,
reduciendo una gran cantidad de código repetitivo. En este capítulo, refactorizaremos la estrategia
1. De una diapositiva en la charla "Análisis de la causa raíz de algunas fallas en patrones de diseño", presentado por Ralph Johnson
en IME/CCSL, Universidade de São Paulo, 15 de noviembre de 2014.
2. Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides, Design Patterns: Elements of Reusable
Software orientado a objetos (Addison-Wesley, 1995), pág. 4.
167
Machine Translated by Google
usando objetos de función, y discuta un enfoque similar para simplificar el patrón Command.
La estrategia es un buen ejemplo de un patrón de diseño que puede ser más simple en Python si aprovecha
las funciones como objetos de primera clase. En la siguiente sección, describimos e implementamos la
estrategia utilizando la estructura "clásica" descrita en Patrones de diseño. Si está familiarizado con el patrón
clásico, puede saltar a "Estrategia orientada a funciones" en la página 172 , donde refactorizamos el código
usando funciones, lo que reduce significativamente el número de líneas.
Estrategia clásica El
diagrama de clases UML de la figura 6-1 muestra una disposición de clases que ejemplifica el patrón de
estrategia.
Figura 6-1. Diagrama de clase UML para el procesamiento de descuento de pedidos implementado con el
patrón de diseño de estrategia
Un claro ejemplo de Estrategia aplicada en el ámbito del comercio electrónico es el cálculo de descuentos
a los pedidos según los atributos del cliente o inspección de los artículos pedidos.
• Clientes con 1.000 o más puntos de fidelidad obtienen un 5% de descuento global por pedido. • Se
aplica un 10% de descuento a cada línea de pedido con 20 o más unidades en el mismo pedido. • Los
El diagrama de clases UML para el patrón de estrategia se muestra en la figura 6-1. sus participantes
son:
Contexto
Proporciona un servicio al delegar algunos cálculos a componentes intercambiables que implementan
algoritmos alternativos. En el ejemplo de comercio electrónico, el contexto es un Pedido, que está
configurado para aplicar un descuento promocional según uno de varios algoritmos.
Estrategia
La interfaz común a los componentes que implementan los diferentes algoritmos.
En nuestro ejemplo, este rol lo desempeña una clase abstracta llamada Promoción.
Estrategia concreta
Una de las subclases concretas de Estrategia. FidelityPromo, BulkPromo y Large OrderPromo son
las tres estrategias concretas implementadas.
El código del ejemplo 6-1 sigue el esquema de la figura 6-1. Como se describe en Patrones de diseño, la
estrategia concreta la elige el cliente de la clase de contexto. En nuestro ejemplo, antes de instanciar un
pedido, el sistema de alguna manera seleccionaría una estrategia de descuento promocional y la pasaría
al constructor de pedidos . La selección de la estrategia está fuera del alcance del patrón.
def debido(auto): si
auto.promocion es Ninguno: descuento
= 0 otro: descuento =
auto.promocion.descuento(auto)
return auto.total() - descuento
@abstractmethod def
discount(self, order):
"""Descuento de devolución como cantidad positiva en dólares"""
Tenga en cuenta que en el Ejemplo 6-1, codifiqué Promoción como una clase base abstracta (ABC), para poder
para usar el decorador @abstractmethod , lo que hace que el patrón sea más explícito.
El ejemplo 6-2 muestra doctests utilizados para demostrar y verificar el funcionamiento de un módulo
implementar las reglas descritas anteriormente.
Ejemplo 6-2. Ejemplo de uso de la clase Order con diferentes promociones aplicadas
El ejemplo 6-1 funciona perfectamente bien, pero se puede implementar la misma funcionalidad con
menos código en Python usando funciones como objetos. La siguiente sección muestra cómo.
estrategia concreta del ejemplo 6-1 es una clase con un solo método, el descuento. Además, las
instancias de estrategia no tienen estado (sin atributos de instancia). Se podría decir que se parecen
mucho a las funciones simples y estaría en lo cierto. El ejemplo 6-3 es una refactorización del ejemplo
6-1, reemplazando las estrategias concretas con funciones simples y eliminando la clase abstracta Promo .
Ejemplo 6-3. Clase de orden con estrategias de descuento implementadas como funciones
def total(self):
return self.price * self.quantity
def total(self): if
not hasattr(self, '__total'): self.__total
= sum(item.total() for item in self.cart) return self.__total
def debido(auto):
si self.promotion es None:
descuento = 0 else:
descuento = self.promotion(self)
volver self.total() - descuento
def fidelidad_promo(pedido):
"""5% de descuento para clientes con 1000 o más puntos de fidelidad""" return
order.total() * .05 if order.customer.fidelity >= 1000 else 0
def bulk_item_promo(pedido):
"""10% de descuento por cada LineItem con 20 o más unidades"""
descuento = 0 para artículo en order.cart:
def gran_pedido_promo(pedido):
"""7% de descuento para pedidos con 10 o más artículos distintos"""
distintos_artículos = {artículo.producto para artículo en pedido.carrito} si
len(artículos distintos) >= 10:
devolver pedido.total() * .07
volver 0
El código del Ejemplo 6-3 es 12 líneas más corto que el del Ejemplo 6-1. El uso de la nueva Orden también es un poco
más simple, como se muestra en las pruebas documentales del Ejemplo 6-4 .
Ejemplo 6-4. Ejemplo de uso de la clase Order con promociones como funciones
Para aplicar una estrategia de descuento a un pedido, simplemente pase la función de promoción como
argumento.
Tenga en cuenta las llamadas en el Ejemplo 6-4: no es necesario crear una instancia de un nuevo objeto de
promoción con cada nuevo pedido: las funciones están listas para usar.
Es interesante notar que en Design Patterns los autores sugieren: "Los objetos de estrategia a menudo son
buenos pesos mosca". 3 Una definición del peso mosca en otra parte de ese trabajo dice: "Un peso mosca es
un objeto compartido que puede usarse en múltiples simultáneamente.”4 Se recomienda compartir para reducir
el costo de crear un nuevo objeto de estrategia concreto cuando la misma estrategia se aplica una y otra vez
con cada nuevo contexto, con cada nueva instancia de Orden , en nuestro ejemplo. Por lo tanto, para superar
un inconveniente del patrón de estrategia, su costo de tiempo de ejecución, los autores recomiendan aplicar otro
patrón más. Mientras tanto, el recuento de líneas y el costo de mantenimiento de su código se acumulan.
Un caso de uso más espinoso, con estrategias concretas complejas que mantienen un estado interno, puede
requerir que se combinen todas las piezas de los patrones de diseño de estrategia y peso ligero. Pero a menudo
las estrategias concretas no tienen un estado interno; solo tratan con datos del contexto. Si ese es el caso,
entonces, por todos los medios, use funciones simples y antiguas en lugar de codificar clases de método único
que implementen una interfaz de método único declarada en otra clase. Una función es más liviana que una
instancia de una clase definida por el usuario, y no hay necesidad de Flyweight porque Python crea cada función
de estrategia solo una vez cuando compila el módulo. Una función simple también es "un objeto compartido que
se puede usar en múltiples contextos simultáneamente".
Ahora que hemos implementado el patrón Estrategia con funciones, surgen otras posibilidades. Supongamos
que desea crear una "metaestrategia" que seleccione el mejor descuento disponible para un pedido determinado.
En las siguientes secciones, presentamos refactorizaciones adicionales
que implementan este requisito utilizando una variedad de enfoques que aprovechan funciones y módulos
como objetos.
clientes y carritos de compras de las pruebas del ejemplo 6-4, ahora agregamos tres pruebas adicionales en
el ejemplo 6-5.
Ejemplo 6-5. La función best_promo aplica todos los descuentos y devuelve el mayor
Al realizar el pago con un carrito simple, best_promo le dio al cliente leal a Ann el descuento por
fidelity_promo.
Ejemplo 6-6. best_promo encuentra el descuento máximo iterando sobre una lista de funciones
def mejor_promo(pedido):
"""Seleccione el mejor descuento disponible
"""
best_promo toma una instancia de Order como argumento, al igual que las otras funciones *_promo .
Usando una expresión generadora, aplicamos cada una de las funciones de promos al pedido y
devolvemos el descuento máximo calculado.
El ejemplo 6-6 es sencillo: promos es una lista de funciones. Una vez que te acostumbras a la idea de que las
funciones son objetos de primera clase, se deduce naturalmente que la construcción de estructuras de datos
que contienen funciones a menudo tiene sentido.
Aunque el Ejemplo 6-6 funciona y es fácil de leer, hay cierta duplicación que podría generar un error sutil: para
agregar una nueva estrategia de promoción, necesitamos codificar la función y
recuerde agregarlo a la lista de promociones , de lo contrario, la nueva promoción funcionará cuando se pase explícitamente
en Python también son objetos de primera clase y la biblioteca estándar proporciona varias funciones para manejarlos. Los
globales incorporados se describen de la siguiente manera en los documentos de Python:
globales()
Devuelve un diccionario que representa la tabla de símbolos global actual. Este es siempre el diccionario del módulo
actual (dentro de una función o método, este es el módulo donde se define, no el módulo desde el que se llama).
El ejemplo 6-7 es una forma un tanto chapucera de usar globales para ayudar a best_promo a encontrar automáticamente
las otras funciones *_promo disponibles .
Ejemplo 6-7. La lista de promociones se crea mediante la introspección del espacio de nombres global del módulo.
def mejor_promo(pedido):
"""Seleccione el mejor descuento disponible
"""
return max(promo(order) for promo in promos)
Otra forma de recopilar las promociones disponibles sería crear un módulo y poner allí todas las funciones de estrategia,
excepto best_promo.
En el ejemplo 6-8, el único cambio significativo es que la lista de funciones de estrategia se construye mediante la
introspección de un módulo separado llamado promociones. Tenga en cuenta que el Ejemplo 6-8 depende de la importación
del módulo de promociones así como de la inspección, que proporciona funciones de introspección de alto nivel (las
importaciones no se muestran por brevedad, porque normalmente estarían en la parte superior del archivo).
Ejemplo 6-8. La lista de promociones se construye mediante la introspección de un nuevo módulo de promociones.
def mejor_promo(pedido):
"""Seleccione el mejor descuento disponible
"""
La función inspect.getmembers devuelve los atributos de un objeto, en este caso, el módulo de promociones ,
opcionalmente filtrado por un predicado (una función booleana). Usamos inspect.isfunction para obtener solo las funciones
del módulo.
El ejemplo 6-8 funciona independientemente de los nombres que se le den a las funciones; lo único que importa es que
el módulo de promociones contiene solo funciones que calculan los descuentos dados a los pedidos. Por supuesto, esta
es una suposición implícita del código. Si alguien creara una función con una firma diferente en el módulo de promociones ,
Podríamos agregar pruebas más estrictas para filtrar las funciones, por ejemplo, inspeccionando sus argumentos. El
objetivo del ejemplo 6-8 no es ofrecer una solución completa, sino resaltar un posible uso de la introspección de módulos.
Una alternativa más explícita para la recopilación dinámica de funciones de descuento promocional sería utilizar un
decorador simple. Mostraremos otra versión más de nuestro ejemplo de estrategia de comercio electrónico en el Capítulo
7, que trata sobre los decoradores de funciones.
En la siguiente sección, analizamos Command, otro patrón de diseño que a veces se implementa a través de clases de
método único cuando lo harían las funciones simples.
Dominio
El comando es otro patrón de diseño que se puede simplificar mediante el uso de funciones pasadas como argumentos.
La Figura 6-2 muestra la disposición de las clases en el patrón Comando.
Comando | 177
Machine Translated by Google
Figura 6-2. Diagrama de clase UML para editor de texto basado en menús implementado con el patrón de
diseño Command. Cada comando puede tener un receptor diferente: el objeto que implementa la acción.
Para PasteCommand, el receptor es el Documento. Para Openÿ Command, el receptor es la aplicación.
El objetivo de Command es desacoplar un objeto que invoca una operación (el invocador) del objeto
proveedor que la implementa (el receptor). En el ejemplo de Design Patterns, cada invocador es un elemento
de menú en una aplicación gráfica y los receptores son el documento que se está editando o la propia
aplicación.
La idea es poner un objeto Comando entre los dos, implementando una interfaz con un solo método,
ejecutar, que llama a algún método en el Receptor para realizar la operación deseada. De esa forma, el
Invocador no necesita conocer la interfaz del Receptor, y se pueden adaptar diferentes receptores a través
de diferentes subclases de Comando . El Invocador se configura con un comando concreto y llama a su
método de ejecución para operarlo. Observe en la Figura 6-2 que MacroCommand puede almacenar una
secuencia de comandos; su método execute() llama al mismo método en cada comando almacenado.
Citando a Gamma et al., "Los comandos son un reemplazo orientado a objetos para las devoluciones de
llamada". La pregunta es: ¿necesitamos un reemplazo orientado a objetos para las devoluciones de llamadas?
A veces sí, pero no siempre.
En lugar de darle al invocador una instancia de comando , simplemente podemos darle una función.
En lugar de llamar a command.execute(), el Invocador puede simplemente llamar a command(). El comando
Macro se puede implementar con una clase que implemente __call__. Las instancias de Macro Command
serían invocables, cada una con una lista de funciones para invocaciones futuras, como se implementó en
el Ejemplo 6-9.
Ejemplo 6-9. Cada instancia de MacroCommand tiene una lista interna de comandos
macrocomando de clase :
"""Un comando que ejecuta una lista de comandos"""
La creación de una lista a partir de los argumentos de los comandos garantiza que sea iterable y
mantiene una copia local de las referencias de los comandos en cada instancia de MacroCommand .
Cuando se invoca una instancia de MacroCommand , cada comando en self.com mands se llama en
secuencia.
Los usos más avanzados del patrón Comando (por ejemplo, para admitir la función de deshacer) pueden
requerir más que una simple función de devolución de llamada. Incluso entonces, Python ofrece un par de
alternativas que merecen consideración:
• Una instancia invocable como MacroCommand en el ejemplo 6-9 puede mantener cualquier estado
necesario, y proporcione métodos adicionales además de __call__.
• Se puede usar un cierre para mantener el estado interno de una función entre llamadas.
Esto concluye nuestro replanteamiento del patrón Command con funciones de primera clase. En un alto nivel,
el enfoque aquí fue similar al que aplicamos a Estrategia: reemplazar con invocables las instancias de una clase
participante que implementó una interfaz de método único. Después de todo, cada Python invocable implementa
una interfaz de método único, y ese método se llama __call__.
De la misma charla citada al comienzo de este capítulo, al reflexionar sobre el 20º aniversario de Design
Patterns: Elements of Reusable Object-Oriented Software, Ralph Johnson ha declarado que una de las fallas
del libro es "Demasiado énfasis en patrones
como puntos finales en lugar de pasos en los patrones de diseño.”5 En este capítulo, usamos el patrón
de Estrategia como punto de partida: una solución de trabajo que podríamos simplificar usando funciones
de primera clase.
En muchos casos, las funciones o los objetos a los que se puede llamar proporcionan una forma más
natural de implementar devoluciones de llamada en Python que imitar los patrones de Estrategia o
Comando, tal como lo describen Gamma, Helm, Johnson y Vlissides. La refactorización de Estrategia y
la discusión de Comando en este capítulo son ejemplos de una visión más general: a veces puede
encontrar un patrón de diseño o una API que requiere que los componentes implementen una interfaz
con un solo método, y ese método tiene un aspecto genérico. nombre como "ejecutar", "ejecutar" o
"hacerlo". Dichos patrones o API a menudo se pueden implementar con menos código repetitivo en
Python utilizando funciones de primera clase u otras llamadas.
El mensaje de las diapositivas de patrones de diseño de Peter Norvig es que los patrones de comando
y estrategia, junto con el método de plantilla y el visitante, se pueden hacer más simples o incluso
"invisibles" con funciones de primera clase, al menos para algunas aplicaciones de estos patrones.
Otras lecturas
Nuestra discusión sobre la estrategia terminó con una sugerencia de que los decoradores de funciones
podrían usarse para mejorar el ejemplo 6-8. También mencionamos el uso de cierres un par de veces en
este capítulo. Los decoradores y los cierres son el tema central del Capítulo 7. Ese capítulo comienza
con una refactorización del ejemplo de comercio electrónico utilizando un decorador para registrar las
promociones disponibles.
“Receta 8.21. Implementing the Visitor Pattern”, en Python Cookbook, Third Edition (O'Reilly), de David
Beazley y Brian K. Jones, presenta una implementación elegante del patrón Visitor en el que una clase
NodeVisitor maneja los métodos como ob de primera clase. ÿ proyectos.
En el tema general de los patrones de diseño, la elección de lecturas para el programador de Python no
es tan amplia como la que está disponible para otras comunidades lingüísticas.
Hasta donde sé, Learning Python Design Patterns, de Gennadiy Zlobin (Packt), es el único libro dedicado
por completo a los patrones en Python, en junio de 2014. Pero el trabajo de Zlobin es bastante corto
(100 páginas) y cubre ocho de los originales. 23 patrones de diseño.
Programación experta en Python de Tarek Ziadé (Packt) es uno de los mejores libros de Python de nivel
intermedio del mercado, y su último capítulo, "Patrones de diseño útiles", presenta siete de los patrones
clásicos desde una perspectiva pitónica.
5. De la misma charla citada al comienzo de este capítulo: “Análisis de la causa raíz de algunas fallas en los patrones de diseño”,
presentado por Johnson en IME-USP, 15 de noviembre de 2014.
Alex Martelli ha dado varias charlas sobre Python Design Patterns. Hay un video de su presentación en
EuroPython 2011 y un conjunto de diapositivas en su sitio web personal. He encontrado diferentes
diapositivas y videos a lo largo de los años, de diferentes longitudes, por lo que vale la pena hacer una
búsqueda exhaustiva de su nombre con las palabras "Patrones de diseño de Python".
Alrededor de 2008, Bruce Eckel, autor del excelente Thinking in Java (Prentice Hall), comenzó un libro
titulado Python 3 Patterns, Recipes and Idioms. Iba a ser escrito por una comunidad de colaboradores
liderada por Eckel, pero seis años después todavía está incompleto y aparentemente estancado
(mientras escribo esto, el último cambio en el repositorio tiene dos años).
Hay muchos libros sobre patrones de diseño en el contexto de Java, pero entre ellos el que más me
gusta es Head First Design Patterns de Eric Freeman, Bert Bates, Kathy Sierra y Elisabeth Robson
(O'Reilly). Explica 16 de los 23 patrones clásicos. Si te gusta el estilo alocado de la serie Head First y
necesitas una introducción a este tema, te encantará ese trabajo. Sin embargo, está centrado en Java.
Para una nueva mirada a los patrones desde el punto de vista de un lenguaje dinámico con digitación
pato y funciones de primera clase, Design Patterns in Ruby de Russ Olsen (Addison Wesley) tiene
muchas ideas que también son aplicables a Python. A pesar de las muchas diferencias sintácticas, a
nivel semántico Python y Ruby están más cerca entre sí que Java o C++.
En Patrones de diseño en lenguajes dinámicos (diapositivas), Peter Norvig muestra cómo las funciones
de primera clase (y otras características dinámicas) hacen que varios de los patrones de diseño
originales sean más simples o innecesarios.
Por supuesto, el libro Design Patterns original de Gamma et al. es de lectura obligatoria si te tomas en
serio este tema. La Introducción por sí sola vale el precio. Esa es la fuente de los principios de diseño
citados a menudo “Programa para una interfaz, no una implementación” y “Favorecer la composición de
objetos sobre la herencia de clases”.
Soapbox
Python tiene funciones y tipos de primera clase, características que, según Norvig, afectan a 10 de
los 23 patrones (diapositiva 10 de Patrones de diseño en lenguajes dinámicos). En el próximo
capítulo, veremos que Python también tiene funciones genéricas (“Funciones genéricas con envío
único” en la página 202), similares a los multimétodos CLOS que Gamma et al. sugerir como una
forma más sencilla de implementar el clásico patrón Visitor. Norvig, por otro lado, dice que los
métodos múltiples simplifican el patrón Builder (diapositiva 10). Hacer coincidir los patrones de
diseño con las características del idioma no es una ciencia exacta.
En las aulas de todo el mundo, los patrones de diseño se enseñan con frecuencia utilizando
ejemplos de Java. Escuché a más de un estudiante afirmar que se les hizo creer que los patrones
de diseño originales son útiles en cualquier lenguaje de implementación. Resulta que los 23
patrones "clásicos" de Gamma et al. El libro se aplica muy bien a Java "clásico", a pesar de que
originalmente se presentó principalmente en el contexto de C++; algunos tienen versiones de Smalltalk.
muestras en el libro. Pero eso no significa que todos esos patrones se apliquen igualmente bien en
cualquier idioma. Los autores son explícitos desde el principio de su libro en que “algunos de nuestros
patrones están respaldados directamente por los lenguajes orientados a objetos menos
comunes” (recuerde la cita completa en la primera página de este capítulo).
La bibliografía de Python sobre patrones de diseño es muy escasa, en comparación con la de Java, C+
+ o Ruby. En “Lecturas adicionales” en la página 180 , mencioné Learning Python Design Patterns de
Gennadiy Zlobin, que se publicó en noviembre de 2013. En contraste, Design Patterns in Ruby de Russ
Olsen se publicó en 2007 y tiene 384 páginas, 284 más que el trabajo de Zlobin. .
Ahora que Python se está volviendo cada vez más popular en el mundo académico, esperemos que se
escriba más sobre patrones de diseño en el contexto de este lenguaje. Además, Java 8 introdujo
referencias de métodos y funciones anónimas, y es probable que esas características tan esperadas
generen nuevos enfoques para los patrones en Java, reconociendo que a medida que evolucionan los
lenguajes, también debe hacerlo nuestra comprensión de cómo aplicar los patrones de diseño clásicos.
CAPÍTULO 7
Ha habido una serie de quejas sobre la elección del nombre "decorador" para esta función. La
principal es que el nombre no es coherente con su uso en el libro GoF.1 El decorador de nombres
probablemente se deba más a su uso en el área del compilador: se recorre y se anota un árbol de
sintaxis.
Los decoradores de funciones nos permiten "marcar" funciones en el código fuente para mejorar su
comportamiento de alguna manera. Esto es algo poderoso, pero dominarlo requiere comprender los cierres.
Una de las palabras clave reservadas más nuevas en Python no es local y se introdujo en Python 3.0.
Puede tener una vida rentable como programador de Python sin usarlo nunca si se adhiere a un régimen
estricto de orientación a objetos centrada en la clase. Sin embargo, si desea implementar sus propios
decoradores de funciones, debe conocer los cierres de adentro hacia afuera, y luego la necesidad de no
locales se vuelve obvia.
Aparte de su aplicación en decoradores, los cierres también son esenciales para una programación asíncrona
efectiva con devoluciones de llamada y para codificar en un estilo funcional siempre que tenga sentido.
El objetivo final de este capítulo es explicar exactamente cómo funcionan los decoradores de funciones,
desde los decoradores de registro más simples hasta los decoradores parametrizados bastante más complicados.
Sin embargo, antes de alcanzar ese objetivo, debemos cubrir:
183
Machine Translated by Google
Comenzamos con una introducción muy básica a los decoradores y luego continuamos con el resto de los
elementos enumerados aquí.
Decoradores 101
Un decorador es un invocable que toma otra función como argumento (la función decorada).2 El decorador
puede realizar algún procesamiento con la función decorada y devolverla o reemplazarla con otra función u
objeto invocable.
@decorate
def target():
print('objetivo en ejecución()')
def objetivo():
print('objetivo en ejecución()')
El resultado final es el mismo: al final de cualquiera de estos fragmentos, el nombre de destino no se refiere
necesariamente a la función de destino original , sino a cualquier función que devuelva decorar(objetivo).
Para confirmar que se reemplazó la función decorada, consulte la sesión de consola en el Ejemplo 7-1.
Ejemplo 7-1. Un decorador suele sustituir una función por otra diferente
>>> def deco(función):
... def interior():
... print('ejecutando interior()')
... volver interior
...
>>>
@deco ... def objetivo():
Estrictamente hablando, los decoradores son solo azúcar sintáctico. Como acabamos de ver, siempre puedes
simplemente llame a un decorador como cualquier invocable regular, pasando otra función. Algunas veces
eso es realmente conveniente, especialmente cuando se hace metaprogramación: cambiar el programa
Comportamiento del programa en tiempo de ejecución.
Para resumir: el primer hecho crucial sobre los decoradores es que tienen el poder de
reemplace la función decorada con una diferente. El segundo hecho crucial es que ellos
se ejecutan inmediatamente cuando se carga un módulo. Esto se explica a continuación.
registro = []
def registro(func):
print('registro en ejecución(%s)' % func)
registro.append(func) return func
@registro
def f1():
imprimir('ejecutando f1()')
@Registrarse
definición f2():
imprimir('ejecutando f2()')
def f3():
print('ejecutando f3()')
def principal():
print('ejecutando main()')
print('registro ->', registro) f1() f2() f3()
si __nombre__=='__principal__':
principal()
main muestra el registro, luego llama a f1(), f2() y f3(). main() solo
Tenga en cuenta que el registro se ejecuta (dos veces) antes que cualquier otra función en el
módulo. Cuando se llama al registro , recibe como argumento el objeto de función que se está
decorando, por ejemplo, <función f1 en 0x100631bf8>.
Después de cargar el módulo, el registro contiene referencias a las dos funciones decoradas: f1
y f2. Estas funciones, así como f3, solo se ejecutan cuando main las llama explícitamente .
El punto principal del ejemplo 7-2 es enfatizar que los decoradores de funciones se ejecutan tan pronto
como se importa el módulo, pero las funciones decoradas solo se ejecutan cuando se invocan
explícitamente. Esto destaca la diferencia entre lo que los Pythonistas llaman tiempo de importación y
tiempo de ejecución.
Teniendo en cuenta cómo se emplean comúnmente los decoradores en el código real, el Ejemplo 7-2 es
inusual en dos formas:
Aunque el decorador de registros del ejemplo 7-2 devuelve la función decorada sin cambios, esa técnica
no es inútil. Se utilizan decoradores similares en muchos marcos web de Python para agregar funciones a
algún registro central, por ejemplo, un registro que asigna patrones de URL a funciones que generan
respuestas HTTP. Dichos decoradores de registro pueden o no cambiar la función decorada. La siguiente
sección muestra un ejemplo práctico.
Recuerde que nuestro problema principal con el Ejemplo 6-6 es la repetición de los nombres de las
funciones en sus definiciones y luego en la lista de promociones usada por la función best_promo para
determinar el descuento más alto aplicable. La repetición es problemática porque alguien puede agregar
una nueva función de estrategia promocional y olvidar agregarla manualmente a la lista de promociones ,
en cuyo caso, best_promo ignorará silenciosamente la nueva estrategia, introduciendo un error sutil en el
sistema. El ejemplo 7-3 resuelve este problema con un decorador de registro.
promociones = []
def promoción(promo_func):
promos.append(promo_func)
devuelve promo_func
@promotion
def fidelity(order): """5%
de descuento para clientes con 1000 o más puntos de fidelidad"""
@promotion
def bulk_item(pedido): """10 %
de descuento por cada elemento de línea con 20 o más unidades""" descuento = 0
para el artículo en order.cart:
@promoción
def gran_pedido (pedido):
"""7% de descuento para pedidos con 10 o más artículos distintos""" distintos_artículos
= {artículo.producto para artículo en pedido.carrito} si len(artículos distintos) >= 10:
def mejor_promo(pedido):
"""Seleccione el mejor descuento disponible
"""
vacía. El decorador de promociones devuelve promo_func sin cambios, después de agregarlo a la lista
de promociones .
Esta solución tiene varias ventajas sobre las otras presentadas en “Estudio de caso: estrategia de refactorización”
en la página 168:
• Las funciones de estrategia de promoción no tienen que usar nombres especiales (es decir, no
necesita usar el sufijo _promo ).
• Las estrategias de descuento promocional se pueden definir en otros módulos, en cualquier parte del
sistema, siempre que se les aplique el decorador @promotion .
La mayoría de los decoradores cambian la función decorada. Por lo general, lo hacen definiendo una función
interna y devolviéndola para reemplazar la función decorada. El código que usa funciones internas casi siempre
depende de los cierres para funcionar correctamente. Para entender cloÿ
Por supuesto, debemos dar un paso atrás y observar de cerca cómo funcionan los ámbitos variables
en Python.
Ejemplo 7-4. Función que lee una variable local y una global
El error que obtuvimos no es sorprendente. Continuando con el Ejemplo 7-4, si asignamos un valor a
una b global y luego llamamos a f1, funciona:
>>> segundo = 6
>>> f1(3)
3
6
Observe la función f2 en el ejemplo 7-5. Sus dos primeras líneas son las mismas que f1 en el Ejemplo
7-4, luego hace una asignación a b e imprime su valor. Pero falla en la segunda impresión, antes de
realizar la asignación.
>>> b = 6
>>> def f2(a):
...
...
... imprime(a) imprime(b) b = 9
...
>>> f2(3)
3
Rastreo (llamadas recientes más última):
Archivo "<stdin>", línea 1, en <módulo>
Archivo "<stdin>", línea 3, en f2
UnboundLocalError: variable local 'b' referenciada antes de la asignación
Tenga en cuenta que la salida comienza con 3, lo que prueba que se ejecutó la instrucción print(a) . Pero
el segundo, print(b), nunca se ejecuta. Cuando vi esto por primera vez me sorprendí, pensando que
debería imprimirse 6 , porque hay una variable global b y la asignación a la b local se realiza después de
print(b).
Pero el hecho es que, cuando Python compila el cuerpo de la función, decide que b es una variable local
porque está asignada dentro de la función. El código de bytes generado refleja esta decisión e intentará
obtener b del entorno local. Más tarde, cuando se realiza la llamada a f2(3) , el cuerpo de f2 obtiene e
imprime el valor de la variable local a, pero al intentar obtener el valor de la variable local b descubre que
b no está vinculada.
Esto no es un error, sino una elección de diseño: Python no requiere que declare variables, pero asume
que una variable asignada en el cuerpo de una función es local. Esto es mucho mejor que el comportamiento
de JavaScript, que tampoco requiere declaraciones de variables, pero si olvida declarar que una variable
es local (con var), puede aplastar una variable global sin saberlo.
Si queremos que el intérprete trate a b como una variable global a pesar de la asignación dentro de la
función, usamos la declaración global :
>>> segundo
>>> f3(3)
a=3b=
8 b = 30
>>>
segundo 30
>>>
Después de ver más de cerca cómo funcionan los ámbitos variables en Python, podemos abordar los
cierres en la siguiente sección, “Cierres” en la página 192. Si tiene curiosidad acerca de las diferencias de
código de bytes entre las funciones de los ejemplos 7-4 y 7-5, consulte la siguiente barra lateral.
El módulo dis proporciona una manera fácil de desensamblar el código de bytes de las funciones de Python.
Lea los ejemplos 7-6 y 7-7 para ver los bytecodes de f1 y f2 de los ejemplos 7-4 y 7-5.
3 10 CARGAR_GLOBAL 0 (imprimir)
13 CARGAR_GLOBAL 1 (b) 1
16 FUNCIÓN_LLAMADA (1 posicional, 0 par de palabras clave)
19 POP_ARRIBA
20 LOAD_CONST 0 (Ninguno)
23 DEVOLUCIÓN_VALOR
Compare el código de bytes para f1 que se muestra en el ejemplo 7-6 con el código de bytes para f2 en
Ejemplo 7-7.
3 10 CARGAR_GLOBAL 0 (imprimir)
13 CARGAR_RÁPIDO 1 (b) 1
16 FUNCIÓN_LLAMADA (1 posicional, 0 par de palabras clave)
19 POP_ARRIBA
4 20 LOAD_CONST 1 (9)
23 ALMACENAR_RÁPIDO 1 (b)
26 CARGAR_CONST 0 (Ninguno)
29 RETURN_VALUE
Cargar nombre local b. Esto muestra que el compilador considera una variable local, incluso si
la asignación a b ocurre más tarde, porque la naturaleza de la variable, ya sea local o no, no
puede cambiar el cuerpo de la función.
La VM de CPython que ejecuta el código de bytes es una máquina de pila, por lo que las operaciones
LOAD y POP se refieren a la pila. Está más allá del alcance de este libro describir más detalladamente
los códigos de operación de Python, pero están documentados junto con el módulo dis en dis —
Disassembler for Python bytecode.
Cierres
En la blogosfera, los cierres a veces se confunden con funciones anónimas. La razón por la que muchos
las confunden es histórica: definir funciones dentro de funciones no es tan común, hasta que empiezas a
usar funciones anónimas. Y los cierres solo importan cuando tiene funciones anidadas. Así que mucha
gente aprende ambos conceptos al mismo tiempo.
En realidad, un cierre es una función con un alcance extendido que abarca variables no globales a las que
se hace referencia en el cuerpo de la función pero que no están definidas allí. No importa si la función es
anónima o no; lo que importa es que puede acceder a variables no globales que se definen fuera de su
cuerpo.
Considere una función promedio para calcular la media de una serie de valores en constante aumento; por
ejemplo, el precio de cierre promedio de un producto básico durante todo su historial. Cada día se agrega
un nuevo precio y se calcula el promedio teniendo en cuenta todos los precios hasta el momento.
Comenzando con una pizarra limpia, así es como se podría usar avg :
>>> promedio(10)
10.0
>>> promedio(11)
10.5 >>>
promedio(12)
11.0
clase Promediador():
def __init__(self):
self.serie = []
self.series.append(new_value) total =
sum(self.series) return total/len(self.series)
>>> promedio(11)
10.5 >>>
promedio(12)
11.0
Ahora, el ejemplo 7-9 es una implementación funcional que utiliza la función de orden superior
make_averager.
Ejemplo 7-9. promedio.py: una función de orden superior para calcular un promedio móvil
def make_averager(): serie = []
def promediador(nuevo_valor):
series.append(nuevo_valor) total =
sum(serie) return total/len(serie)
promediador de retorno
Cuando se invoca, make_averager devuelve un objeto de función de promedio . Cada vez que se
llama a un promediador , agrega el argumento pasado a la serie y calcula el promedio actual, como
se muestra en el Ejemplo 7-10.
Tenga en cuenta las similitudes de los ejemplos: llamamos a Averager() o make_averager() para
obtener un promedio de objeto invocable que actualizará la serie histórica y calculará la media actual.
En el ejemplo 7-8, avg es una instancia de Averager y en el ejemplo 7-9 es la función interna,
averager. De cualquier manera, simplemente llamamos avg(n) para incluir n en la serie y obtener
la media actualizada.
Cierres | 193
Machine Translated by Google
Tenga en cuenta que series es una variable local de make_averager porque la inicialización series = []
ocurre en el cuerpo de esa función. Pero cuando se llama avg(10) , make_averager ya ha regresado y
su alcance local desapareció hace mucho tiempo.
Dentro del promedio, la serie es una variable libre. Este es un término técnico que significa una variable
que no está limitada en el ámbito local. Consulte la Figura 7-1.
Figura 7-1. El cierre para promediador extiende el alcance de esa función para incluir el enlace para la
serie de variables libres.
La inspección del objeto de promedio devuelto muestra cómo Python mantiene los nombres de las
variables locales y libres en el atributo __code__ que representa el cuerpo compilado de la función. El
ejemplo 7-11 lo demuestra.
>>> avg.__code__.co_varnames
('nuevo_valor', 'total') >>>
avg.__code__.co_vars libres
('serie',)
El enlace para series se mantiene en el atributo __closure__ de la función devuelta avg. Cada elemento
en avg.__closure__ corresponde a un nombre en avg.__code__.co_free vars. Estos elementos son
celdas y tienen un atributo llamado cell_contents donde se puede encontrar el valor real. El ejemplo 7-12
muestra estos atributos.
>>> avg.__code__.co_freevars
('series',) >>> avg.__closure__
(<cell at 0x107a44f78: list object
at 0x107a91a48>,) >>> avg.__closure__[0].cell_contents [10,
11, 12 ]
Para resumir: un cierre es una función que retiene los enlaces de las variables libres que existen
cuando se define la función, para que puedan usarse más tarde cuando se invoca la función y el
ámbito de definición ya no está disponible.
Tenga en cuenta que la única situación en la que una función puede necesitar tratar con variables
externas que no son globales es cuando está anidada en otra función.
La declaración no local
Nuestra implementación anterior de make_averager no fue eficiente. En el Ejemplo 7-9, almacenamos
todos los valores en la serie histórica y calculamos su suma cada vez que se llamó al promediador .
Una mejor implementación simplemente almacenaría el total y la cantidad de elementos hasta el
momento, y calcularía la media de estos dos números.
El ejemplo 7-13 es una implementación rota, solo para aclarar un punto. ¿Puedes ver dónde se
rompe?
Ejemplo 7-13. Una función rota de orden superior para calcular un promedio móvil sin guardar todo el
historial
def make_averager():
cuenta = 0 total = 0
def promediador(nuevo_valor):
contar += 1
total += valor_nuevo
devolver total / recuento
promediador de retorno
El problema es que la declaración cuenta += 1 en realidad significa lo mismo que cuenta = cuenta +
1, cuando cuenta es un número o cualquier tipo inmutable. Entonces, en realidad estamos asignando
para contar en el cuerpo del promediador, y eso lo convierte en una variable local. El mismo problema
afecta a la variable total .
No tuvimos este problema en el Ejemplo 7-9 porque nunca asignamos el nombre de la serie; solo
llamamos series.append e invocamos sum y len en él. Así que aprovechamos el hecho de que las
listas son mutables.
Pero con tipos inmutables como números, cadenas, tuplas, etc., todo lo que puede hacer es leer, pero
nunca actualizar. Si intenta volver a enlazarlos, como en cuenta = cuenta + 1, entonces está creando
implícitamente una cuenta variable local . Ya no es una variable libre, y por tanto no se guarda en el cierre.
Para evitar esto, se introdujo la declaración no local en Python 3. Le permite marcar una variable como
una variable libre incluso cuando se le asigna un nuevo valor dentro de la función.
Si se asigna un nuevo valor a una variable no local , se cambia el enlace almacenado en el cierre. Una
implementación correcta de nuestro make_averager más reciente se parece al Ejemplo 7-14.
Ejemplo 7-14. Calcule un promedio móvil sin mantener todo el historial (corregido con el uso de no local)
def promediador(nuevo_valor):
recuento no local , recuento
total += 1 total += new_value
return total / count
promediador de retorno
Ahora que hemos cubierto los cierres de Python, podemos implementar decoradores de manera efectiva
con funciones anidadas.
Ejemplo 7-15. Un decorador simple para generar el tiempo de ejecución de las funciones.
tiempo de importación
Defina la función interna sincronizada para aceptar cualquier número de argumentos posicionales.
Esta línea solo funciona porque el cierre de clocked abarca la variable func free.
#relojdeco_demo.py
importar
hora desde clockdeco importar reloj
@clock
def snooze(segundos):
time.sleep(segundos)
@clock
def factorial(n):
devuelve 1 si n < 2 sino n*factorial(n-1)
if __name__=='__principal__':
print('*' * 40, 'Llamando a snooze(.123)')
snooze(.123) print('*' * 40, 'Llamando a factorial(6)')
print('6 ! =', factorial(6))
$ python3 clockdeco_demo.py
**************************************** Posponer llamadas (123) [0.12405610s]
posponer (.123) -> Ninguno ************************************** ** Llamando a
factorial(6) [0.00000191s] factorial(1) -> 1 [0.00004911s] factorial(2) -> 2
[0.00008488s] factorial(3) -> 6 [0.00013208s] factorial(4) -> 24 [0.00019193s]
factorial(5) -> 120
Cómo funciona
Recuerda que este código:
@clock
def factorial(n):
devuelve 1 si n < 2 sino n*factorial(n-1)
def factorial(n):
devuelve 1 si n < 2 si no n*factorial(n-1)
factorial = reloj(factorial)
Entonces, en ambos ejemplos, clock obtiene la función factorial como su argumento func (vea el Ejemplo 7-15).
Luego crea y devuelve la función cronometrada , que el intérprete de Python asigna a factorial en segundo
plano. De hecho, si importas el módulo clockde co_demo y compruebas el __name__ de factorial, esto es lo que
obtienes:
Entonces factorial ahora en realidad tiene una referencia a la función de reloj . De ahora en adelante, cada vez
que se llame a factorial(n) , se ejecutará clocked(n) . En esencia, clocked hace lo siguiente:
Este es el comportamiento típico de un decorador: reemplaza la función decorada con una nueva función que
acepta los mismos argumentos y (generalmente) devuelve lo que se suponía que devolvería la función decorada,
mientras que también realiza un procesamiento adicional.
funciones de
def clock(func):
@functools.wraps(func) def
clocked(*args, **kwargs): t0 =
time.time() resultado =
func(*args, **kwargs) transcurrido =
time.time() - t0 nombre =
func.__name__ arg_lst = [] if args:
arg_lst.append(', '.join(repr(arg) for
arg in args)) if kwargs: pares =
['%s=%r' % (k, w) para k, w en sorted(kwargs.items())]
arg_lst.append(', '.join(pairs)) arg_str = ', '.join(arg_lst) print('[%0.8fs]
%s( %s) -> %r retorno de resultado retorno cronometrado
'
% (transcurrido, nombre, arg_str, resultado))
functools.wraps es solo uno de los decoradores listos para usar en la biblioteca estándar.
En la siguiente sección, conoceremos a dos de los decoradores más impresionantes que
ofrece functools : lru_cache y singledispatch.
Otro decorador que se ve con frecuencia es functools.wraps, un ayudante para crear decoradores
que se comporten bien. Lo usamos en el Ejemplo 7-17. Dos de los decoradores más interesantes
de la biblioteca estándar son lru_cache y el nuevo singledispatch (agregado en Python 3.4). Ambos
se definen en el módulo functools . Los cubriremos a continuación.
Una buena demostración es aplicar lru_cache a la dolorosamente lenta función recursiva para
generar el n-ésimo número en la secuencia de Fibonacci, como se muestra en el Ejemplo 7-18.
Ejemplo 7-18. La muy costosa forma recursiva de calcular el n-ésimo número en la serie de Fibonacci
@clock
def fibonacci(n): si n
< 2:
retorno
n retorno fibonacci(n-2) + fibonacci(n-1)
if __nombre__=='__main__':
print(fibonacci(6))
Este es el resultado de ejecutar fibo_demo.py. Excepto por la última línea, toda la salida es generada
por el decorador de reloj :
$ python3 fibo_demo.py
[0.00000095s] fibonacci(0) -> 0
[0.00000095s] fibonacci(1) -> 1
[0.00007892s] fibonacci(2) -> 1
[0.00000095s] fibonacci(1) -> 1
[ 0.00000095s] fibonacci(0) -> 0
[0.00000095s] fibonacci(1) -> 1
[0.00003815s] fibonacci(2) -> 1
[0.00007391s] fibonacci(3) -> 2
[0.00018883s] fibonacci(4 ) -> 3
[0.00000000s] fibonacci(1) -> 1
[0.00000095s] fibonacci(0) -> 0
[0.00000119s] fibonacci(1) -> 1
[0.00004911s] fibonacci(2) -> 1
El desperdicio es obvio: fibonacci(1) se llama ocho veces, fibonacci(2) cinco veces, etc.
Pero si solo agregamos dos líneas para usar lru_cache, el rendimiento mejora mucho. Vea el Ejemplo
7-19.
herramientas de importación
@functools.lru_cache() # @clock
# def fibonacci(n): si n < 2:
regreso m
devuelve fibonacci(n-2) + fibonacci(n-1)
if __nombre__=='__main__':
print(fibonacci(6))
Tenga en cuenta que lru_cache debe invocarse como una función regular; observe los paréntesis
en la línea: @functools.lru_cache(). El motivo es que acepta parámetros de configuración, como
veremos en breve.
El tiempo de ejecución se reduce a la mitad y la función se llama solo una vez para cada valor de n:
$ python3 fibo_demo_lru.py
[0.00000119s] fibonacci(0) -> 0
[0.00000119s] fibonacci(1) -> 1
[0.00010800s] fibonacci(2) -> 1
[0.00000787s] fibonacci(3) -> 2
[ 0.00016093s] fibonacci(4) -> 3
[0.00001216s] fibonacci(5) -> 5
[0.00025296s] fibonacci(6) -> 8
En otra prueba, para calcular fibonacci(30), el Ejemplo 7-19 realizó las 31 llamadas necesarias en 0,0005 s,
mientras que el Ejemplo 7-18 no almacenado en caché llamó a fibonacci 2ÿ692ÿ537 veces y tomó 17,7 segundos
en una computadora portátil Intel Core i7.
Además de hacer que los algoritmos recursivos tontos sean viables, lru_cache realmente brilla en las aplicaciones
que necesitan obtener información de la Web.
Es importante tener en cuenta que lru_cache se puede ajustar pasando dos argumentos opcionales.
Su firma completa es:
functools.lru_cache(maxsize=128, escrito=Falso)
El argumento maxsize determina cuántos resultados de llamadas se almacenan. Una vez que la memoria caché
está llena, los resultados más antiguos se descartan para dejar espacio. Para un rendimiento óptimo, maxsize
debe ser una potencia de 2. El argumento escrito , si se establece en True, almacena los resultados de diferentes
tipos de argumentos por separado, es decir, distingue entre argumentos flotantes y enteros que normalmente se
consideran iguales, como 1 y 1.0. Por cierto, debido a que lru_cache usa un dict para almacenar los resultados, y
las claves se crean a partir de los argumentos posicionales y de palabras clave utilizados en las llamadas, todos
los argumentos tomados por la función decorada deben ser hash.
estamos creando una herramienta para depurar aplicaciones web. Queremos poder generar pantallas HTML para
diferentes tipos de objetos de Python.
importar html
def htmlize(obj):
contenido = html.escape(repr(obj))
return '<pre>{}</pre>'.formato(contenido)
Eso funcionará para cualquier tipo de Python, pero ahora queremos extenderlo para generar pantallas
personalizadas para algunos tipos:
• str: reemplace los caracteres de nueva línea incrustados con '<br>\n' y use etiquetas <p> en su lugar
de <pre>.
• lista: genera una lista HTML, dando formato a cada elemento según su tipo.
>>> htmlize(abs)
'<pre><función integrada abs></pre>' >>>
htmlize('Heimlich & Co.\n- un juego') '<p>Heimlich
& Co.<br>\n- un juego</p>' >>> htmlize(42)
'<pre>42 (0x2a)</pre>' >>> print(htmlize(['alpha',
66, { 3, 2, 1}])) <ul>
<li><p>alfa</p></li>
<li><pre>66 (0x42)</pre></li>
<li><pre>{1, 2, 3}</pre> </li> </ul>
De forma predeterminada, la representación con escape HTML de un objeto se muestra encerrada en <pre></
pre>.
Los objetos str también se escapan de HTML pero se envuelven en <p></p> con <br> saltos de línea.
Cada elemento de la lista se formatea según su tipo y la secuencia completa se representa como una lista
HTML.
Debido a que no tenemos sobrecarga de métodos o funciones en Python, no podemos crear variaciones de htmlize
con diferentes firmas para cada tipo de datos que queremos manejar de manera diferente. Una solución común en
Python sería convertir htmlize en una función de despacho, con una cadena de if/elif/elif llamando a funciones
especializadas como htmlize_str, htmlize_int, etc. Esto no es extensible para los usuarios de nuestro módulo y es difícil
de manejar: con el tiempo, el despachador de htmlize se volvería demasiado grande y el acoplamiento entre él y las
funciones especializadas sería muy estrecho.
El nuevo decorador functools.singledispatch en Python 3.4 permite que cada módulo contribuya a la solución general y
le permite proporcionar fácilmente una función especializada incluso para las clases que no puede editar. Si decoras
una función simple con @singledispatch, se convierte en una función genérica: un grupo de funciones para realizar la
misma operación de diferentes maneras, según el tipo del primer argumento.3 El ejemplo 7-21 muestra cómo.
3. Esto es lo que significa el término envío único. Si se usaran más argumentos para seleccionar las funciones específicas,
tendríamos envíos múltiples.
Ejemplo 7-21. singledispatch crea un htmlize.register personalizado para agrupar varias funciones en una
función genérica
@singledispatch
def htmlize(obj):
content = html.escape(repr(obj)) return
'<pre>{}</pre>'.format(content)
@htmlize.registrar(cadena)
def _(texto):
contenido = html.escape(texto).reemplazar('\n', '<br>\n') return
'<p>{0}</p>'.format(contenido)
@htmlize.register(numbers.Integral) def
_(n): return '<pre>{0} (0x{0:x})</
pre>'.format(n)
@htmlize.register(tupla)
@htmlize.register(abc.MutableSequence) def
_(secuencia):
interior = '</li>\n<li>'.join(htmlize(elemento) para elemento en secuencia)
return '<ul>\n<li>' + interior + '</li>\n</ul> '
El nombre de las funciones especializadas es irrelevante; esto _ es una buena opción para hacer
claro
Para que cada tipo adicional reciba un tratamiento especial, registre una nueva función.
números.Integral es una superclase virtual de int.
Puede apilar varios decoradores de registro para admitir diferentes tipos con la misma función.
Cuando sea posible, registre las funciones especializadas para manejar ABC (clases abstractas) como
números.Integral y abc.MutableSequence en lugar de implementaciones concretas como int y list. Esto permite
que su código admita una mayor variedad de tipos compatibles.
Por ejemplo, una extensión de Python puede proporcionar alternativas al tipo int con longitudes de bits fijas
como subclases de números. Integral.
Una cualidad notable del mecanismo de envío único es que puede registrar funciones especializadas
en cualquier parte del sistema, en cualquier módulo. Si luego agrega un módulo con un nuevo tipo
definido por el usuario, puede proporcionar fácilmente una nueva función personalizada para manejar ese tipo.
Y puede escribir funciones personalizadas para clases que no escribió y que no puede cambiar.
singledispatch es una adición bien pensada a la biblioteca estándar y ofrece más funciones de las que
podemos describir aquí. La mejor documentación para ello es PEP 443 —
Funciones genéricas de despacho único.
Los decoradores son funciones y, por lo tanto, pueden estar compuestos (es decir, puede aplicar un
decorador a una función que ya está decorada, como se muestra en el Ejemplo 7-21). La siguiente
sección explica cómo funciona.
Decoradores apilados
El ejemplo 7-19 demostró el uso de decoradores apilados: @lru_cache se aplica al resultado de @clock
sobre fibonacci. En el ejemplo 7-21, el decorador @htmlize.register se aplicó dos veces a la última
función del módulo.
Cuando se aplican dos decoradores @d1 y @d2 a una función f en ese orden, el resultado es el mismo
que f = d1(d2(f)).
@d1
@d2 def f(): imprimir('f')
Es lo mismo que:
def f():
imprimir('f')
f = d1(d2(f))
Además de los decoradores apilados, este capítulo ha mostrado algunos decoradores que toman
argumentos, por ejemplo, @lru_cache() y htmlize.register(«type») producido por @singledispatch en el
Ejemplo 7-21. La siguiente sección muestra cómo crear decoradores que acepten parámetros.
Decoradores parametrizados
Al analizar un decorador en el código fuente, Python toma la función decorada y la pasa como primer
argumento a la función decoradora. Entonces, ¿cómo hacer que un decorador acepte otros argumentos?
La respuesta es: hacer una fábrica de decoradores que tome esos argumentos y devuelva un decorador,
que luego se aplica a la función a decorar.
¿Confuso? Por supuesto. Comencemos con un ejemplo basado en el decorador más simple que hemos
visto: registrarse en el Ejemplo 7-22.
Ejemplo 7-22. Módulo registration.py abreviado del Ejemplo 7-2, repetido aquí para
conveniencia
registro = []
def registro(func):
print('registro en ejecución(%s)' % func)
registro.append(func) return func
@register
def f1():
print('ejecutando f1()')
print('ejecutando main()')
print('registro ->', registro) f1()
activación o desactivación de la función registro realizada por registro, haremos que acepte un parámetro
activo opcional que, si es falso, salta el registro de la función decorada. El ejemplo 7-23 muestra cómo.
Conceptualmente, la nueva función de registro no es un decorador sino una fábrica de decoradores. Cuando
se llama, devuelve el decorador real que se aplicará a la función de destino.
Ejemplo 7-23. Para aceptar parámetros, el nuevo decorador de registros debe llamarse como un
función
registro = conjunto ()
función de retorno
volver decorar
@registrar(activo=Falso) def
f1():
imprimir('ejecutando f1()')
@registrar()
def f2():
imprimir('ejecutando f2()')
definición f3():
imprimir('ejecutando f3()')
el registro ahora es un conjunto, por lo que agregar y eliminar funciones es más rápido.
La fábrica @register debe ser invocada como una función, con el deseado
parámetros
Si no se pasan parámetros, el registro aún debe llamarse como una función: @reg
ister()—es decir, para devolver el decorador real, decor.
El punto principal es que register() devuelve decor, que luego se aplica a la decÿ
función oratoria.
si __nombre__ == '__principal__':
@clock()
def snooze(segundos):
time.sleep(segundos)
_args contiene los argumentos reales de clocked , mientras que args se usa para mostrar.
El uso de **locals() aquí permite hacer referencia a cualquier variable local de clocked en el
archivo fmt.
clocked reemplazará la función decorada, por lo que debería devolver lo que devuelva esa
función.
En esta autocomprobación, clock() se llama sin argumentos, por lo que el decorador aplicado
utilizará el formato predeterminado str.
$ python3 clockdeco_param.py
[0.12412500s] posponer (0.123) -> Ninguno
[0.12411904s] posponer (0.123) -> Ninguno
[0.12410498s] posponer (0.123) -> Ninguno
Para ejercitar la nueva funcionalidad, los ejemplos 7-26 y 7-27 son otros dos módulos que utilizan
clockdeco_param y las salidas que generan.
importar
tiempo desde clockdeco_param importar reloj
$ python3 clockdeco_param_demo1.py
posponer: 0.12414693832397461s
posponer: 0.1241159439086914s posponer:
0.12412118911743164s
importar
tiempo desde clockdeco_param importar reloj
$ python3 clockdeco_param_demo2.py
posponer (0.123) dt = 0.124 s posponer
(0.123) dt = 0.124 s posponer (0.123) dt =
0.124 s
Esto termina nuestra exploración de los decoradores en la medida en que el espacio lo permita dentro
del alcance de este libro. Consulte “Lectura adicional” en la página 212, en particular el blog de Graham
Dumpleton y el módulo Wrapt para técnicas de resistencia industrial al construir decoradores.
Comenzamos con un decorador @register simple sin una función interna y terminamos con un @clock()
parametrizado que involucra dos niveles de funciones anidadas.
Los decoradores de registro, aunque simples en esencia, tienen aplicaciones reales en marcos de
Python avanzados. Aplicamos la idea de registro a una mejora de nuestra refactorización del patrón de
diseño de estrategia del Capítulo 6.
Los decoradores parametrizados casi siempre involucran al menos dos funciones anidadas, tal vez más
si desea usar @functools.wraps para producir un decorador que brinde un mejor soporte para técnicas
más avanzadas. Una de esas técnicas son los decoradores apilados, que cubrimos brevemente.
Comprender cómo funcionan realmente los decoradores requería cubrir la diferencia entre el tiempo de
importación y el tiempo de ejecución, y luego sumergirse en el alcance variable, los cierres y la nueva
declaración no local . Dominar los cierres y no locales es valioso no solo para crear decoradores, sino
también para codificar programas orientados a eventos para GUI o E/S asíncronas con devoluciones de
llamada.
Otras lecturas
El capítulo 9, “Metaprogramación”, del Python Cookbook, tercera edición de David Beazley y Brian K. Jones
(O'Reilly), contiene varias recetas, desde decoradores elementales hasta recetas muy sofisticadas, incluida
una que se puede llamar regular. decorador o como una fábrica de decoradores, por ejemplo, @clock o
@clock(). Eso es “Receta 9.6. Definiendo un decorador que toma un argumento opcional” en ese libro de
cocina.
Graham Dumpleton tiene una serie de publicaciones de blog detalladas sobre técnicas para implementar
decoradores de buen comportamiento, comenzando con "Cómo implementó su decorador de Python es
incorrecto". Su profunda experiencia en este tema también está muy bien empaquetada en el módulo wrapt
que escribió para simplificar la implementación de decoradores y envolturas de funciones dinámicas, que
admiten la introspección y se comportan correctamente cuando se decoran más, cuando se aplican a
métodos y cuando se usan como descriptores. (Los descriptores son el tema del capítulo Capítulo 20.)
Michele Simionato fue autor de un paquete con el objetivo de "simplificar el uso de decoradores para el
programador promedio y popularizar a los decoradores mostrando varios ejemplos no triviales", según los
documentos. Está disponible en PyPI como paquete decorador.
Creada cuando los decoradores todavía eran una característica nueva en Python, la página wiki de la
biblioteca de decoradores de Python tiene docenas de ejemplos. Debido a que esa página comenzó hace
años, algunas de las técnicas mostradas han sido reemplazadas, pero la página sigue siendo una excelente
fuente de inspiración.
PEP 443 proporciona la justificación y una descripción detallada de la facilidad de funciones genéricas de
despacho único. Una publicación de blog anterior (marzo de 2005) de Guido van Rossum, "Múltiples
métodos de cinco minutos en Python", explica una implementación de funciones genéricas (también
conocidas como multimétodos) utilizando decoradores. Su código admite envío múltiple (es decir, envío
basado en más de un argumento posicional). El código multimétodos de Guido es interesante, pero es un
ejemplo didáctico. Para una implementación moderna y lista para la producción de múltiples funciones
genéricas de envío, consulte Reg de Martijn Faassen, autor del marco web Morepath basado en modelos y
experto en REST.
"Cierres en Python" es una breve publicación de blog de Fredrik Lundh que explica la terminología de los
cierres.
PEP 3104: Acceso a nombres en ámbitos externos describe la introducción de la declaración no local para
permitir volver a vincular nombres que no son ni locales ni globales. También incluye una excelente
descripción general de cómo se resuelve este problema en otros lenguajes dinámicos (Perl, Ruby,
JavaScript, etc.) y las ventajas y desventajas de las opciones de diseño disponibles para Python.
En un nivel más teórico, PEP 227 — Ámbitos anidados estáticamente documenta la introducción del ámbito
léxico como opción en Python 2.1 y como estándar en Python 2.2.
Plataforma improvisada
El diseñador de cualquier lenguaje con funciones de primera clase se enfrenta a este problema: al ser objetos
de primera clase, las funciones se definen en un determinado ámbito, pero pueden invocarse en otros ámbitos.
La pregunta es: ¿cómo evaluar las variables libres? La primera y más simple respuesta es "alcance dinámico".
Esto significa que las variables libres se evalúan mirando el entorno donde se invoca la función.
Si Python tuviera un alcance dinámico y no tuviera cierres, podríamos improvisar avg, similar a
Ejemplo 7-9, así:
>>> ### ¡Esta no es una sesión de consola de Python real! ### >>>
promedio = hacer_promedio() >>> serie = [] # >>> promedio(10) 10.0
>>> promedio(11) # 10.5
>>>
promedio(12)
11.0 >>> serie = [1] #
>>> promedio(5) 3.0
Antes de usar avg, tenemos que definir series = [] nosotros mismos, por lo que debemos saber que
promediador (dentro de make_averager) se refiere a una lista con ese nombre.
Detrás de escena, la serie se utiliza para acumular los valores que se promediarán.
Cuando se ejecuta series = [1], se pierde la lista anterior. Esto podría suceder por accidente, cuando
se manejan dos promedios móviles independientes al mismo tiempo.
Las funciones deben ser cajas negras, con su implementación oculta para los usuarios. Pero con el alcance
dinámico, si una función usa variables libres, el programador debe conocer su interior para configurar un
entorno en el que funcione correctamente.
Por otro lado, el alcance dinámico es más fácil de implementar, por lo que probablemente fue el camino que
tomó John McCarthy cuando creó Lisp, el primer lenguaje en tener funciones de primera clase. El artículo de
Paul Graham “The Roots of Lisp” es una explicación accesible del artículo original de John McCarthy sobre el
lenguaje Lisp: “Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I”. El
trabajo de McCarthy es una obra maestra tan grande como la 9ª Sinfonía de Beethoven. Paul Graham lo
tradujo para el resto de nosotros, de matemáticas a inglés y ejecución de código.
El comentario de Paul Graham también muestra cuán complicado es el alcance dinámico. Citando de "Las
raíces de Lisp":
Es un testimonio elocuente de los peligros del alcance dinámico que incluso el primer ejemplo de
funciones Lisp de orden superior se rompió a causa de él. Puede ser que McCarthy no fuera
completamente consciente de las implicaciones del alcance dinámico en 1960. El alcance dinámico
permaneció en las implementaciones de Lisp durante un tiempo sorprendentemente largo, hasta que
Sussman y Steele desarrollaron Scheme en 1975. El alcance léxico no complica la definición de eval.
mucho, pero puede hacer que los compiladores sean más difíciles de escribir.
Hoy en día, el alcance léxico es la norma: las variables libres se evalúan teniendo en cuenta el entorno
donde se define la función. El alcance léxico complica la implementación de lenguajes con funciones de
primera clase, porque requiere el apoyo de clausuras. Por otro lado, el alcance léxico hace que el código
fuente sea más fácil de leer. La mayoría de los idiomas inventados desde Algol tienen alcance léxico.
Durante muchos años, las lambdas de Python no proporcionaron cierres, lo que contribuyó al mal nombre
de esta característica entre los fanáticos de la programación funcional en la blogósfera. Esto se solucionó
en Python 2.2 (diciembre de 2001), pero la blogosfera tiene mucha memoria. Desde entonces, lambda es
vergonzoso solo por su sintaxis limitada.
Los decoradores de funciones de Python se ajustan a la descripción general de Decorator dada por Gamma
et al. en Patrones de diseño: “Adjunte responsabilidades adicionales a un objeto dinámicamente.
Los decoradores brindan una alternativa flexible a la subclasificación para ampliar la funcionalidad”.
En el patrón de diseño, Decorator y Component son clases abstractas. Una instancia de un decorador
concreto envuelve una instancia de un componente concreto para agregarle comportamientos. Citando de
patrones de diseño:
El decorador se ajusta a la interfaz del componente que decora para que su presencia sea transparente
para los clientes del componente. El decorador reenvía las solicitudes al componente y puede realizar
acciones adicionales (como dibujar un borde) antes o después del reenvío. La transparencia le permite
anidar decoradores de forma recursiva, lo que permite un número ilimitado de responsabilidades
adicionales”. (pág. 175)
Tenga en cuenta que no estoy sugiriendo que los decoradores de funciones se deban usar para implementar
el patrón Decorator en los programas de Python. Aunque esto se puede hacer en situaciones específicas,
PARTE IV
CAPÍTULO 8
Referencias a objetos,
mutabilidad y reciclaje
'Estás triste', dijo el Caballero con tono ansioso: 'déjame cantarte una canción para consolarte.
[…] El nombre de la canción se llama “HADDOCKS' EYES”.' 'Oh,
'No, no lo entiendes', dijo el Caballero, luciendo un poco molesto. 'Así es como se LLAMA el nombre.
El nombre realmente ES "EL ANCIANO ANCIANO"' (adaptado del Capítulo VIII.
'Es mi propia invención').
- Lewis Carroll
A través del espejo y lo que Alicia encontró allí
Alicia y el Caballero marcan la pauta de lo que veremos en este capítulo. El tema es la distinción entre los objetos y
sus nombres. Un nombre no es el objeto; un nombre es una cosa separada.
Comenzamos el capítulo presentando una metáfora de las variables en Python: las variables son etiquetas, no cajas.
Si las variables de referencia son viejas para usted, la analogía aún puede ser útil si necesita explicar problemas de
alias a otros.
Luego discutimos los conceptos de identidad de objeto, valor y alias. Se revela un rasgo sorprendente de las tuplas:
son inmutables pero sus valores pueden cambiar. Esto lleva a una discusión sobre copias superficiales y profundas.
Las referencias y los parámetros de función son nuestro próximo tema: el problema con los parámetros
predeterminados mutables y el manejo seguro de los argumentos mutables pasados por los clientes de nuestras
funciones.
Las últimas secciones del capítulo cubren la recolección de basura, el comando del y cómo usar referencias débiles
para "recordar" objetos sin mantenerlos vivos.
Este es un capítulo bastante seco, pero sus temas se encuentran en el corazón de muchos errores sutiles en los
programas reales de Python.
219
Machine Translated by Google
Comencemos por desaprender que una variable es como una caja donde almacenas datos.
El ejemplo 8-1 es una interacción simple que la idea de "variables como cajas" no puede explicar.
La Figura 8-1 ilustra por qué la metáfora del cuadro es incorrecta para Python, mientras que las notas
adhesivas brindan una imagen útil de cómo funcionan realmente las variables.
Ejemplo 8-1. Las variables a y b contienen referencias a la misma lista, no copias de la lista
Figura 8-1. Si imagina que las variables son como cajas, no puede entender la asignación en Python; en su
lugar, piense en las variables como notas adhesivas: el ejemplo 8-1 se vuelve fácil de explicar
El Prof. Stein también habló sobre la asignación de manera muy deliberada. Por ejemplo, cuando habla de
un objeto de balancín en una simulación, diría: "La variable s está asignada al balancín", pero nunca "El
balancín está asignado a la variable s". Con las variables de referencia, tiene mucho más sentido decir que
la variable se asigna a un objeto y no al revés. Después de todo, el objeto se crea antes de la asignación. El
ejemplo 8-2 prueba que el lado derecho de una asignación ocurre primero.
Ejemplo 8-2. Las variables se asignan a los objetos solo después de que se crean los objetos
La salida Gizmo id: ... es un efecto secundario de crear una instancia de Gizmo .
Aquí hay una prueba de que se creó una instancia de un segundo Gizmo antes del
se intentó la multiplicación.
Pero nunca se creó la variable y, porque la excepción ocurrió mientras se evaluaba el lado
derecho de la asignación.
Como las variables son meras etiquetas, nada impide que un objeto tenga varias etiquetas
asignado a ella. Cuando eso sucede, tiene aliasing, nuestro próximo tema.
(4300473992, 4300473992)
>>> luis ['saldo'] = 950 >>>
carlos
{'nombre': 'Charles L. Dodgson', 'saldo': 950, 'nacido': 1832}
Sin embargo, supongamos que un impostor, llamémoslo Dr. Alexander Pedachenko, afirma que es
Charles L. Dodgson, nacido en 1832. Sus credenciales pueden ser las mismas, pero el Dr.
Pedachenko no es el Prof. Dodgson. La Figura 8-2 ilustra este escenario.
Figura 8-2. charles y lewis están ligados al mismo objeto; alex está atado a un objeto separado de
igual contenido
>>> alex = {'nombre': 'Charles L. Dodgson', 'nacido': 1832, 'saldo': 950} >>> alex ==
charles
alex se refiere a un objeto que es una réplica del objeto asignado a charles.
Los objetos se comparan iguales, debido a la implementación de __eq__ en la clase dict .
Pero son objetos distintos. Esta es la forma pitónica de escribir la comparación de identidad
negativa: a no es b.
El ejemplo 8-3 es un ejemplo de creación de alias. En ese código, lewis y charles son alias: dos
variables vinculadas al mismo objeto. Por otro lado, alex no es un alias para
charles: estas variables están vinculadas a objetos distintos. Los objetos vinculados a alex y charles tienen el
mismo valor, eso es lo que == compara, pero tienen identidades diferentes.
Cada objeto tiene una identidad, un tipo y un valor. La identidad de un objeto nunca cambia una
vez que ha sido creado; puede considerarlo como la dirección del objeto en la memoria. El
operador is compara la identidad de dos objetos; la función id() devuelve un número entero que
representa su identidad.
El significado real del ID de un objeto depende de la implementación. En CPython, id() devuelve la dirección de
memoria del objeto, pero puede ser otra cosa en otro intérprete de Python. El punto clave es que se garantiza
que la identificación es una etiqueta numérica única y nunca cambiará durante la vida útil del objeto.
En la práctica, rara vez usamos la función id() durante la programación. Las comprobaciones de identidad se
realizan con mayor frecuencia con el operador is y no mediante la comparación de ID. A continuación, hablaremos de
es contra ==.
los valores de los objetos (los datos que contienen), mientras que compara sus identidades.
A menudo nos preocupamos por los valores y no por las identidades, por lo que == aparece con más frecuencia que en
el código de Python.
Sin embargo, si está comparando una variable con un singleton, entonces tiene sentido usar is.
Con mucho, el caso más común es verificar si una variable está vinculada a Ninguno. Esta es la forma
recomendada de hacerlo:
x es Ninguno
x no es ninguno
El operador is es más rápido que == , porque no se puede sobrecargar, por lo que Python no tiene que buscar e
invocar métodos especiales para evaluarlo, y la computación es tan simple como comparar dos ID de enteros.
Por el contrario, a == b es azúcar sintáctico para a.__eq__(b). El método __eq__ heredado de object compara
los ID de objeto, por lo que produce el mismo resultado que es. Pero la mayoría de los tipos incorporados anulan
__eq__ con implementaciones más significativas que realmente toman en cuenta los valores de los atributos del
objeto. La igualdad puede implicar mucho procesamiento, por ejemplo, cuando se comparan grandes colecciones
o estructuras profundamente anidadas.
Para concluir esta discusión de identidad versus igualdad, veremos que la famosa tupla inmutable no es tan rígida
como cabría esperar.
como la mayoría de las colecciones de Python (listas, dictados, conjuntos, etc.), contienen referencias a objetos.
1 Si los elementos a los que se hace referencia son mutables, pueden cambiar incluso si la tupla en sí no lo hace.
En otras palabras, la inmutabilidad de las tuplas realmente se refiere al contenido físico de la estructura de datos
de la tupla (es decir, las referencias que contiene), y no se extiende a los objetos referenciados.
El ejemplo 8-5 ilustra la situación en la que el valor de una tupla cambia como resultado de cambios en un objeto
mutable al que se hace referencia en ella. Lo que nunca puede cambiar en una tupla es la identidad de los
elementos que contiene.
Ejemplo 8-5. t1 y t2 inicialmente se comparan iguales, pero cambiar un elemento mutable dentro de la tupla t1 lo
hace diferente
Verdadero >>>
id(t1[-1]) 4302515784
>>> t1[-1].agregar(99)
>>> t1
(1, 2, [30, 40, 99]) >>>
id(t1[-1]) 4302515784
>>> t1 == t2
Falso
1. Por otro lado, las secuencias de un solo tipo como str, bytes y array.array son planas: no contienen
referencias pero mantienen físicamente sus datos (caracteres, bytes y números) en la memoria contigua.
Esta inmutabilidad relativa de las tuplas está detrás del acertijo “A += Rompecabezas de asignación” en la
página 40. También es la razón por la que algunas tuplas no se pueden modificar, como vimos en “¿Qué es
un hashable?” en la página 65.
La distinción entre igualdad e identidad tiene implicaciones adicionales cuando necesita copiar un objeto.
Una copia es un objeto igual con una identificación diferente. Pero si un objeto contiene otros objetos,
¿debería la copia también duplicar los objetos internos, o está bien compartirlos?
No hay una respuesta única. Siga leyendo para una discusión.
La forma más fácil de copiar una lista (o la mayoría de las colecciones mutables integradas) es usar el
constructor integrado para el tipo en sí. Por ejemplo:
Verdadero >>> l2 es l1
Falso
Para listas y otras secuencias mutables, el atajo l2 = l1[:] también hace una copia.
Sin embargo, usar el constructor o [:] produce una copia superficial (es decir, el contenedor más externo se
duplica, pero la copia se llena con referencias a los mismos elementos que se encuentran en el contenedor
original). Esto ahorra memoria y no causa problemas si todos los elementos son inmutables. Pero si hay
elementos mutables, esto puede llevar a sorpresas desagradables.
En el Ejemplo 8-6, creamos una copia superficial de una lista que contiene otra lista y una tupla, y luego
hacemos cambios para ver cómo afectan los objetos a los que se hace referencia.
Ejemplo 8-6. Hacer una copia superficial de una lista que contiene otra lista; copiar y pegar
este código para verlo animado en el Online Python Tutor
Para un objeto mutable como la lista referida por l2[1], el operador += cambia el
lista en su lugar. Este cambio es visible en l1[1], que es un alias para l2[1].
+= en una tupla crea una nueva tupla y vuelve a enlazar la variable l2[2] aquí. Esto es
lo mismo que hacer l2[2] = l2[2] + (10, 11). Ahora las tuplas en el último
posición de l1 y l2 ya no son el mismo objeto. Consulte la Figura 8-4.
El resultado del Ejemplo 8-6 es el Ejemplo 8-7, y el estado final de los objetos se representa
en la Figura 8-4.
Figura 8-3. Estado del programa inmediatamente después de la asignación l2 = list(l1) en el ejemplo
8-6. l1 y l2 se refieren a listas distintas, pero las listas comparten referencias al mismo objeto de lista
interna [66, 55, 44] y tupla (7, 8, 9). (Diagrama generado por Online Python Tutor.)
Figura 8-4. Estado final de l1 y l2: aún comparten referencias al mismo objeto de lista, que ahora
contiene [66, 44, 33, 22], pero la operación l2[2] += (10, 11) creó una nueva tupla con contenido ( 7, 8,
9, 10, 11), sin relación con la tupla (7, 8, 9) a la que hace referencia l1[2]. (Diagrama generado por
Online Python Tutor.)
Debería quedar claro ahora que las copias superficiales son fáciles de hacer, pero pueden o no ser lo
que usted quiere. Cómo hacer copias profundas es nuestro próximo tema.
con copias superficiales no siempre es un problema, pero a veces es necesario realizar copias profundas
(es decir, duplicados que no comparten referencias de objetos incrustados). El módulo de copia proporciona
las funciones de copia profunda y copia que devuelven copias profundas y superficiales de objetos
arbitrarios.
Para ilustrar el uso de copy() y deepcopy(), el ejemplo 8-8 define una clase simple, Bus, que representa
un autobús escolar cargado de pasajeros y luego recoge o deja pasajeros en su ruta.
Ahora, en el ejemplo interactivo 8-9 que crearemos, crearemos un objeto de autobús (autobús1) y dos
clones, una copia superficial (autobús2) y una copia profunda (autobús3), para observar lo que sucede
cuando el autobús1 deja a un estudiante.
La inspección de los atributos de los pasajeros muestra que bus1 y bus2 comparten el mismo
objeto de lista, porque bus2 es una copia superficial de bus1. bus3 es una copia profunda de bus1,
Tenga en cuenta que hacer copias profundas no es un asunto simple en el caso general. Los objetos
pueden tener referencias cíclicas que harían que un algoritmo ingenuo entrara en un ciclo infinito. La
función de copia profunda recuerda los objetos ya copiados para manejar las referencias cíclicas con
gracia. Esto se demuestra en el Ejemplo 8-10.
Ejemplo 8-10. Referencias cíclicas: b se refiere a a, y luego se agrega a a; deepcopy todavía logra copiar
un
Además, una copia profunda puede ser demasiado profunda en algunos casos. Por ejemplo, los objetos
pueden hacer referencia a recursos externos o singletons que no deben copiarse. Puede controlar el
comportamiento tanto de la copia como de la copia profunda implementando los métodos especiales
__copy__() y __deepcopy__() como se describe en la documentación del módulo de copia .
El intercambio de objetos a través de alias también explica cómo funciona el paso de parámetros en
Python y el problema de usar tipos mutables como parámetros predeterminados. Estos temas se tratarán
a continuación.
El resultado de este esquema es que una función puede cambiar cualquier objeto mutable pasado como
parámetro, pero no puede cambiar la identidad de esos objetos (es decir, no puede reemplazar por
completo un objeto con otro). El ejemplo 8-11 muestra una función simple que usa += en uno de sus
parámetros. A medida que pasamos números, listas y tuplas a la función, los argumentos reales pasados
se ven afectados de diferentes maneras.
Ejemplo 8-11. Una función puede cambiar cualquier objeto mutable que reciba
>>> x, y
(1, 2)
>>> a = [1, 2]
>>> b = [3, 4]
>>> f(a, b) [1, 2,
3, 4] > >> a, b
([1, 2, 3, 4], [3,
4]) >>> t = (10, 20) >>> u
= (30, 40) >>> f(t, u ) (10,
20, 30, 40) >>> t, u ((10,
20), (30, 40))
El número x no cambia.
Se cambia la lista a .
La tupla t no cambia.
Otro problema relacionado con los parámetros de función es el uso de valores mutables para los valores predeterminados, como
se analiza a continuación.
parámetros opcionales con valores predeterminados son una gran característica de las definiciones de funciones
de Python, lo que permite que nuestras API evolucionen sin dejar de ser compatibles con versiones anteriores. Sin
embargo, debe evitar los objetos mutables como valores predeterminados para los parámetros.
Para ilustrar este punto, en el Ejemplo 8-12, tomamos la clase Bus del Ejemplo 8-8 y cambiamos su método
__init__ para crear HauntedBus. Aquí tratamos de ser inteligentes y en lugar de tener un valor predeterminado
de pasajeros=Ninguno, tenemos pasajeros=[], evitando así el if en el __init__ anterior. Esta “inteligencia” nos
mete en problemas.
Ejemplo 8-12. Una clase simple para ilustrar el peligro de un valor predeterminado mutable.
clase HauntedBus:
"""Un modelo de autobús perseguido por pasajeros fantasmas"""
self.pasajeros = pasajeros
bus2 comienza vacío, por lo que la lista vacía predeterminada se asigna a self.passengers.
El problema es que las instancias de Bus que no obtienen una lista de pasajeros inicial terminan
compartiendo la misma lista de pasajeros entre ellas.
Dichos errores pueden ser sutiles. Como demuestra el Ejemplo 8-13 , cuando se crea una instancia de
HauntedBus con pasajeros, funciona como se esperaba. Suceden cosas extrañas solo cuando un
HauntedBus comienza vacío, porque entonces self.passengers se convierte en un alias para el valor
predeterminado del parámetro de pasajeros . El problema es que cada valor predeterminado se evalúa
cuando se define la función, es decir, generalmente cuando se carga el módulo, y los valores
predeterminados se convierten en atributos del objeto de la función. Entonces, si un valor predeterminado
es un objeto mutable y lo cambia, el cambio afectará cada llamada futura de la función.
Después de ejecutar las líneas del Ejemplo 8-13, puede inspeccionar el objeto HauntedBus.__init__ y
ver a los estudiantes fantasmas rondando su atributo __defaults__ :
Finalmente, podemos verificar que bus2.passengers es un alias ligado al primer elemento del atributo
HauntedBus.__init__.__defaults__ :
El problema con los valores predeterminados mutables explica por qué Ninguno se usa a menudo como
valor predeterminado para los parámetros que pueden recibir valores mutables. En el Ejemplo 8-8,
__init__ verifica si el argumento de pasajeros es Ninguno y asigna una nueva lista vacía a self.passengers.
Como se explica en la siguiente sección, si pasajeros no es Ninguno, la implementación correcta asigna
una copia de la misma a self.pasajeros. Ahora echemos un vistazo más de cerca.
codificando una función que recibe un parámetro mutable, debe considerar cuidadosamente si la
persona que llama espera que se cambie el argumento pasado.
Por ejemplo, si su función recibe un dict y necesita modificarlo mientras lo procesa, ¿este efecto
secundario debería ser visible fuera de la función o no? En realidad depende del contexto. Es realmente
una cuestión de alinear la expectativa del codificador de la función y la de la persona que llama.
El último ejemplo de autobús en este capítulo muestra cómo un TwilightBus rompe las expectativas al
compartir su lista de pasajeros con sus clientes. Antes de estudiar la implementación, ver en
Ejemplo 8-14 cómo funciona la clase TwilightBus desde la perspectiva de un cliente de la clase.
Ejemplo 8-14. Los pasajeros desaparecen cuando los deja caer un TwilightBus
>>> equipo_de_baloncesto = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat'] >>> bus =
TwilightBus(equipo_de_baloncesto) >>> bus.drop('Tina') >>> bus .drop('Pat') >>>
equipo_de_baloncesto ['Sue', 'Maya', 'Diana']
TwilightBus viola el "Principio de menor asombro", una de las mejores prácticas de diseño de interfaz.
Seguramente es asombroso que cuando el autobús deja caer a una estudiante, su nombre se elimina
de la lista del equipo de baloncesto.
Ejemplo 8-15. Una clase simple para mostrar los peligros de mutar los argumentos recibidos.
clase CrepúsculoBus:
"""Un modelo de autobús que hace desaparecer a los pasajeros"""
Aquí tenemos cuidado de crear una nueva lista vacía cuando pasajeros es Ninguno.
Sin embargo, esta asignación convierte a self.passengers en un alias para pasajeros, que a su
vez es un alias para el argumento real pasado a __init__ (es decir, equipo_de_baloncesto en el
ejemplo 8-14).
Cuando los métodos .remove() y .append() se usan con self.passen gers, en realidad estamos
mutando la lista original recibida como argumento para el
constructor.
El problema aquí es que el bus crea un alias en la lista que se pasa al constructor.
En su lugar, debería mantener su propia lista de pasajeros. La solución es simple: en __init__, cuando
se proporciona el parámetro de pasajeros , self.passengers debe inicializarse con una copia del mismo,
como hicimos correctamente en el Ejemplo 8-8 ("Copias profundas y superficiales de objetos arbitrarios"
en la página 228):
Ahora nuestro manejo interno de la lista de pasajeros no afectará el argumento utilizado para inicializar
el bus. Como beneficio adicional, esta solución es más flexible: ahora el argumento pasado al
parámetro pasajeros puede ser una tupla o cualquier otro iterable, como un conjunto o incluso los
resultados de la base de datos, porque el constructor de la lista acepta cualquier iterable. A medida
que creamos nuestra propia lista para administrar, nos aseguramos de que sea compatible con las
operaciones .remove() y .app pend() necesarias que usamos en los métodos .pick() y .drop() .
La instrucción del elimina nombres, no objetos. Un objeto puede ser recolectado como basura como
resultado de un comando del , pero solo si la variable eliminada contiene la última referencia al objeto,
o si el objeto se vuelve inalcanzable.2 Volver a vincular una variable también puede hacer que la
cantidad de referencias a un objeto alcance cero, provocando su destrucción.
2. Si dos objetos se refieren entre sí, como en el ejemplo 8-10, pueden destruirse si el recolector de elementos no utilizados
determina que, de otro modo, son inalcanzables porque sus únicas referencias son sus referencias mutuas.
Para demostrar el final de la vida de un objeto, el ejemplo 8-16 usa el comando débilref.finalizar para registrar
una función de devolución de llamada que se llamará cuando se destruya un objeto.
Ejemplo 8-16. Observar el final de un objeto cuando ya no hay más referencias que lo apunten
Verdadero
Esta función no debe ser un método vinculado del objeto a punto de ser destruido o contener una referencia a
él.
Volver a enlazar la última referencia, s2, hace que {1, 2, 3} sea inalcanzable. Se destruye, se invoca la
devolución de llamada bye y ender.alive se convierte en False.
El objetivo del ejemplo 8-16 es hacer explícito que del no elimina objetos, pero los objetos pueden eliminarse como
consecuencia de que no se puede acceder a ellos después de que se usa del .
Quizás se pregunte por qué se destruyó el objeto {1, 2, 3} en el ejemplo 8-16. Después de todo, la referencia s1 se pasó
a la función finalizar , que debe haberla retenido para monitorear el objeto e invocar la devolución de llamada. Esto
funciona porque final ize tiene una referencia débil a {1, 2, 3}, como se explica en la siguiente sección.
Referencias débiles
La presencia de referencias es lo que mantiene vivo un objeto en la memoria. Cuando el recuento de referencias de un
objeto llega a cero, el recolector de basura lo desecha. Pero a veces es útil tener una referencia a un objeto que no se
conserva más tiempo del necesario.
Un caso de uso común es un caché.
Las referencias débiles a un objeto no aumentan su recuento de referencias. El objeto que es el objetivo de una
referencia se llama referente. Por lo tanto, decimos que una referencia débil no evita que el referente sea recolectado
como basura.
Las referencias débiles son útiles en las aplicaciones de almacenamiento en caché porque no desea que los objetos
almacenados en caché se mantengan vivos solo porque la memoria caché hace referencia a ellos.
El ejemplo 8-17 muestra cómo se puede llamar a una instancia de ref.débil.ref para llegar a su referente. Si el objeto
está vivo, llamar a la referencia débil lo devuelve; de lo contrario, se devuelve Ninguno .
Ejemplo 8-17. Una referencia débil es un invocable que devuelve el objeto al que se hace referencia o Ninguno
si el referente ya no es
Invocar wref() devuelve el objeto al que se hace referencia, {0, 1}. Porque esto es una consola.
sesión, el resultado {0, 1} está vinculado a la _ variable.
a_set ya no hace referencia al conjunto {0, 1} , por lo que su recuento de referencias se reduce. Pero
la _ variable todavía se refiere a ella.
Cuando se evalúa esta expresión, {0, 1} vive, por lo tanto, wref() no es Ninguno.
Pero _ luego se vincula al valor resultante, False. Ahora no hay más fuerte
Debido a que el objeto {0, 1} ya no está, esta última llamada a wref() devuelve Ninguno.
Nuestra demostración de WeakValueDictionary está inspirada en la obra de teatro clásica Cheese Shop
de Monty Python, en la que un cliente pide más de 40 tipos de queso, incluidos cheddar y mozzarella,
pero no hay ninguno disponible.3 El ejemplo 8-18 implementa una clase trivial para representar cada
tipo de queso.
def __repr__(self):
return 'Cheese(%r)' % self.kind
En el Ejemplo 8-19, cada queso se carga desde un catálogo a un stock implementado como
WeakValueDictionary. Sin embargo, todos menos uno desaparecen del stock tan pronto como se
elimina el catálogo . ¿Puedes explicar por qué el queso parmesano dura más que los demás?4 El
consejo después del código tiene la respuesta.
['Parmesan']
>>> del queso
>>> sorted(stock.keys()) []
3. cheeseshop.python.org también es un alias de PyPI, el repositorio de software Python Package Index, que comenzó su vida bastante
vacío. Al momento de escribir este artículo, Python Cheese Shop tiene 41,426 paquetes. No está mal, pero todavía está lejos de los
más de 131.000 módulos disponibles en CPAN, la red integral de archivos de Perl, la envidia de todas las comunidades lingüísticas
dinámicas.
4. El queso parmesano se envejece al menos un año en la fábrica, por lo que es más duradero que el queso fresco, pero esto no es así.
la respuesta que estamos buscando.
stock es un WeakValueDictionary.
El stock asigna el nombre del queso a una referencia débil a la instancia de queso
en el catálogo.
El stock está completo.
Después de eliminar el catálogo , la mayoría de los quesos desaparecen del inventario, como se esperaba
en WeakValueDictionary. ¿Por qué no todos, en este caso?
Una contraparte del WeakValueDictionary es el WeakKeyDictionary en el que las claves son referencias débiles. La
documentación deweakref.WeakKeyDictionary sugiere posibles usos:
El módulo de ref débil también proporciona un WeakSet , simplemente descrito en los documentos como “Clase de
conjunto que mantiene referencias débiles a sus elementos. Un elemento será descartado cuando ya no exista una
fuerte referencia a él”. Si necesita crear una clase que conozca cada una de sus instancias, una buena solución es
crear un atributo de clase con un WeakSet para contener las referencias a las instancias. De lo contrario, si se usara
un conjunto regular , las instancias nunca se recolectarían basura, porque la clase en sí tendría fuertes referencias
a ellas, y las clases vivirían tanto como el proceso de Python a menos que las elimine deliberadamente.
Estas colecciones, y las referencias débiles en general, están limitadas en cuanto a los tipos de objetos que pueden
manejar. La siguiente sección explica.
No todos los objetos de Python pueden ser el objetivo o el referente de una referencia débil. Es posible que las
instancias básicas de listas y dictados no sean referentes, pero una simple subclase de cualquiera puede resolver
este problema fácilmente:
class MyList(list):
"""subclase de lista cuyas instancias pueden estar débilmente referenciadas"""
Una instancia de conjunto puede ser un referente, y por eso se usó un conjunto en el ejemplo 8-17. Los tipos definidos
por el usuario tampoco plantean ningún problema, lo que explica por qué se necesitaba la tonta clase Cheese en el
ejemplo 8-19. Pero las instancias de int y tuple no pueden ser objetivos de referencias débiles, incluso si se crean
subclases de esos tipos.
La mayoría de estas limitaciones son detalles de implementación de CPython que pueden no aplicarse a otros intérpretes
de Python. Son el resultado de optimizaciones internas, algunas de las cuales se analizan en la siguiente sección
(altamente opcional).
Me sorprendió saber que, para una tupla t, t[:] no hace una copia, sino que devuelve una referencia al mismo objeto.
También obtiene una referencia a la misma tupla si escribe 5. El ejemplo 8-20 lo demuestra. tupla(t).
Ejemplo 8-20. Una tupla construida a partir de otra es en realidad la misma tupla exacta
Verdadero >>>
t3 = t1[:] >>> t3 es t1
Verdadero
Y también lo es t3.
El mismo comportamiento se puede observar con instancias de str, bytes y frozenset. Tenga en cuenta que un conjunto
congelado no es una secuencia, por lo que fs[:] no funciona si fs es un conjunto congelado. Pero
5. Esto está claramente documentado. Escriba ayuda (tupla) en la consola de Python para leer: "Si el argumento es una
tupla, el valor devuelto es el mismo objeto". Pensé que sabía todo acerca de las tuplas antes de escribir este libro.
fs.copy() tiene el mismo efecto: engaña y devuelve una referencia al mismo objeto, y no una copia en absoluto,
como muestra el Ejemplo 8-21.6
El intercambio de literales de cadena es una técnica de optimización llamada interning. CPython usa la misma
técnica con números enteros pequeños para evitar la duplicación innecesaria de números "populares" como 0, -1 y
42. Tenga en cuenta que CPython no interna todas las cadenas o enteros, y el criterio que usa para hacerlo es un
detalle de implementación no documentado.
Los trucos discutidos en esta sección, incluido el comportamiento de frozenset.copy(), son "mentiras piadosas";
ahorran memoria y hacen que el intérprete sea más rápido. No te preocupes por ellos, no deberían darte ningún
problema porque solo se aplican a tipos inmutables.
Probablemente el mejor uso de estos fragmentos de trivia sea ganar apuestas con otros Pythonistas.
6. La mentira piadosa de tener el método de copia que no copia nada puede explicarse por la compatibilidad de la
interfaz: hace que frozenset sea más compatible con set. De todos modos, no hace ninguna diferencia para el usuario
final si dos objetos inmutables idénticos son iguales o son copias.
Si dos variables se refieren a objetos inmutables que tienen valores iguales (a == b es Verdadero), en la práctica rara
vez importa si se refieren a copias o son alias que se refieren al mismo objeto porque el valor de un objeto inmutable
no cambia, con una excepción La excepción son las colecciones inmutables como tuplas y conjuntos congelados: si
una colección inmutable contiene referencias a elementos mutables, entonces su valor puede cambiar cuando cambia
el valor de un elemento mutable. En la práctica, este escenario no es tan común. Lo que nunca cambia en una
colección inmutable son las identidades de los objetos dentro.
El hecho de que las variables contengan referencias tiene muchas consecuencias prácticas en la programación de
Python:
aumentada con += o *= crea nuevos objetos si la variable de la izquierda está vinculada a un objeto inmutable,
pero puede modificar un objeto mutable en su lugar. • Asignar un nuevo valor a una variable existente no
cambia el objeto previamente vinculado a ella. Esto se denomina reenlace: la variable ahora está vinculada a un
objeto diferente.
Si esa variable fue la última referencia al objeto anterior, ese objeto será recolectado como basura. • Los
parámetros de la función se pasan como alias, lo que significa que la función puede cambiar cualquier objeto
mutable recibido como argumento. No hay forma de evitar esto, excepto hacer copias locales o usar objetos
inmutables (por ejemplo, pasar una tupla en lugar de una lista).
• El uso de objetos mutables como valores predeterminados para los parámetros de función es peligroso porque
si los parámetros se cambian en su lugar, entonces se cambia el valor predeterminado, lo que afecta todas las
llamadas futuras que se basen en el valor predeterminado.
En CPython, los objetos se descartan tan pronto como el número de referencias a ellos llega
a cero. También pueden descartarse si forman grupos con referencias cíclicas pero sin
referencias externas. En algunas situaciones, puede ser útil mantener una referencia a un
objeto que, por sí mismo, no mantendrá vivo un objeto. Un ejemplo es una clase que quiere
realizar un seguimiento de todas sus instancias actuales. Esto se puede hacer con referencias
débiles, un mecanismo de bajo nivel que subyace a las colecciones más útiles
WeakValueDictionary, WeakKey Dictionary, WeakSet y la función de finalización del módulo de referencia débil .
7. En realidad, el tipo de un objeto puede cambiarse simplemente asignando una clase diferente a su atributo __class__,
pero eso es pura maldad y lamento escribir esta nota al pie.
Otras lecturas
El capítulo "Modelo de datos" de The Python Language Reference comienza con una explicación clara de
las identidades y los valores de los objetos.
Wesley Chun, autor de la serie de libros Core Python , realizó una excelente presentación sobre muchos de
los temas tratados en este capítulo durante OSCON 2013. Puede descargar las diapositivas de la página
de discusión "Python 103: modelo de memoria y mejores prácticas". También hay un video de YouTube de
una presentación más larga que Wesley dio en EuroPython 2011, cubriendo no solo el tema de este capítulo
sino también el uso de métodos especiales.
Doug Hellmann escribió una larga serie de excelentes publicaciones de blog tituladas Python Module of the
Week, que se convirtió en un libro, The Python Standard Library by Example. Sus publicaciones "copia -
Objetos duplicados" y "weakref - Referencias a objetos recopilables en basura" cubren algunos de los temas
que acabamos de discutir.
Una implementación puede posponer la recolección de basura u omitirla por completo; es una
cuestión de calidad de implementación cómo se implementa la recolección de basura, siempre
que no se recolecten objetos que aún estén accesibles.
Fredrik Lundh, creador de bibliotecas clave como ElementTree, Tkinter y la biblioteca de imágenes PIL,
tiene una breve publicación sobre el recolector de basura de Python titulada "¿Cómo administra Python la
memoria?" Él enfatiza que el recolector de basura es una característica de implementación que se comporta
de manera diferente entre los intérpretes de Python. Por ejemplo, Jython usa el recolector de basura de
Java.
El recolector de elementos no utilizados de CPython 3.4 mejoró el manejo de objetos con un método
__del__ , como se describe en PEP 442 — Finalización segura de objetos.
Wikipedia tiene un artículo sobre internamiento de cadenas, que menciona el uso de esta técnica en varios
idiomas, incluido Python.
Plataforma improvisada
Aprendí Java antes de descubrir Python. El operador == en Java nunca me pareció adecuado. Es mucho
más común que los programadores se preocupen por la igualdad que por la identidad, pero para los
objetos (no los tipos primitivos) Java == compara referencias y no valores de objetos.
Incluso para algo tan básico como comparar cadenas, Java te obliga a usar el método .equals . Incluso
entonces, hay otra trampa: si escribe a.equals(b) y a es nulo, obtiene una excepción de puntero nulo.
Los diseñadores de Java sintieron la necesidad de sobrecargar + para cadenas, así que ¿por qué no
seguir adelante y sobrecargar == también?
Python lo entiende bien. El operador == compara valores de objetos y compara referencias. Y debido a
que Python tiene una sobrecarga de operadores, == funciona de manera sensata con todos los objetos
en la biblioteca estándar, incluido Ninguno, que es un objeto adecuado, a diferencia del nulo de Java.
Y, por supuesto, puede definir __eq__ en sus propias clases para decidir qué significa == para sus
instancias. Si no anula __eq__, el método heredado de object compara los ID de objeto, por lo que la
alternativa es que cada instancia de una clase definida por el usuario se considere diferente.
Estas son algunas de las cosas que me hicieron cambiar de Java a Python tan pronto como terminé de
leer el Tutorial de Python una tarde de septiembre de 1998.
Mutabilidad
Este capítulo sería redundante si todos los objetos de Python fueran inmutables. Cuando se trata de
objetos que no cambian, no importa si las variables contienen los objetos reales o las referencias a
objetos compartidos. Si a == b es verdadero, y ninguno de los objetos puede cambiar, también podrían
ser iguales. Es por eso que la internación de cuerdas es segura. La identidad del objeto se vuelve
importante solo cuando los objetos son mutables.
En la programación funcional "pura", todos los datos son inmutables: agregar a una colección en realidad
crea una nueva colección. Python, sin embargo, no es un lenguaje funcional, y mucho menos puro. Las
instancias de clases definidas por el usuario son mutables de forma predeterminada en Python, como
en la mayoría de los lenguajes orientados a objetos. Al crear sus propios objetos, debe tener mucho
cuidado para hacerlos inmutables, si ese es un requisito. Cada atributo del objeto también debe ser
inmutable; de lo contrario, terminará con algo como la tupla: inmutable en lo que respecta a los ID de
objeto, pero el valor de una tupla puede cambiar si contiene un objeto mutable.
Los objetos mutables también son la razón principal por la que es tan difícil programar con subprocesos:
los subprocesos que mutan objetos sin la sincronización adecuada producen datos corruptos. La
sincronización excesiva, por otro lado, provoca interbloqueos.
No hay ningún mecanismo en Python para destruir directamente un objeto, y esta omisión es en
realidad una gran característica: si pudieras destruir un objeto en cualquier momento, ¿qué pasaría
con las fuertes referencias existentes que apuntan a él?
Pero el conteo de referencias sigue estando allí como línea de base y provoca la eliminación
inmediata de objetos con referencias cero. Esto significa que, en CPython, al menos por ahora, es
seguro escribir esto:
Ese código es seguro porque el recuento de referencias del objeto de archivo será cero después de
que regrese el método de escritura , y Python cerrará inmediatamente el archivo antes de destruir el
objeto que lo representa en la memoria. Sin embargo, la misma línea no es segura en Jython o
IronPython que usan el recolector de basura de sus tiempos de ejecución de host (Java VM y .NET
CLR), que son más sofisticados pero no dependen del recuento de referencias y pueden tardar más
en destruir el objeto y cierre el archivo. En todos los casos, incluido CPython, la mejor práctica es
cerrar explícitamente el archivo, y la forma más confiable de hacerlo es usar la declaración with , que
garantiza que el archivo se cerrará incluso si se generan excepciones mientras está abierto. Usando
with, el fragmento anterior se convierte en:
Si le interesa el tema de los recolectores de basura, es posible que desee leer el artículo de Thomas
Perl "Implementaciones de recolectores de basura de Python: CPython, PyPy y GaS", del cual
aprendí un poco sobre la seguridad de open().write() en CPython.
Una forma popular de explicar cómo funciona el paso de parámetros en Python es la frase: "Los
parámetros se pasan por valor, pero los valores son referencias". Esto no está mal, pero genera
confusión porque los modos de paso de parámetros más comunes en los lenguajes más antiguos
son llamada por valor (la función obtiene una copia del argumento) y llamada por referencia (la
función obtiene un puntero al argumento). En Python, la función obtiene una copia de los argumentos,
pero los argumentos siempre son referencias. Por lo tanto, el valor de los objetos a los que se hace
referencia se puede cambiar, si son mutables, pero su identidad no. Además, debido a que la función
obtiene una copia de la referencia en un argumento, volver a vincularla no tiene ningún efecto fuera
de la función. Adopté el término llamada compartiendo después de leer sobre el tema en Programación
de lenguaje pragmático, tercera edición de Michael L. Scott (Morgan Kaufmann), particularmente
"8.3.1: Modos de parámetros".
Me encanta este pasaje, pero era demasiado largo para abrir un capítulo. Así que aquí está el diálogo
completo sobre la canción del Caballero, su nombre y cómo se llaman la canción y su nombre:
—Estás triste —dijo el Caballero con tono ansioso—, déjame cantarte una canción para consolarte.
¿Es muy largo? preguntó Alice, porque había escuchado mucha poesía ese día.
'Es largo', dijo el Caballero, 'pero muy, MUY hermoso. A todos los que me oyen cantarla, o les saltan
LÁGRIMAS a los ojos, o si no... —¿O si no qué? dijo Alicia, porque el Caballero había hecho una
pausa repentina.
'Oh, ese es el nombre de la canción, ¿verdad?' Dijo Alice, tratando de sentirse interesada.
'No, no lo entiendes', dijo el Caballero, luciendo un poco molesto. 'Así es como se LLAMA el nombre.
El nombre realmente ES "EL HOMBRE ENVEJECIDO".' '¿Entonces debería haber dicho 'Así es como
'No, no deberías: ¡eso es otra cosa! La CANCIÓN se llama “CAMINOS Y MEDIOS”: pero así
es como se LLAMA, ¿sabes? 'Bueno, entonces, ¿cuál ES la canción?' dijo Alice, quien en ese
—Estaba llegando a eso —dijo el Caballero. 'La canción realmente ES "A-SITTING ON A GATE": y la
melodía es mi propia invención.'
- Lewis Carroll
Capítulo VIII, “Es mi propia invención”, a través del espejo
CAPÍTULO 9
Un objeto pitónico
Nunca, nunca use dos guiones bajos al principio. Esto es irritantemente privado.1
—Ian Biking
Creador de pip, virtualenv, Paste y muchos otros proyectos
Gracias al modelo de datos de Python, sus tipos definidos por el usuario pueden comportarse con tanta naturalidad
como los tipos integrados. Y esto se puede lograr sin herencia, en el espíritu de la tipificación pato: simplemente
implementa los métodos necesarios para que sus objetos se comporten como se espera.
Este capítulo comienza donde terminó el Capítulo 1 , mostrando cómo implementar varios métodos especiales que
se ven comúnmente en objetos Python de muchos tipos diferentes.
• Apoyar las funciones integradas que producen representaciones de objetos alternativas (p. ej.,
repr(), bytes(), etc.).
• Implementar un constructor alternativo como método de clase. • Ampliar el
247
Machine Translated by Google
Haremos todo eso mientras desarrollamos un tipo de vector euclidiano bidimensional simple.
Representaciones de objetos
Cada lenguaje orientado a objetos tiene al menos una forma estándar de obtener una representación
de cadena de cualquier objeto. Python tiene dos:
repr()
Devuelve una cadena que representa el objeto como el desarrollador quiere verlo.
cadena()
Devuelve una cadena que representa el objeto como el usuario quiere verlo.
Como sabe, implementamos los métodos especiales __repr__ y __str__ para admitir repr() y str().
Hay dos métodos especiales adicionales para admitir representaciones alternativas de objetos:
__bytes__ y __format__. El método __bytes__ es análogo a __str__: bytes() lo llama para obtener el
objeto representado como una secuencia de bytes. Con respecto a __format__, tanto la función
incorporada format() como el método str.format() lo llaman para obtener visualizaciones de cadenas
de objetos usando códigos de formato especiales. Cubriremos __bytes__ en el siguiente ejemplo y
__format__ después de eso.
Para demostrar los muchos métodos usados para generar representaciones de objetos, usaremos
una clase Vector2d similar a la que vimos en el Capítulo 1. La desarrollaremos en esta y futuras
secciones. El ejemplo 9-1 ilustra el comportamiento básico que esperamos de una instancia de
Vector2d .
3.0 4.0
>>> x, y = v1
>>> x, y (3.0,
4.0) >>> v1
Vector2d(3.0,
4.0) >>> v1_clone =
eval(repr(v1)) >>> v1 == v1_clone
True > >> imprimir(v1) (3.0, 4.0)
>>> octetos = bytes(v1)
>>> octetos
b'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@' >> > abdominales
(v1) 5.0
Se puede acceder a los componentes de un Vector2d directamente como atributos (sin llamadas a
métodos getter).
El uso de eval aquí muestra que la repetición de un Vector2d es una representación fiel de la llamada
de su constructor.2
Vector2d del Ejemplo 9-1 se implementa en vector2d_v0.py (Ejemplo 9-2). El código se basa en el Ejemplo
1-2, pero los operadores infijos se implementarán en el Capítulo 13 , excepto == (que es útil para realizar
pruebas). En este punto, Vector2d usa varios métodos especiales para proporcionar operaciones que un
Pythonista espera en un objeto bien diseñado.
Ejemplo 9-2. vector2d_v0.py: los métodos hasta ahora son todos métodos especiales
2. Usé eval para clonar el objeto aquí solo para hacer un comentario sobre repr; para clonar una instancia, el copy.copy
la función es más segura y más rápida.
clase Vector2d:
código de tipo = 'd'
__iter__ hace un Vector2d iterable; esto es lo que hace que el desempaquetado funcione (p. ej.,
x, y = mi_vector). Lo implementamos simplemente usando una expresión generadora
para producir los componentes uno tras otro.3
__repr__ construye una cadena interpolando los componentes con {!r} para obtener su
repr; debido a que Vector2d es iterable, *self alimenta los componentes x e y para
formato.
A partir de un Vector2d iterable , es fácil crear una tupla para mostrarla ordenada
par.
3. Esta línea también podría escribirse como yield self.x; rendimiento.auto.y. Tengo mucho más que decir sobre el
__iter__ método especial, expresiones generadoras y la palabra clave yield en el Capítulo 14.
Para comparar rápidamente todos los componentes, cree tuplas a partir de los operandos. Esto
funciona para operandos que son instancias de Vector2d, pero tiene problemas. Consulte la siguiente
advertencia.
El método __eq__ del Ejemplo 9-2 funciona para los operandos Vector2d
pero también devuelve True cuando se comparan instancias de Vector2d
con otros iterables que tienen los mismos valores numéricos (p. ej., Vector(3,
4) == [3, 4]). Esto puede considerarse una característica o un error. La
discusión adicional debe esperar hasta el Capítulo 13, cuando cubrimos la
sobrecarga del operador.
Tenemos un conjunto bastante completo de métodos básicos, pero falta una operación obvia: reconstruir un
Vector2d a partir de la representación binaria producida por bytes().
Un constructor alternativo
Debido a que podemos exportar un Vector2d como bytes, naturalmente necesitamos un método que importe
un Vector2d desde una secuencia binaria. Buscando inspiración en la biblioteca estándar, encontramos que
array.array tiene un método de clase llamado .frombytes que se adapta a nuestro propósito; lo vimos en
"Arreglos" en la página 48. Adoptamos su nombre y usamos su funcionalidad en un método de clase para
Vector2d en vector2d_v1.py (Ejemplo 9-3).
Ejemplo 9-3. Parte de vector2d_v1.py: este fragmento muestra solo el método de clase frombytes, agregado
a la definición de Vector2d en vector2d_v0.py (Ejemplo 9-2)
@classmethod
def frombytes(cls, octetos):
typecode = chr(octets[0]) memv
= memoryview(octets[1:]).cast(typecode) return
cls(*memv)
Cree una vista de memoria a partir de la secuencia binaria de octetos y use el código de tipo para
lanzarlo.4
Debido a que acabamos de usar un decorador de método de clase , y es muy específico de Python, tengamos
una palabra al respecto.
Comencemos con el método de clase. El ejemplo 9-3 muestra su uso: para definir un método que opera
en la clase y no en las instancias. classmethod cambia la forma en que se llama al método,
por lo que recibe la propia clase como primer argumento, en lugar de una instancia. Su más com-
mon use es para constructores alternativos, como frombytes en el ejemplo 9-3. Tenga en cuenta cómo el
última línea de frombytes en realidad usa el argumento cls invocándolo para construir un nuevo
instancia: cls(*memv). Por convención, el primer parámetro de un método de clase debe ser
named cls (pero a Python no le importa cómo se nombra).
Por el contrario, el decorador de métodos estáticos cambia un método para que no reciba ningún cambio especial.
primer argumento. En esencia, un método estático es como una simple función que le sucede a
vive en un cuerpo de clase, en lugar de definirse a nivel de módulo. Ejemplo 9-4 contrastes
el funcionamiento de classmethod y staticmethod.
... @métodoclase
... def klassmeth(*argumentos):
... devolver argumentos
... # @staticmethod
... def declaración(*argumentos):
... devolver argumentos #
...
>>> Demo.klassmeth() #
(<clase '__main__.Demo'>,)
>>> Demostración.klassmeth('spam')
(<clase '__main__.Demo'>, 'correo no deseado')
>>> Demostración.statmeth()
# ()
4. Tuvimos una breve introducción a memoryview, explicando su método .cast en “Vistas de memoria” en la página 51.
>>> Demo.statmeth('spam')
('spam',)
Ahora que hemos visto para qué es bueno el método de clase (y que el método estático no
es muy útil), volvamos al tema de la representación de objetos y veamos cómo admitir la
salida formateada.
Pantallas formateadas
La función incorporada format() y el método str.format() delegan el formato real a cada tipo
llamando a su método .__format__(format_spec) . format_spec es un especificador de
formato, que es:
de los dos puntos en un campo de reemplazo delimitado con {} dentro de una cadena de formato utilizada con
str.format()
Por ejemplo:
0.4115226337448559
>>> formato(brl, '0.4f') # '0.4115'
5. Leonardo Rochael, uno de los revisores técnicos de este libro, no está de acuerdo con mi baja opinión sobre el método
estático y recomienda la publicación de blog “La guía definitiva sobre cómo usar métodos estáticos, de clase o abstractos
en Python” de Julien Danjou como contrapartida . argumento. La publicación de Danjou es muy buena; lo recomiendo
Pero no fue suficiente para cambiar de opinión sobre el método estático. Tendrás que decidir por ti mismo.
La segunda llamada destaca un punto importante: una cadena de formato como '{0.mass: 5.3e}' en
realidad usa dos notaciones separadas. El '0.mass' a la izquierda de los dos puntos es la parte del
nombre_campo de la sintaxis del campo de reemplazo; el '5.3e' después de los dos puntos es el
especificador de formato. La notación utilizada en el especificador de formato se denomina
minilenguaje de especificación de formato.
Si una clase no tiene __formato__, el método heredado de objeto devuelve str(mi_objeto). Debido
a que Vector2d tiene un __str__, esto funciona:
Arreglaremos eso implementando nuestro propio mini-lenguaje de formato. El primer paso será asumir que
el especificador de formato proporcionado por el usuario está destinado a formatear cada componente
flotante del vector. Este es el resultado que queremos:
El ejemplo 9-5 implementa __format__ para producir las pantallas que se acaban de mostrar.
Utilice el formato incorporado para aplicar fmt_spec a cada componente vectorial, creando una
iteración de cadenas formateadas.
Con eso, podemos mejorar nuestro __format__ para producir coordenadas polares. Ver
Ejemplo 9-6.
Ejemplo 9-6. Método Vector2d.format, toma #2, ahora con coordenadas polares
Ahora pasemos a un tema que no se trata solo de apariencias: haremos que nuestro Vec tor2d sea hashable, para
que podamos construir conjuntos de vectores o usarlos como claves de dictado . Pero antes de que podamos hacer
eso, debemos hacer que los vectores sean inmutables. Haremos lo que sea necesario a continuación.
Un Hashable Vector2d
Como se definió, hasta ahora nuestras instancias de Vector2d no se pueden modificar, por lo que no podemos ponerlas en un conjunto:
Para hacer un Vector2d hashable, debemos implementar __hash__ ( también se requiere __eq__, y ya lo tenemos).
También necesitamos hacer que las instancias vectoriales sean inmutables, como vimos en "¿Qué es Hashable?"
en la página 65.
En este momento, cualquiera puede hacer v1.x = 7 y no hay nada en el código que sugiera que está prohibido
cambiar un Vector2d . Este es el comportamiento que queremos:
Lo haremos haciendo que los componentes x e y tengan propiedades de solo lectura en el ejemplo 9-7.
Ejemplo 9-7. vector2d_v3.py: aquí solo se muestran los cambios necesarios para que Vector2d sea inmutable; ver el
listado completo en el Ejemplo 9-9
clase Vector2d :
código de tipo = 'd'
@property
def x(self):
return self.__x
@property
def y(self):
return self.__y
def __iter__(self):
return (i for i in (self.x, self.y))
Use exactamente dos guiones bajos al principio (con cero o un guión bajo al final) para hacer que
un atributo sea privado.6 El decorador @property marca el método getter de una propiedad.
Todos los métodos que solo leen los componentes x, y pueden permanecer como estaban, leyendo
las propiedades públicas a través de self.x y self.y en lugar del atributo privado, por lo que esta
lista omite el resto del código de la clase.
Ahora que nuestros vectores son razonablemente inmutables, podemos implementar el método __hash__ .
Debería devolver un int e idealmente tener en cuenta los valores hash de los atributos del objeto que
también se usan en el método __eq__ , porque los objetos que se comparan iguales deberían tener el
mismo valor hash. La documentación del método especial __hash__ sugiere usar el operador XOR bit a
bit (^) para mezclar los hash de los componentes, así que eso es lo que hacemos. El código para nuestro
método Vector2d.__hash__ es realmente simple, como se muestra en el Ejemplo 9-8.
def __hash__(self):
return hash(self.x) ^ hash(self.y)
6. No es así como lo haría Ian Bicking; recuerda la cita al comienzo del capítulo. Los pros y los contras de los
atributos privados son el tema de los próximos "Atributos privados y "protegidos" en Python" en la página 262.
(7, 384307168202284039)
>>> conjunto([v1, v2])
{Vector2d(3.1, 4.2), Vector2d(3.0, 4.0)}
Si está creando un tipo que tiene un valor numérico escalar sensible, también puede implementar los métodos
__int__ y __float__ , invocados por los constructores int() y float() , que se utilizan para la coacción de tipos en
algunos contextos. También hay un método __com plex__ para admitir el constructor integrado complex() .
Tal vez Vector2d debería proporcionar __complejo__, pero lo dejaré como ejercicio para usted.
Hemos estado trabajando en Vector2d durante un tiempo, mostrando solo fragmentos, por lo que el Ejemplo
9-9 es una lista completa y consolidada de vector2d_v3.py, incluidas todas las pruebas de documentos que
utilicé al desarrollarlo.
>>> x, y = v1
>>> x, y
(3.0, 4.0)
>>> v1
Vector2d(3.0, 4.0) >>>
v1_clone = eval(repr(v1)) >>> v1 ==
v1_clone True >>> print(v1) (3.0, 4.0)
>>> octetos = bytes(v1) >>> octetos
b'd\\x00\\x00\\x00\\x00\\x00\\x00\
\x08@\ \x00\\x00\\x00\\x00\\x00\\x00\
\x10@' > >> abdominales (v1) 5.0
>>> formato(v1)
'(3.0, 4.0)' >>>
formato(v1, '.2f') '(3.00, 4.00)'
>>> formato(v1, '.3e') '(3.000e+
00, 4.000e+00)'
Pruebas de hashing:
"""
clase Vector2d:
código de tipo = 'd'
@property
def x(self):
return self.__x
@property
def y(self):
return self.__y
def __iter__(self):
return (i for i in (self.x, self.y))
def __repr__(self):
class_name = type(self).__name__
return '{}({!r}, {!r})'.format(class_name, *self)
def __str__(yo):
return str(tupla(yo))
def __bytes__(self):
return (bytes([ord(self.typecode)]) +
bytes(matriz(self.typecode, self)))
def __hash__(self):
return hash(self.x) ^ hash(self.y)
def __abs__(self):
return math.hypot(self.x, self.y)
def __bool__(auto):
return bool(abs(auto))
@classmethod
def frombytes(cls, octetos):
typecode = chr(octets[0]) memv
= memoryview(octets[1:]).cast(typecode) return
cls(*memv)
Para recapitular, en esta sección y en las anteriores, vimos algunos métodos especiales esenciales que
quizás desee implementar para tener un objeto completo. Por supuesto, es una mala idea implementar todos
estos métodos si su aplicación no tiene un uso real para ellos. A los clientes no les importa si sus objetos
son "Pythonic" o no.
Como se codificó en el Ejemplo 9-9, Vector2d es un ejemplo didáctico con una larga lista de métodos
especiales relacionados con la representación de objetos, no una plantilla para cada clase definida por el usuario.
En la siguiente sección, tomaremos un descanso de Vector2d para analizar el diseño y los inconvenientes
del mecanismo de atributo privado en Python: el prefijo de doble guión bajo en self.__x.
Considere este escenario: alguien escribió una clase llamada Perro que usa un atributo de instancia de
estado de ánimo internamente, sin exponerlo. Necesitas subclasificar a Dog como Beagle. Si crea su propio
atributo de instancia de estado de ánimo sin darse cuenta del conflicto de nombres, aplastará el atributo de
estado de ánimo utilizado por los métodos heredados de Dog. Esto sería un dolor para depurar.
Para evitar esto, si nombra un atributo de instancia en la forma __estado de ánimo (dos guiones bajos al
principio y cero o como máximo un guión bajo al final), Python almacena el nombre en la instancia __dict__
prefijado con un guión bajo al principio y el nombre de la clase, por lo que en el
Clase de perros , __mood se convierte en _Dog__mood, y en Beagle es _Beagle__mood. Esta característica del
lenguaje se conoce con el encantador nombre de manipulación de nombres.
Ejemplo 9-10. Los nombres de atributos privados se “destrozan” anteponiendo el prefijo _ y la clase
nombre
>>> v1 = Vector2d(3, 4)
>>> v1.__dict__
{'_Vector2d__y': 4.0, '_Vector2d__x': 3.0} >>>
v1._Vector2d__x 3.0
La manipulación de nombres tiene que ver con la seguridad, no con la seguridad: está diseñada para evitar el acceso
accidental y no las malas acciones intencionales (la Figura 9-1 ilustra otro dispositivo de seguridad).
Figura 9-1. Una tapa en un interruptor es un dispositivo de seguridad, no de seguridad: evita la activación accidental,
no el uso malicioso
Cualquiera que sepa cómo se alteran los nombres privados puede leer el atributo privado directamente, como muestra
la última línea del Ejemplo 9-10 ; eso es realmente útil para la depuración y serialización. También pueden asignar
directamente un valor a un componente privado de un Vector2d simplemente escribiendo v1._Vector__x = 7. Pero si
está haciendo eso en el código de producción, no puede quejarse si algo explota.
La funcionalidad de modificación de nombres no es amada por todos los Pythonistas, y tampoco lo es el aspecto
sesgado de los nombres escritos como self.__x. Algunos prefieren evitar esta sintaxis y usan solo un prefijo de
subrayado para "proteger" los atributos por convención (por ejemplo, self._x). Los críticos de la manipulación automática
de doble guión bajo sugieren que las preocupaciones sobre la destrucción accidental de atributos deben abordarse
mediante convenciones de nomenclatura. Esta es la cita completa del prolífico Ian Bicking, citada al comienzo de este
capítulo:
Nunca, nunca use dos guiones bajos al principio. Esto es irritantemente privado. Si le preocupan los
conflictos de nombres, utilice la manipulación explícita de nombres en su lugar (p. ej.,
_MyThing_blahblah). Esto es esencialmente lo mismo que el doble guión bajo, solo que es transparente
donde el doble guión bajo oscurece.7
El prefijo de subrayado único no tiene un significado especial para el intérprete de Python cuando
se usa en nombres de atributos, pero es una convención muy fuerte entre los programadores de
Python que no debe acceder a tales atributos desde fuera de la clase.8 Es fácil respetar la privacidad
de un objeto que marca sus atributos con un solo _, al igual que es fácil respetar la convención de
que las variables en ALL_CAPS deben tratarse como constantes.
Para concluir: los componentes de Vector2d son "privados" y nuestras instancias de Vector2d son
"inmutables", entre comillas, porque no hay forma de hacerlos realmente privados e inmutables.10
Ahora regresaremos a nuestra clase Vector2d . En esta sección final, cubrimos un atributo especial (no un método)
que afecta el almacenamiento interno de un objeto, con un impacto potencialmente enorme en el uso de la memoria
pero poco efecto en su interfaz pública: __slots__.
8. En los módulos, un solo _ delante de un nombre de nivel superior tiene un efecto: si escribe desde mymod import *, los nombres con un
prefijo _ no se importan desde mymod. Sin embargo, aún puede escribir desde mymod import
_privatefunc. Esto se explica en el Tutorial de Python, sección 6.1. Más sobre Módulos.
10. Si este estado de cosas lo deprime y le hace desear que Python se parezca más a Java en este sentido, no lea mi discusión sobre la
fuerza relativa del modificador privado de Java en "Soapbox" en la página 272.
Para definir __slots__, crea un atributo de clase con ese nombre y asígnele un iterable de str con
identificadores para los atributos de instancia. Me gusta usar una tupla para eso, porque transmite
el mensaje de que la definición de __slots__ no puede cambiar. Vea el Ejemplo 9-11.
Al definir __slots__ en la clase, le está diciendo al intérprete: "Estos son todos los atributos de
instancia en esta clase". Luego, Python los almacena en una estructura similar a una tupla en
cada instancia, evitando la sobrecarga de memoria del __dict__ por instancia. Esto puede marcar
una gran diferencia en el uso de la memoria si tiene millones de instancias activas al mismo
tiempo.
El ejemplo 9-12 muestra dos ejecuciones de un script que simplemente crea una lista, usando
una comprensión de lista, con 10,000,000 de instancias de Vector2d. El script mem_test.py toma
el nombre de un módulo con una variante de clase Vector2d como argumento de línea de
comandos. En la primera ejecución, estoy usando vector2d_v3.Vector2d (del Ejemplo 9-7); en la
segunda ejecución, se usa la versión __slots__ de vector2d_v3_slots.Vector2d .
Ejemplo 9-12. mem_test.py crea 10 millones de instancias de Vector2d utilizando la clase definida
en el módulo nombrado (por ejemplo, vector2d_v3.py)
$ tiempo python3 mem_test.py vector2d_v3.py Tipo de
Vector2d seleccionado : vector2d_v3.Vector2d Creando
10,000,000 instancias de Vector2d Uso inicial de RAM:
5,623,808
reales 0m16.721s
usuario 0m15.568s
sys 0m1.149s $ time
python3 mem_test.py vector2d_v3_slots.py Tipo de Vector2d
seleccionado : vector2d_v3_slots.Vector2d Creación de 10 000 000 instancias
de Vector2d Uso inicial de RAM: 5 718 016 Uso final de RAM: 655 466 496
reales 0m13.605s
usuario 0m13.163s
sistema 0m0.434s
Como revela el ejemplo 9-12 , la huella de RAM del script aumenta a 1,5 GB cuando se usa la instancia
__dict__ en cada una de las 10 millones de instancias de Vector2d , pero se reduce a 655 MB cuando
Vector2d tiene un atributo __slots__ . La versión __slots__ también es más rápida.
El script mem_test.py en esta prueba básicamente se ocupa de cargar un módulo, verificar el uso de
la memoria y formatear los resultados. El código no es realmente relevante aquí, por lo que se
encuentra en el Apéndice A, Ejemplo A-4.
Sin embargo, puede ser posible "guardar memoria y comérsela también": si agrega el nombre
'__dict__' a la lista de __ranuras__ , sus instancias mantendrán los atributos nombrados en __ranuras__
en la tupla por instancia, pero también admitirán dinámicamente atributos creados, que se almacenarán
en el __dict__ habitual. Por supuesto, tener '__dict__' en __slots__ puede frustrar por completo su
propósito, dependiendo de la cantidad de atributos estáticos y dinámicos en cada instancia y cómo se
usan. La optimización descuidada es incluso peor que la optimización prematura.
Hay otro atributo especial por instancia que quizás desee mantener: el atributo __weak ref__ es
necesario para que un objeto admita referencias débiles (cubierto en “Referencias débiles” en la página
236). Ese atributo está presente de forma predeterminada en instancias de clases definidas por el
usuario. Sin embargo, si la clase define __slots__ y necesita que las instancias sean objetivos de
referencias débiles, entonces debe incluir '__weakref__' entre los atributos nombrados en __slots__.
Para resumir, __slots__ tiene algunas advertencias y no debe abusarse solo para limitar los atributos
que los usuarios pueden asignar. Es principalmente útil cuando se trabaja con datos tabulares, como
registros de bases de datos, donde el esquema es fijo por definición y los conjuntos de datos pueden
ser muy grandes. Sin embargo, si realiza este tipo de trabajo con frecuencia, debe consultar no solo
NumPy, sino también la biblioteca de análisis de datos de pandas, que puede manejar datos no
numéricos e importar/exportar a muchos formatos de datos tabulares diferentes.
• Debe recordar volver a declarar __slots__ en cada subclase, porque el intérprete ignora el atributo
heredado. • Las instancias solo podrán tener los atributos enumerados en __slots__, a menos que
incluya '__dict__' en __slots__ (pero hacerlo puede anular el ahorro de memoria). • Las instancias no
pueden ser objetivos de referencias débiles a menos que recuerde incluir
'__weakref__' en __slots__.
Si su programa no está manejando millones de instancias, probablemente no valga la pena crear una
clase algo inusual y engañosa cuyas instancias pueden no aceptar atributos dinámicos o no admitir
referencias débiles. Como cualquier optimización, __slots__ debe usarse solo si lo justifica una
necesidad presente y cuando su beneficio se demuestra mediante un perfilado cuidadoso.
El último tema de este capítulo tiene que ver con anular un atributo de clase en instancias y subclases.
Pero si escribe en un atributo de instancia que no existe, crea un nuevo atributo de instancia, por
ejemplo, un atributo de instancia de código tipo, y el atributo de clase con el mismo nombre no se
modifica. Sin embargo, a partir de ese momento, cada vez que el código que maneja esa instancia lea
self.typecode, se recuperará el código de tipo de la instancia , ocultando efectivamente el atributo de
clase con el mismo nombre. Esto abre la posibilidad de personalizar una instancia individual con un
código de tipo diferente .
El Vector2d.typecode predeterminado es 'd', lo que significa que cada componente del vector se representará como un flotante de
doble precisión de 8 bytes al exportar a bytes. Si configuramos el código de tipo de una instancia de Vector2d en 'f' antes de
exportar, cada componente se exportará como un flotante de precisión simple de 4 bytes. El ejemplo 9-13 lo demuestra.
Ejemplo 9-13. Personalizar una instancia configurando el atributo de código de tipo que anteriormente se heredó de la clase
Ahora debería quedar claro por qué la exportación de bytes de un Vector2d tiene el prefijo del código de tipo: queríamos admitir
Si desea cambiar un atributo de clase, debe configurarlo directamente en la clase, no a través de una instancia. Puede cambiar el
código de tipo predeterminado para todas las instancias (que no tienen su propio código de tipo) haciendo esto:
Sin embargo, existe una forma idiomática de Python de lograr un efecto más permanente y ser más explícito sobre el cambio.
Debido a que los atributos de clase son públicos, son heredados por las subclases, por lo que es una práctica común crear
atributo. Las vistas basadas en clases de Django utilizan esta técnica ampliamente. El ejemplo 9-14
muestra cómo.
Ejemplo 9-14. ShortVector2d es una subclase de Vector2d, que solo sobrescribe el código de tipo
predeterminado
Cree ShortVector2d como una subclase de Vector2d solo para sobrescribir el atributo de clase de
código de tipo .
Este ejemplo también explica por qué no codifiqué class_name en Vec to2d.__repr__, sino que lo obtuve
de type(self).__name__, así:
def __repr__(self):
class_name = type(self).__name__
return '{}({!r}, {!r})'.format(class_name, *self)
Si hubiera codificado el class_name, las subclases de Vector2d como ShortVector2d tendrían que
sobrescribir __repr__ solo para cambiar el class_name. Al leer el nombre del tipo de instancia, hice que
__repr__ fuera más seguro de heredar.
Esto finaliza nuestra cobertura de la implementación de una clase simple que aprovecha el modelo de
datos para funcionar bien con el resto de Python, ofreciendo diferentes representaciones de objetos,
implementando un código de formato personalizado, exponiendo atributos de solo lectura y admitiendo
hash() para integrarse con conjuntos y asignaciones.
¿Es vector2d_v3.py (Ejemplo 9-9) más Pythonic que vector2d_v0.py (Ejemplo 9-2)?
La clase Vector2d en vector2d_v3.py ciertamente exhibe más características de Python. Pero
Un objeto Pythonic debe ser tan simple como lo permitan los requisitos, y no un desfile de
características del lenguaje.
Pero mi objetivo al expandir el código Vector2d era brindar contexto para discutir los métodos
especiales de Python y las convenciones de codificación. Si mira hacia atrás en la Tabla 1-1, los
varios listados en este capítulo demostraron:
• El operador __eq__ , para probar la conversión de bytes y habilitar el hashing (junto con
__picadillo__).
En la preparación para hacer que las instancias de Vector2d fueran hash, hicimos un esfuerzo para
hacerlas inmutables, al menos evitando cambios accidentales codificando los atributos x e y como
privados y exponiéndolos como propiedades de solo lectura. Luego implementamos __hash__
usando la técnica recomendada de xor-ing los hash de los atributos de la instancia.
El último tema que cubrimos fue la anulación de un atributo de clase al que se accede a través de las
instancias (por ejemplo, self.typecode). Lo hicimos primero creando un atributo de instancia y luego
subclasificando y sobrescribiendo en el nivel de clase.
A lo largo del capítulo, mencioné cómo se informaron las opciones de diseño en los ejemplos al estudiar
la API de los objetos estándar de Python. Si este capítulo se puede resumir en una frase, es ésta:
Para construir objetos Pythonic, observe cómo se comportan los objetos Python reales.
Otras lecturas
Este capítulo cubrió varios métodos especiales del modelo de datos, por lo que, naturalmente, las
referencias principales son las mismas que las proporcionadas en el Capítulo 1, que brinda una visión
de alto nivel del mismo tema. Para mayor comodidad, repetiré esas cuatro recomendaciones anteriores
aquí y agregaré algunas otras:
En este capítulo, cubrimos todos los métodos especiales relacionados con la representación de objetos,
excepto __index__. Se usa para obligar a un objeto a un índice entero en el contexto específico del
corte de secuencias y se creó para resolver una necesidad en NumPy. En la práctica, es probable que
usted y yo no necesitemos implementar __index__ a menos que decidamos escribir un nuevo tipo de
datos numéricos y queramos que se pueda usar como argumentos para __getitem__. Si tiene curiosidad
al respecto, What's New in Python 2.5 de AM Kuchling tiene una breve explicación, y PEP 357: permitir
que cualquier objeto se use para cortar detalla la necesidad de __index__, desde la perspectiva de un
implementador de una extensión C, Travis Oliphant, el autor principal de NumPy.
Una primera realización de la necesidad de representaciones de cadenas distintas para los objetos
apareció en Smalltalk. El artículo de 1996 "Cómo mostrar un objeto como una cadena: printString y
displayString" de Bobby Woolf analiza la implementación de los métodos printString y displayString en
ese lenguaje. De ese artículo, tomé prestada la descripción concisa.
cripciones "la forma en que el desarrollador quiere verlo" y "la forma en que el usuario quiere
verlo" al definir repr() y str() en "Representaciones de objetos" en la página 248.
Plataforma improvisada
En las versiones iniciales de Vector2d, los atributos x e y eran públicos, al igual que todos los atributos de clase e
instancia de Python de forma predeterminada. Naturalmente, los usuarios de vectores deben poder acceder a sus
componentes. Aunque nuestros vectores son iterables y se pueden descomprimir en un par de variables, también
es deseable poder escribir my_vector.x y my_vector.y para obtener cada componente.
Esto demuestra que siempre podemos comenzar nuestras clases de la manera más simple posible, con atributos
públicos, porque cuando (o si) más tarde necesitamos imponer más control con getters y setters, estos pueden
implementarse a través de propiedades sin cambiar nada del código que ya interactúa con nuestros objetos a
través de los nombres (por ejemplo, x e y) que inicialmente eran simples atributos públicos.
Este enfoque es lo opuesto al recomendado por el lenguaje Java: un programador de Java no puede comenzar
con atributos públicos simples y luego, si es necesario, implementar propiedades, porque no existen en el lenguaje.
Por lo tanto, escribir getters y setters es la norma en Java, incluso cuando esos métodos no hacen nada útil,
porque la API no puede evolucionar de simples atributos públicos a getters y setters sin romper todo el código que
usa esos atributos.
Además, como señala nuestro revisor técnico Alex Martelli, escribir llamadas getter/setter en todas partes es una
tontería. Tienes que escribir cosas como:
---
>>> mi_objeto.set_foo(mi_objeto.get_foo() + 1)
---
---
>>> mi_objeto.foo += 1
---
Ward Cunningham, inventor del wiki y pionero de la Programación Extrema, recomienda preguntar "¿Qué es lo
más simple que podría funcionar?" la idea es
concéntrese en la meta.11 Implementar setters y getters desde el principio es una distracción de la meta. En
Python, podemos simplemente usar atributos públicos sabiendo que podemos cambiarlos a propiedades más
tarde, si surge la necesidad.
Habiendo enseñado Python a muchos programadores de Java a lo largo de los años, he descubierto que
muchos de ellos confían demasiado en las garantías de privacidad que ofrece Java. Como resultado, los
modificadores privados y protegidos de Java normalmente solo brindan protección contra accidentes (es decir,
seguridad). Solo pueden garantizar la seguridad contra intenciones maliciosas si la aplicación se implementa
con un administrador de seguridad, y eso rara vez sucede en la práctica, incluso en entornos corporativos.
Para probar mi punto, me gusta mostrar esta clase de Java (Ejemplo 9-15).
Ejemplo 9-15. Confidential.java: una clase Java con un campo privado llamado secreto
clase pública confidencial {
En el Ejemplo 9-15, guardo el texto en el campo secreto después de convertirlo a mayúsculas, solo para que
sea obvio que todo lo que esté en ese campo estará en mayúsculas.
La demostración real consiste en ejecutar la exposición.py con Jython. Ese script usa la introspección
("reflexión" en lenguaje Java) para obtener el valor de un campo privado. El código está en el Ejemplo 9-16.
Ejemplo 9-16. exposición.py: código Jython para leer el contenido de un campo privado en
otra clase
importar Confidencial
11. Consulte “Lo más simple que podría funcionar: una conversación con Ward Cunningham, Parte V”.
$ jython exponer.py
mensaje.secret = TEXTO SECRETO SUPERIOR
La cadena 'TEXTO SECRETO MÁXIMO' se leyó del campo privado secreto de la clase Confidencial .
No hay magia negra aquí: exposición.py usa la API de reflexión de Java para obtener una referencia al campo
privado llamado 'secreto', y luego llama a 'secret_field.setAccessi ble(True)' para que sea legible. Lo mismo se
puede hacer con el código Java, por supuesto (pero se necesitan más del triple de líneas para hacerlo; vea el
archivo Expose.java en el repositorio de código de Fluent Python ).
La llamada crucial .setAccessible(True) fallará solo si el script Jython o el programa principal de Java (por ejemplo,
Expose.class) se ejecuta bajo la supervisión de un SecurityManager.
Pero en el mundo real, las aplicaciones de Java rara vez se implementan con un SecurityManager, a excepción
de los subprogramas de Java (¿los recuerda?).
Mi punto es: en Java también, los modificadores de control de acceso son principalmente sobre seguridad y no
seguridad, al menos en la práctica. Así que relájate y disfruta del poder que Python te da. Úselo responsablemente.
CAPÍTULO 10
No verifique si es un pato: verifique si grazna como un pato, camina como un pato, etc., etc.,
dependiendo exactamente de qué subconjunto de comportamiento de pato necesita para jugar
sus juegos de lenguaje. con. (comp.lang.python, 26 de julio de 2000)
— Alex Martelli
En este capítulo, crearemos una clase para representar una clase Vector multidimensional, un avance
significativo respecto al Vector2d bidimensional del Capítulo 9. Vector se comportará como una secuencia
plana inmutable estándar de Python. Sus elementos serán flotantes y admitirá lo siguiente al final de este
capítulo:
También implementaremos el acceso a atributos dinámicos con __getattr__ como una forma de
reemplazar las propiedades de solo lectura que usamos en Vector2d, aunque esto no es típico de los
tipos de secuencia.
La presentación intensiva en código será interrumpida por una discusión conceptual sobre la idea de los
protocolos como una interfaz informal. Hablaremos sobre cómo se relacionan los protocolos y la
tipificación pato , y sus implicaciones prácticas cuando crea sus propios tipos.
Empecemos.
275
Machine Translated by Google
¿Quién necesita un vector con 1000 dimensiones? Pista: ¡no artistas 3D! Sin embargo, los vectores n
dimensionales (con valores grandes de n) se utilizan ampliamente en la recuperación de información, donde
los documentos y las consultas de texto se representan como vectores, con una dimensión por palabra. Esto
se llama el modelo de espacio vectorial. En este modelo, una métrica de relevancia clave es la similitud del
coseno (es decir, el coseno del ángulo entre un vector de consulta y un vector de documento). A medida que
el ángulo disminuye, el coseno se acerca al valor máximo de 1, y también lo hace la relevancia del documento
para la consulta.
Habiendo dicho eso, la clase Vector en este capítulo es un ejemplo didáctico y no haremos muchas
matemáticas aquí. Nuestro objetivo es solo demostrar algunos métodos especiales de Python en el contexto
de un tipo de secuencia.
NumPy y SciPy son las herramientas que necesita para las matemáticas vectoriales del mundo real. El
paquete gemsim de PyPI, de Radim Rehurek, implementa el modelado de espacio vectorial para el
procesamiento del lenguaje natural y la recuperación de información, utilizando NumPy y SciPy.
Nuestra estrategia para implementar Vector será usar composición, no herencia. Almacenaremos
los componentes en una matriz de flotantes e implementaremos los métodos necesarios para que
nuestro Vector se comporte como una secuencia plana inmutable.
Pero antes de implementar los métodos de secuencia, asegurémonos de tener una implementación
básica de Vector que sea compatible con nuestra clase Vector2d anterior , excepto cuando dicha
compatibilidad no tenga sentido.
La primera versión de Vector debería ser lo más compatible posible con nuestra clase anterior Vec
tor2d .
Sin embargo, por diseño, el constructor Vector no es compatible con el constructor Vector2d .
Podríamos hacer que Vector(3, 4) y Vector(3, 4, 5) funcionen tomando argumentos arbitrarios con
*args en __init__, pero la mejor práctica para un constructor de secuencias es tomar los datos como
un argumento iterable en el constructor. , como lo hacen todos los tipos de secuencia integrados. El
ejemplo 10-1 muestra algunas formas de instanciar nuestros nuevos objetos vectoriales .
>>> Vector(rango(10))
Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])
Aparte de la firma del nuevo constructor, me aseguré de que todas las pruebas que hice con Vector2d
(por ejemplo, Vector2d(3, 4)) pasaran y produjeran el mismo resultado con un Vec tor de dos componentes
([3, 4]).
El ejemplo 10-2 enumera la implementación de nuestra primera versión de Vector (este ejemplo se basa
en el código que se muestra en los ejemplos 9-2 y 9-3).
clase vectorial:
código de tipo = 'd'
def __iter__(self):
return iter(self._components)
def __repr__(self):
componentes = reprlib.repr(self._components)
componentes = componentes[componentes.find('['):-1] return
'Vector({})'.format(componentes)
def __bytes__(self):
return (bytes([ord(self.typecode)]) +
bytes(self._components))
def __abs__(self):
return math.sqrt(sum(x * x for x in self))
def __bool__(auto):
return bool(abs(auto))
@classmethod
def frombytes(cls, octetos):
typecode = chr(octets[0]) memv
= memoryview(octets[1:]).cast(typecode) return
cls(memv)
Elimine la matriz ('d', el prefijo y el final ) antes de conectar la cadena en una llamada de
constructor de Vector .
Ya no podemos usar hipot , así que sumamos los cuadrados de los componentes y
calculamos el sqrt de eso.
El único cambio necesario de los frombytes anteriores está en la última línea: pasamos la
vista de memoria directamente al constructor, sin desempaquetar con * antes. como lo hicimos nosotros
La forma en que usé reprlib.repr merece algo de elaboración. Esa función produce representaciones
seguras de estructuras grandes o recursivas limitando la longitud de la cadena de salida y marcando
el corte con '...'. Quería que la representación de un Vector se pareciera a Vector([3.0, 4.0, 5.0]) y
no a Vector(array('d', [3.0, 4.0, 5.0])), porque el hecho de que haya una matriz dentro de un Vector
es un detalle de implementación. Debido a que estas llamadas al constructor construyen objetos
vectoriales idénticos , prefiero la sintaxis más simple usando un argumento de lista .
directamente y luego corte los caracteres fuera del []. Eso es lo que hace la segunda línea de
__repr__ en el ejemplo 10-2.
Tenga en cuenta que los métodos __str__, __eq__ y __bool__ no han cambiado desde Vector2d, y
solo se cambió un carácter en frombytes (se eliminó un * en la última línea).
Este es uno de los beneficios de hacer que el Vector2d original sea iterable.
Por cierto, podríamos haber subclasificado Vector de Vector2d, pero decidí no hacerlo por dos
razones. Primero, los constructores incompatibles realmente hacen que la creación de subclases no
sea aconsejable. Podría solucionar eso con un manejo inteligente de parámetros en __init__, pero la
segunda razón es más importante: quiero que Vector sea un ejemplo independiente de una clase que
implementa el protocolo de secuencia. Eso es lo que haremos a continuación, después de una
discusión sobre el término protocolo.
Ejemplo 10-3. Código del Ejemplo 1-1, reproducido aquí por conveniencia
importar colecciones
clase FrenchDeck:
rangos = [str(n) for n in range(2, 11)] + list('JQKA') palos = 'picas
diamantes tréboles corazones'.split()
def __len__(self):
return len(self._cards)
La clase FrenchDeck del ejemplo 10-3 aprovecha muchas funciones de Python porque implementa el
protocolo de secuencia, incluso si no está declarado en ninguna parte del código.
Cualquier programador de Python con experiencia lo mirará y entenderá que es una secuencia, incluso
si subclasifica objetos. Decimos que es una secuencia porque se comporta como tal, y eso es lo que
importa.
Esto se conoció como digitación de pato, después de la publicación de Alex Martelli citada al comienzo
de este capítulo.
Debido a que los protocolos son informales y no se aplican, a menudo puede salirse con la suya
implementando solo una parte de un protocolo, si conoce el contexto específico donde se usará una
clase. Por ejemplo, para admitir la iteración, solo se requiere __getitem__ ; no es necesario
proporcionar __len__.
clase vectorial:
# muchas lineas omitidas
# ...
def __len__(self):
return len(self._components)
>>> matriz
v7[1:4] ('d', [1.0, 2.0, 3.0])
Como puede ver, incluso se admite el corte, pero no muy bien. Sería mejor si una porción de Vector también
fuera una instancia de Vector y no una matriz. La antigua clase FrenchDeck tiene un problema similar:
cuando la cortas, obtienes una lista. En el caso de Vector, se pierde mucha funcionalidad cuando el corte
produce matrices simples.
Considere los tipos de secuencia incorporados: cada uno de ellos, cuando se divide, produce una nueva
instancia de su propio tipo, y no de algún otro tipo.
Para hacer que Vector produzca cortes como instancias de Vector , no podemos simplemente delegar el
corte a la matriz. Necesitamos analizar los argumentos que obtenemos en __getitem__ y hacer lo correcto.
Ahora, veamos cómo Python convierte la sintaxis my_seq[1:3] en argumentos para my_seq.__getitem__(...).
Una demostración vale más que mil palabras, así que observe el Ejemplo 10-4.
>>> s[1:4] #
segmento(1, 4,
Ninguno) >>>
s[1:4:2] # segmento(1,
4, 2) >>> s[1:4:2, 9 ] #
(sector(1, 4, 2), 9) >>>
s[1:4:2, 7:9] #
(segmento(1, 4, 2), segmento(7, 9, Ninguno))
Sorpresa: la presencia de comas dentro de [] significa que __getitem__ recibe una tupla.
>>> segmento
# <clase
'segmento'> >>>
dir(segmento) # ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__getattribute__ ', '__gt__', '__hash__', '__init__',
'__le__', '__lt__', '__ne__', '__nuevo__', '__reduce__', '__reduce_ex__',
'__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__',
'índices', 'inicio', 'paso', 'detener']
slice es un tipo incorporado (lo vimos por primera vez en "Objetos de división" en la página 34).
Al inspeccionar un segmento , encontramos los atributos de datos inicio, parada y paso, y un método de
índices .
En el Ejemplo 10-5, llamar a dir(segmento) revela un atributo de índices , que resulta ser un método muy interesante
pero poco conocido. Esto es lo que revela help(slice.indices) :
En otras palabras, los índices exponen la lógica engañosa que se implementa en las secuencias integradas para
manejar correctamente los índices faltantes o negativos y los segmentos que son más largos que la secuencia de
destino. Este método produce tuplas "normalizadas" de números enteros no negativos de inicio, parada y paso
ajustados para encajar dentro de los límites de una secuencia de la longitud dada.
Aquí hay un par de ejemplos, considerando una secuencia de len == 5, por ejemplo, 'ABCDE':
En nuestro código Vector , no necesitaremos el método slice.indices() porque cuando obtengamos un argumento
de división, delegaremos su manejo a la matriz _components. Pero si no puede contar con los servicios de una
secuencia subyacente, este método puede ahorrarle mucho tiempo.
Ahora que sabemos cómo manejar las porciones, echemos un vistazo a la implementación mejorada de
Vector.__ge titem__ .
Aware enumera los dos métodos necesarios para hacer que Vector se comporte como una secuencia: __len__
y __getitem__ (este último ahora implementado para manejar el corte correctamente).
Ejemplo 10-6. Parte de vector_v2.py: se agregaron los métodos __len__ y __getitem__ a la clase Vector desde
vector_v1.py (ver Ejemplo 10-2)
def __len__(self):
return len(self._components)
porción... ...invoque la clase para construir otra instancia de Vector a partir de una porción de la matriz
_components .
El uso excesivo de isinstance puede ser un signo de mal diseño OO, pero
el manejo de segmentos en __getitem__ es un caso de uso justificado.
Nótese en el ejemplo 10-6 la prueba contra números. Integral: una clase
base abstracta. El uso de ABC en pruebas de instancia hace que una API
sea más flexible y preparada para el futuro . El capítulo 11 explica por qué.
Desafortunadamente, no hay ABC para slice en la biblioteca estándar de Python 3.4.
Para descubrir qué excepción generar en la cláusula else de __getitem__, utilicé la consola interactiva
para comprobar el resultado de 'ABC'[1, 2]. Luego supe que Python genera un TypeError, y también
copié la redacción del mensaje de error: "los índices deben ser números enteros". Para crear objetos
Pythonic, imite los propios objetos de Python.
Una vez que el código del Ejemplo 10-6 se agrega a la clase Vector , tenemos un comportamiento de
corte adecuado, como lo demuestra el Ejemplo 10-7 .
Vector no admite la indexación multidimensional, por lo que una tupla de índices o sectores
genera un error.
Esta es la sintaxis alternativa que queremos proporcionar para leer los primeros cuatro componentes de
un vector:
>>> v = Vector(rango(10))
>>> vx
0.0
>>> vy, vz, vt (1.0,
2.0, 3.0)
“El intérprete invoca el método __getattr__ cuando falla la búsqueda de atributos. En términos simples,
dada la expresión my_obj.x, Python verifica si la instancia de my_obj tiene un atributo llamado x; si no,
la búsqueda va a la clase (my_obj.__class__), y luego hacia arriba en el gráfico de herencia.2 Si no se
encuentra el atributo x , entonces se llama al método __getattr__ definido en la clase de my_obj con
self y el nombre del atributo como una cadena (por ejemplo, 'x').
El ejemplo 10-8 enumera nuestro método __getattr__ . Esencialmente, comprueba si el atributo que se
busca es una de las letras xyzt y, de ser así, devuelve el componente del vector correspondiente.
Ejemplo 10-8. Parte de vector_v3.py: método __getattr__ agregado a la clase Vector de vector_v2.py
atajo_nombres = 'xyzt'
Encuentra la posición del nombre de 1 letra; str.find también ubicaría 'yz' y no queremos eso,
esta es la razón de la prueba anterior.
Si falla alguna de las pruebas, genere AttributeError con un mensaje de texto estándar.
2. La búsqueda de atributos es más complicada que esto; veremos los detalles sangrientos en la Parte VI. Por ahora, esto simplificado
la explicación servirá.
Ejemplo 10-9. Comportamiento inapropiado: la asignación a vx no genera ningún error, pero introduce
una inconsistencia
>>> v = Vector(rango(5))
>>> v
Vector([0.0, 1.0, 2.0, 3.0, 4.0]) >>> vx #
0.0
>>> vx = 10 #
>>> vx #
10
>>> v
Vector([0.0, 1.0, 2.0, 3.0, 4.0]) #
¿Puedes explicar lo que está pasando? En particular, ¿por qué la segunda vez que vx devuelve 10 si
ese valor no está en la matriz de componentes del vector? Si no lo sabe desde el principio, estudie la
explicación de __getattr__ dada justo antes del Ejemplo 10-8. Es un poco sutil, pero es una base muy
importante para entender mucho de lo que viene más adelante en el libro.
La inconsistencia en el Ejemplo 10-9 se introdujo debido a la forma en que funciona __getattr__ : Python
solo llama a ese método como respaldo, cuando el objeto no tiene el atributo nombrado. Sin embargo,
después de que asignamos vx = 10, el objeto v ahora tiene un atributo x , por lo que ya no se llamará a
__getattr__ para recuperar vx: el intérprete simplemente devolverá el valor 10 que está vinculado a vx .
Por otro lado, nuestra implementación de __get attr__ no presta atención a los atributos de la instancia
que no sean self._components, desde donde recupera los valores de los "atributos virtuales" enumerados
en atajos_nombres.
Necesitamos personalizar la lógica para establecer atributos en nuestra clase Vector para evitar esta
inconsistencia.
Recuerde que en los últimos ejemplos de Vector2d del Capítulo 9, al intentar asignar a la instancia los
atributos .x o .y se generaba AttributeError. En Vector queremos la misma excepción con cualquier
intento de asignar a todos los nombres de atributos en minúsculas de una sola letra, solo para evitar
confusiones. Para hacerlo, implementaremos __setattr__ como se muestra en el ejemplo 10-10.
if len(name) == 1: if
name in cls.shortcut_names: error
= 'atributo de solo lectura {attr_name!r}' elif
name.islower(): error = "no se pueden establecer los
atributos 'a' a 'z' en {cls_name!r}" más:
error = ''
si error:
msg = error.format(cls_name=cls.__name__, attr_name=name) raise
AttributeError(msg) super().__setattr__(name, value)
Si el nombre está en minúsculas, establezca un mensaje de error sobre todos los nombres de una sola letra.
Mientras elegía el mensaje de error para mostrar con AttributeError, mi primera verificación fue el comportamiento del tipo complejo
integrado , porque son inmutables y tienen un par de atributos de datos reales e imag. Intentar cambiar cualquiera de ellos en una instancia
compleja genera AttributeError con el mensaje "no se puede establecer el atributo". Por otro lado, intentar establecer un atributo de solo
lectura protegido por una propiedad como hicimos en “Un Vector2d Hashable” en la página 257 produce el mensaje "atributo de solo
lectura". Me inspiré en ambos textos para establecer la cadena de error en __setitem__, pero fui más explícito sobre los atributos prohibidos.
Tenga en cuenta que no estamos deshabilitando la configuración de todos los atributos, solo de una sola letra, en minúsculas, para evitar
Incluso sin admitir la escritura en los componentes de Vector , aquí hay una conclusión importante
de este ejemplo: muy a menudo, cuando implementa __getattr__ , también necesita codificar
__setattr__ , para evitar un comportamiento inconsistente en sus objetos.
El __hash__ del Ejemplo 9-8 simplemente calculó hash(self.x) ^ hash(self.y). Ahora nos gustaría
aplicar el operador ^ (xor) a los valores hash de cada componente, en sucesión, así: v[0] ^ v[1] ^
v[2]…. Para eso está la función functools.reduce . Anteriormente dije que reduce no es tan popular
como antes,3 pero calcular el hash de todos los componentes del vector es un trabajo perfecto para
ello. La figura 10-1 muestra la idea general de la función reduce .
Figura 10-1. Las funciones reductoras (reduce, sum, any, all) producen un único resultado agregado
a partir de una secuencia o de cualquier objeto iterable finito.
3. La suma, cualquiera y todos cubren los usos más comunes de reducir. Vea la discusión en “Reemplazos modernos
para mapear, filtrar y reducir” en la página 142.
Hasta ahora hemos visto que functools.reduce() puede ser reemplazado por sum(), pero ahora vamos a
explicar correctamente cómo funciona. La idea clave es reducir una serie de valores a un solo valor. El
primer argumento de reduce() es una función de dos argumentos y el segundo argumento es iterable.
Digamos que tenemos una función fn de dos argumentos y una lista lst.
Cuando llame a reduce(fn, lst), fn se aplicará al primer par de elementos, fn(lst[0], lst[1]), produciendo un
primer resultado, r1. Luego se aplica fn a r1 y al siguiente elemento, fn(r1, lst[2]), lo que produce un
segundo resultado, r2. Ahora se llama a fn(r2, lst[3]) para producir r3 ... y así sucesivamente hasta el
último elemento, cuando se devuelve un único resultado, rN .
¡Así es como podrías usar reduce para calcular 5! (el factorial de 5):
Volviendo a nuestro problema de hash, el ejemplo 10-11 muestra la idea de calcular el xor agregado
haciéndolo de tres maneras: con un bucle for y dos llamadas reduce .
>>> n = 0
>>> para i en el rango (1, 6): # n ^=
... i
...
>>> norte
1
>>> import functools
>>> functools.reduce(lambda a, b: a^b, range(6)) # 1 >>> import
operator >>> functools.reduce(operator.xor, range(6)) #
De las alternativas del ejemplo 10-11, la última es mi favorita y el ciclo for ocupa el segundo lugar. ¿Cuál
es tu preferencia?
Como se vio en “El módulo operator” en la página 156, operator proporciona la funcionalidad de todos
los operadores infijos de Python en forma de función, lo que reduce la necesidad de lambda.
Para codificar Vector.__hash__ en mi estilo preferido, necesitamos importar las funciones y los módulos
de operador . El ejemplo 10-12 muestra los cambios relevantes.
Ejemplo 10-12. Parte de vector_v4.py: dos importaciones y el método __hash__ agregado a la clase Vector desde
vector_v3.py
clase vectorial:
código de tipo = 'd'
def __hash__(self):
hashes = (hash(x) for x in self._components) # return
functools.reduce(operator.xor, hashes, 0) #
Sin cambios en __eq__; Lo enumeré aquí porque es una buena práctica mantener __eq__ y __hash__ cerca
en el código fuente, porque necesitan trabajar juntos.
Cree una expresión de generador para calcular perezosamente el hash de cada componente.
Alimente hashes para reducir con la función xor para calcular el valor hash agregado; el tercer argumento, 0,
es el inicializador (vea la siguiente advertencia).
Tal como se implementó, el método __hash__ del ejemplo 10-8 es un ejemplo perfecto de cálculo de reducción de
mapa (figura 10-2).
Figura 10-2. Map-reduce: aplique la función a cada elemento para generar una nueva serie (mapa),
luego calcule el agregado (reduzca)
El paso de mapeo produce un hash para cada componente, y el paso de reducción agrega todos
los hash con el operador xor . El uso de map en lugar de genex hace que el paso de mapeo sea
aún más visible:
def __hash__(self):
hashes = map(hash, self._components)
return functools.reduce(operator.xor, hashes)
Esto funciona para Vector2d y para Vector; incluso considera que Vector([1, 2]) es igual a (1, 2), lo
que puede ser un problema, pero lo pasaremos por alto por ahora.4 Pero para las instancias de
Vector que pueden tener miles de componentes, es muy ineficiente. Construye dos tuplas copiando
todo el contenido de los operandos solo para usar el __eq__ de la tupla
4. Consideraremos seriamente el tema del Vector([1, 2]) == (1, 2) en “Operator Overloading 101” en la página
372.
escribe. Para Vector2d (con solo dos componentes), es un buen atajo, pero no para los grandes vectores
multidimensionales. Una mejor manera de comparar un Vector con otro Vector o iterable sería el Ejemplo
10-13.
Ejemplo 10-13. Vector.eq usando zip en un bucle for para una comparación más eficiente
Si las lentes de los objetos son diferentes, no son iguales. zip produce
un generador de tuplas a partir de los elementos de cada argumento iterable. Consulte “The Awesome
zip” en la página 293 si zip es nuevo para usted. La comparación de longitudes anterior es necesaria
porque zip deja de producir valores sin previo aviso tan pronto como se agota una de las entradas.
El ejemplo 10-13 es eficiente, pero la función all puede producir el mismo cálculo agregado del ciclo for en
una línea: si todas las comparaciones entre los componentes correspondientes en los operandos son True, el
resultado es True. Tan pronto como una comparación es Falsa, todas devuelven Falsa. El ejemplo 10-14
muestra cómo se ve __eq__ usando all.
Ejemplo 10-14. Vector.eq usando zip y todo: la misma lógica que el Ejemplo 10-13
Tenga en cuenta que primero verificamos que los operandos tengan la misma longitud, porque zip se detendrá
en el operando más corto.
La cremallera impresionante
Tener un ciclo for que itera sobre los elementos sin jugar con las variables de índice es excelente y evita
muchos errores, pero exige algunas funciones de utilidad especiales. Uno de ellos es el zip incorporado, que
facilita la iteración en paralelo sobre dos o más iterables al devolver tuplas que puede descomprimir en
variables, una para cada elemento en las entradas paralelas. Vea el Ejemplo 10-15.
[(0, 'A', 0.0), (1, 'B', 1.1), (2, 'C', 2.2), (-1, -1, 3.3)]
Aquí construimos una lista a partir de él solo para mostrar; normalmente iteramos sobre el generador.
zip tiene una característica sorprendente: se detiene sin previo aviso cuando se agota uno de los
iterables.5
La enumeración incorporada es otra función generadora que se usa a menudo en bucles for para evitar el
manejo manual de variables de índice. Si no está familiarizado con la enumeración, definitivamente debería
comprobarlo en la documentación de "Funciones integradas". El código postal y enumerar
5. Eso es sorprendente (al menos para mí). Creo que zip debería generar ValueError si las secuencias no tienen la
misma longitud, que es lo que sucede al desempaquetar un iterable en una tupla de variables de diferente longitud.
Las funciones incorporadas de ate , junto con varias otras funciones de generador en la biblioteca
estándar, se tratan en “Funciones de generador en la biblioteca estándar” en la página 424.
Por ejemplo, dado un objeto Vector en el espacio 4D (len(v) == 4), el código 'h' producirá una visualización
como <r, ÿÿ, ÿÿ, ÿÿ> donde r es la magnitud (abs(v)) y los números restantes son las coordenadas
angulares ÿ1, ÿ2, ÿ3.
Aquí hay algunas muestras del formato de coordenadas esféricas en 4D, tomadas de las pruebas
documentales de vector_v5.py (ver Ejemplo 10-16):
Antes de que podamos implementar los cambios menores requeridos en __format__, necesitamos
codificar un par de métodos de soporte: angle(n) para calcular una de las coordenadas angulares (p. ej.,
ÿ1), y angles() para devolver una iteración de todas las coordenadas angulares . No describiré las
matemáticas aquí; si tiene curiosidad, la entrada "esfera n" de Wikipedia tiene las fórmulas que usé para
calcular las coordenadas esféricas a partir de las coordenadas cartesianas en la matriz de componentes
vectoriales .
6. El sitio Wolfram Mathworld tiene un artículo sobre Hypersphere; en Wikipedia, "hiperesfera" redirige a la "n
entrada esfera”.
El ejemplo 10-16 es una lista completa de vector_v5.py que consolida todo lo que hemos implementado
desde "Vector Take #1: Vector2d Compatible" en la página 276 e introduce un formato personalizado.
Ejemplo 10-16. vector_v5.py: doctests y todo el código para la clase Vector final; las llamadas resaltan
las adiciones necesarias para admitir __format__
"""
5.0
>>> bool(v1), bool(Vector([0, 0]))
(Verdadero Falso)
7.071067811...
>>> bool(v1), bool(Vector([0, 0, 0]))
(Verdadero Falso)
Prueba de corte::
>>> v7[-1:]
Vector([6.0])
>>> v7[1,2]
Rastreo (llamadas recientes más última):
...
TypeError: los índices vectoriales deben ser números enteros
>>> v7.k
Traceback (última llamada más reciente):
...
AttributeError: el objeto 'Vector' no tiene el atributo 'k' >>> v3 =
Vector(rango(3)) >>> v3.t Rastreo (última llamada más reciente):
...
AttributeError: el objeto 'Vector' no tiene atributo 't' >>> v3.spam
Traceback (última llamada más reciente):
...
AttributeError: el objeto 'Vector' no tiene atributo 'spam'
Pruebas de hash::
La mayoría de los valores hash de valores no enteros varían de una compilación de CPython de 32 bits a 64 bits:
"""
clase vectorial:
código de tipo = 'd'
def __iter__(self):
return iter(self._components)
def __repr__(self):
componentes = reprlib.repr(self._components)
componentes = componentes[componentes.find('['):-1]
return 'Vector({})'.format(componentes)
def __str__(yo):
return str(tupla(yo))
def __bytes__(self):
return (bytes([ord(self.typecode)]) +
bytes(self._components))
def __hash__(self):
hashes = (hash(x) for x in self) return
functools.reduce(operator.xor, hashes, 0)
def __abs__(self):
return math.sqrt(sum(x * x for x in self))
def __bool__(auto):
return bool(abs(auto))
def __len__(self):
return len(self._components)
atajo_nombres = 'xyzt'
devolver self._components[pos]
msg = '{.__name__!r} objeto no tiene atributo {!r}'
aumentar AttributeError(msg.format(cls, nombre))
external_fmt = '({})'
components = (format(c, fmt_spec) for c in coords) return
outside_fmt.format(', '.join(components))
@métodoclase
def frombytes(cls, octetos):
typecode = chr(octetos[0])
memv = memoryview(octetos[1:]).cast(typecode)
devolver cls(memv)
Calcule una de las coordenadas angulares, utilizando fórmulas adaptadas del artículo sobre n esferas.
Cree una expresión de generador para calcular todas las coordenadas angulares a pedido.
Use itertools.chain para producir genex para iterar sin problemas sobre el
magnitud y las coordenadas angulares.
Cree una expresión de generador para dar formato a cada elemento de coordenadas a pedido.
Esto concluye nuestra misión para este capítulo. La clase Vector se mejorará con operadores infijos en
el Capítulo 13, pero nuestro objetivo aquí fue explorar técnicas para codificar métodos especiales que
son útiles en una amplia variedad de clases de colección.
Luego observamos cómo funciona la sintaxis my_seq[a:b:c] detrás de escena, creando un objeto
slice(a, b, c) y entregándoselo a __getitem__. Armados con este conocimiento, hicimos que Vector
respondiera correctamente al corte, devolviendo nuevas instancias de Vector , tal como se espera que
lo haga una secuencia Pythonic.
El siguiente paso fue proporcionar acceso de solo lectura a los primeros componentes de Vector usando
notación como my_vec.x. Lo hicimos implementando __getattr__. Hacer eso abrió la posibilidad de
tentar al usuario a asignar esos componentes especiales escribiendo my_vec.x = 7, revelando un error
potencial. Lo solucionamos implementando __setattr__ también, para prohibir la asignación de valores
a atributos de una sola letra. Muy a menudo, cuando codifica un __getattr__ , también necesita agregar
__setattr__ para evitar un comportamiento inconsistente.
La implementación de la función __hash__ proporcionó el contexto perfecto para usar func tools.reduce,
porque necesitábamos aplicar el operador xor ^ en sucesión a los valores
componentes
hash de todos
del Vector
los para
producir un valor hash agregado para todo el Vector.
Después de aplicar reduce en __hash__, usamos el método integrado de reducción total para crear un
método __eq__ más eficiente.
La última mejora de Vector fue volver a implementar el método __format__ de Vector2d al admitir
coordenadas esféricas como alternativa a las coordenadas cartesianas predeterminadas. Usamos un
poco de matemática y varios generadores para codificar __format__ y sus funciones auxiliares, pero
estos son detalles de implementación, y volveremos a los generadores en el Capítulo 14. El objetivo de
la última sección era admitir un
formato, cumpliendo así la promesa de un Vector que podría hacer todo lo que hizo un Vector2d , y
más.
Como hicimos en el Capítulo 9, aquí a menudo observamos cómo se comportan los objetos estándar
de Python, para emularlos y proporcionar una apariencia "Pythonic" a Vector.
En el Capítulo 13, implementaremos varios operadores infijos en Vector. Las matemáticas serán mucho
más simples que las del método angle() aquí, pero explorar cómo funcionan los operadores infijos en
Python es una gran lección en el diseño OO. Pero antes de llegar a la sobrecarga de operadores,
dejaremos de trabajar en una clase y analizaremos la organización de varias clases con interfaces y
herencia, los temas de los capítulos 11 y 11.
Otras lecturas
La mayoría de los métodos especiales cubiertos en el ejemplo de Vector también aparecen en el
ejemplo de Vector2d del Capítulo 9, por lo que las referencias en “Lecturas adicionales” en la página
271 son relevantes aquí.
La potente función de reducción de orden superior también se conoce como plegar, acumular, agregar,
comprimir e inyectar. Para obtener más información, consulte el artículo “Fold (función de orden
superior)” de Wikipedia, que presenta aplicaciones de esa función de orden superior con énfasis en la
programación funcional con estructuras de datos recursivas. El artículo también incluye una tabla que
enumera funciones similares a pliegues en docenas de lenguajes de programación.
Plataforma improvisada
Los protocolos no son una invención de Python. El equipo de Smalltalk, que también acuñó la expresión
"orientado a objetos", utilizó "protocolo" como sinónimo de lo que ahora llamamos interfaces. Algunos
entornos de programación de Smalltalk permitían a los programadores etiquetar un grupo de métodos
como un protocolo, pero eso era simplemente una ayuda de documentación y navegación, y el lenguaje
no lo aplicaba. Es por eso que creo que "interfaz informal" es una breve explicación razonable para
"protocolo" cuando me dirijo a una audiencia que está más familiarizada con las interfaces formales (y
forzadas por el compilador).
Los protocolos establecidos evolucionan de forma natural en cualquier lenguaje que utilice tipos
dinámicos, es decir, cuando se realiza la verificación de tipos en tiempo de ejecución porque no hay
información de tipos estáticos en las firmas y variables de los métodos. Ruby es otro lenguaje OO
importante que tiene escritura dinámica y utiliza protocolos.
En la documentación de Python, a menudo puede saber cuándo se está discutiendo un protocolo cuando
ve un lenguaje como "un objeto similar a un archivo". Esta es una forma rápida de decir "algo que se
comporta lo suficientemente como un archivo, implementando las partes de la interfaz del archivo que
son relevantes en el contexto".
Puede pensar que implementar solo una parte de un protocolo es descuidado, pero tiene la ventaja
de simplificar las cosas. La sección 3.3 del capítulo “Modelo de datos” sugiere:
Al implementar una clase que emula cualquier tipo integrado, es importante que la emulación
solo se implemente en la medida en que tenga sentido para el objeto que se modela. Por
ejemplo, algunas secuencias pueden funcionar bien con la recuperación de elementos
individuales, pero extraer un corte puede no tener sentido.
Cuando no necesitamos codificar métodos sin sentido solo para cumplir con un contrato de interfaz
sobrediseñado y mantener contento al compilador, se vuelve más fácil seguir el principio KISS.
Tendré más que decir sobre protocolos e interfaces en el Capítulo 11, donde ese es realmente el
enfoque principal.
Creo que la comunidad de Ruby, más que cualquier otra, ayudó a popularizar el término "duck
digitación", como lo predicaron a las masas de Java. Pero la expresión se ha utilizado en las
discusiones de Python antes de que Ruby o Python fueran "populares". Según Wikipedia, un
ejemplo temprano de la analogía del pato en la programación orientada a objetos es un mensaje a
la lista Python de Alex Martelli del 26 de julio de 2000: polimorfismo (¿era Re: verificación de tipos
en Python?). De ahí proviene la cita al comienzo de este capítulo. Si tiene curiosidad acerca de los
orígenes literarios del término "escribir patos" y las aplicaciones de este concepto OO en muchos
idiomas, consulte la entrada "Escribir patos" de Wikipedia.
Así es como lo haría: de forma predeterminada, cualquier Vector formateado mostraría un número
razonable pero limitado de componentes, digamos 30. Si hay más elementos que eso, el
comportamiento predeterminado sería similar a lo que hace reprlib : cortar el exceso y poner ...
suen
lugar. Sin embargo, si el especificador de formato terminara con el código especial * , que significa
"todos", la limitación de tamaño estaría deshabilitada. Por lo tanto, un usuario que desconozca el
problema de las pantallas muy largas no se verá afectado por accidente. Pero si la limitación
predeterminada se convierte en una molestia, entonces la presencia de ... debería incitar al usuario
a investigar la documentación y descubrir el código de formato * .
¡ Envíe una solicitud de extracción al repositorio de Fluent Python en GitHub si implementa esto!
No hay una respuesta única a "¿Qué es Pythonic?" así como no hay una respuesta única a "¿Qué es
hermoso?" Decir, como suelo hacer, que significa usar "Python idiomático" no es 100% satisfactorio,
porque lo que puede ser "idiomático" para ti puede no serlo para mí. Una cosa sé: "idiomático" no
significa usar las características más oscuras del lenguaje.
En la lista de Python, hay un hilo de abril de 2003 titulado "¿Pythonic Way to Sum n-th List Element?".
Es relevante para nuestra discusión de reducir en este capítulo.
El cartel original, Guy Middleton, pidió una mejora en esta solución, afirmando que no le gustaba usar
lambda: 7
>>> mi_lista = [[1, 2, 3], [40, 50, 60], [9, 8, 7]] >>> import
functools >>> functools.reduce(lambda a, b: a+b , [sub[1] para
sub en mi_lista]) 60
Ese código usa muchos modismos: lambda, reduce y una lista de comprensión. Probablemente ocuparía
el último lugar en un concurso de popularidad, porque ofende a las personas que odian lambda y a las
que desprecian las listas de comprensión, prácticamente ambos lados de una línea divisoria.
Si va a usar lambda, probablemente no haya ninguna razón para usar una lista de comprensión, excepto
por el filtrado, que no es el caso aquí.
Aquí hay una solución propia que complacerá a los amantes de lambda :
No participé en el hilo original, y no lo usaría en código real, porque no me gusta mucho lambda , pero
quería mostrar un ejemplo sin una lista de comprensión.
La primera respuesta provino de Fernando Pérez, creador de IPython, destacando que Num-Py admite
arreglos n-dimensionales y cortes en n-dimensionales:
Creo que la solución de Pérez es genial, pero Guy Middleton elogió esta próxima solución, de Paul
Rubin y Skip Montanaro:
>>> operador de
importación >>> functools.reduce(operator.add, [sub[1] for sub in my_list], 0) 60
7. Adapté el código para esta presentación: en 2003, reduce estaba integrado, pero en Python 3 necesitamos importar
eso; también, reemplacé los nombres x e y con my_list y sub, para sublista.
>>> t = 0
>>> para sub en mi_lista:
... total += sub[1]
>>> t
60
Mucha gente estuvo de acuerdo en que era bastante pitónico. Alex Martelli llegó a decir que
probablemente así es como lo codificaría Guido.
Me gusta el código de Evan Simpson, pero también me gusta el comentario de David Eppstein al respecto:
Si desea la suma de una lista de elementos, debe escribirla de una manera que parezca "la suma de una lista
de elementos", no de una manera que parezca "recorrer estos elementos, mantener otra variable t, realizar
una secuencia de adiciones”. ¿Por qué tenemos lenguajes de alto nivel si no es para expresar nuestras
intenciones en un nivel superior y dejar que el lenguaje se preocupe por las operaciones de bajo nivel que se
necesitan para implementarlo?
"La suma" se necesita con tanta frecuencia que no me importaría en absoluto si Python lo destacara como un
elemento integrado. Pero "reduce(operator.add, ..." simplemente no es una buena forma de expresarlo, en mi
opinión (y, sin embargo, como un viejo APL'er y FP-liker, me gustaría , pero no lo hago) .
Alex continúa sugiriendo una función sum() , que él contribuyó. Se convirtió en una función integrada en
Python 2.3, lanzada solo tres meses después de que tuvo lugar esa conversación. Entonces, la sintaxis
preferida de Alex se convirtió en la norma:
A fines del año siguiente (noviembre de 2004), se lanzó Python 2.4 con expresiones generadoras, lo
que ahora, en mi opinión, es la respuesta más pitónica a la pregunta original de Guy Middleton:
Esto no solo es más legible que reduce sino que también evita la trampa de la secuencia vacía: sum([])
es 0, así de simple.
En la misma conversación, Alex Martelli sugiere que la reducción incorporada en Python 2 fue más
problemática de lo que valió la pena, porque fomentó modismos de codificación que eran difíciles de
explicar. Fue muy convincente: la función fue degradada al módulo functools en Python 3.
Aún así, functools.reduce tiene su lugar. Resolvió el problema de nuestro Vector.__hash__ de una
manera que yo llamaría Pythonic.
CAPÍTULO 11
- Bjarne Stroustrup
Creador de C++
Las interfaces son el tema de este capítulo: desde los protocolos dinámicos que son el sello distintivo de
la tipificación pato hasta las clases base abstractas (ABC) que hacen que las interfaces sean explícitas
y verifican la conformidad de las implementaciones.
Si tiene experiencia en Java, C# o similar, la novedad aquí está en los protocolos informales de
tipificación pato. Pero para los pythonistas o rubyistas veteranos, esa es la forma “normal” de pensar en
las interfaces, y la noticia es la formalidad y la verificación de tipos de ABC. El lenguaje tenía 15 años
cuando se introdujeron los ABC en Python 2.6.
Luego, un ensayo invitado de Alex Martelli presentará el ABC y dará nombre a una nueva tendencia en
la programación de Python. El resto del capítulo se dedicará a ABC, comenzando con su uso común
como superclases cuando necesite implementar una interfaz. Luego veremos cuándo un ABC verifica
subclases concretas para verificar su conformidad con la interfaz que define, y cómo un mecanismo de
registro permite a los desarrolladores declarar que una clase implementa una interfaz sin subclases.
Finalmente, veremos cómo se puede programar un ABC para que “reconozca” automáticamente clases
arbitrarias que se ajusten a su interfaz, sin subclases ni registros explícitos.
307
Machine Translated by Google
Implementaremos un nuevo ABC para ver cómo funciona, pero Alex Martelli y yo no queremos
alentarlo a que comience a escribir su propio ABC a diestra y siniestra. El riesgo de sobreingeniería
con ABC es muy alto.
Los ABC, al igual que los descriptores y las metaclases, son herramientas
para construir marcos. Por lo tanto, solo una minoría muy pequeña de
desarrolladores de Python puede crear ABC sin imponer limitaciones
irrazonables y trabajo innecesario a sus compañeros programadores.
¿Cómo funcionan las interfaces en un lenguaje de tipo dinámico? Primero, lo básico: incluso sin una
palabra clave de interfaz en el lenguaje, e independientemente del ABC, cada clase tiene una interfaz:
los atributos públicos establecidos (métodos o atributos de datos) implementados o heredados por la
clase. Esto incluye métodos especiales, como __getitem__ o __add__.
Por definición, los atributos protegidos y privados no forman parte de una interfaz, incluso si "protegido"
es simplemente una convención de nomenclatura (el único guión bajo inicial) y se puede acceder
fácilmente a los atributos privados (recuerde los atributos "Privados" y "Protegidos" en Python” en la
página 262). Es de mala educación violar estas convenciones.
Por otro lado, no es un pecado tener atributos de datos públicos como parte de la interfaz de un objeto,
porque, si es necesario, un atributo de datos siempre se puede convertir en una propiedad que
implementa la lógica de obtención/establecimiento sin romper el código del cliente que usa el sintaxis
simple de obj.attr . Hicimos eso en la clase Vector2d : en el Ejemplo 11-1, vemos la primera
implementación con atributos públicos x e y .
Ejemplo 11-1. vector2d_v0.py: x e y son atributos de datos públicos (mismo código que en el Ejemplo
9-2)
clase Vector2d:
código de tipo = 'd'
En el Ejemplo 9-7, convertimos x e y en propiedades de solo lectura (Ejemplo 11-2). Esta es una
refactorización significativa, pero una parte esencial de la interfaz de Vector2d no ha cambiado: los
usuarios aún pueden leer my_vector.x y my_vector.y.
@property
def x(self):
return self.__x
@property
def y(self):
return self.__y
def __iter__(self):
return (i for i in (self.x, self.y))
Una definición complementaria útil de interfaz es: el subconjunto de los métodos públicos de un objeto
que le permiten desempeñar un papel específico en el sistema. Eso es lo que se implica cuando la
documentación de Python menciona "un objeto similar a un archivo" o "un iterable", sin especificar una
clase. Una interfaz vista como un conjunto de métodos para cumplir un rol es lo que Smalltalkers llamó
un protocolo, y el término se extendió a otras comunidades lingüísticas dinámicas. Los protocolos son
independientes de la herencia. Una clase puede implementar varios protocolos, lo que permite que
sus instancias cumplan varios roles.
Los protocolos son interfaces, pero debido a que son informales, definidos solo por la documentación
y las convenciones, los protocolos no se pueden aplicar como las interfaces formales (veremos cómo
los ABC imponen la conformidad de la interfaz más adelante en este capítulo). Un protocolo puede
implementarse parcialmente en una clase en particular, y eso está bien. A veces, todo lo que requiere
una API específica de "un objeto similar a un archivo" es que tenga un método .read() que devuelva
bytes. Los métodos de archivo restantes pueden o no ser relevantes en el contexto.
Mientras escribo esto, la documentación de Python 3 de memoryview dice que funciona con objetos
que “admiten el protocolo de búfer, que solo está documentado en el nivel de la API de C. El
constructor bytearray acepta un "objeto conforme a la interfaz del búfer". Ahora
hay un movimiento para adoptar "objeto similar a bytes" como un término más amigable.2 Señalo
esto para enfatizar que "objeto similar a X", "protocolo X" e "interfaz X" son sinónimos en la mente de
Pythonistas.
Una de las interfaces más fundamentales en Python es el protocolo de secuencia. El intérprete hace
todo lo posible para manejar objetos que proporcionan incluso una implementación mínima de ese
protocolo, como se demuestra en la siguiente sección.
La filosofía del modelo de datos de Python es cooperar con los protocolos esenciales tanto como sea
posible. Cuando se trata de secuencias, Python se esfuerza por trabajar incluso con las
implementaciones más simples.
La figura 11-1 muestra cómo la interfaz de Secuencia formal se define como un ABC.
Figura 11-1. Diagrama de clases UML para la Secuencia ABC y clases abstractas relacionadas de
collections.abc. Las flechas de herencia apuntan desde la subclase a sus superclases. Los nombres
en cursiva son métodos abstractos.
Ahora, eche un vistazo a la clase Foo en el Ejemplo 11-3. No hereda de abc.Sequence, y solo
implementa un método del protocolo de secuencia: __getitem__ (falta __len__ ).
Ejemplo 11-3. Implementación del protocolo de secuencia parcial con __getitem__: suficiente para el
acceso a elementos, la iteración y el operador in
>>> clase Foo:
... def __getitem__(self, pos):
... rango de retorno (0, 30, 10) [pos]
2. Problema 16518: "agregar protocolo de búfer al glosario" en realidad se resolvió al reemplazar muchas menciones de
"objeto que admite el protocolo/interfaz/API de búfer" con "objeto similar a bytes"; un tema de seguimiento es "Otras
menciones del protocolo de búfer".
...
>>> f[1]
10
>>> f = Foo()
>>> para i en f: print(i)
...
0
10
20 >>> 20 en f
Verdadero >>> 15 en f
Falso
No hay un método __iter__ pero las instancias de Foo son iterables porque, como alternativa, cuando Python
ve un método __getitem__ , intenta iterar sobre el objeto llamando a ese método con índices enteros que
comienzan con 0. Porque Python es lo suficientemente inteligente como para iterar sobre Foo instancias,
también puede hacer que el operador in funcione incluso si Foo no tiene un método __contains__ : realiza un
escaneo completo para verificar si un elemento está presente.
En resumen, dada la importancia del protocolo de secuencia, en ausencia de __iter__ y __contains__, Python
aún logra hacer que la iteración y el operador in funcionen invocando a __getitem__.
Nuestro FrenchDeck original del Capítulo 1 tampoco es una subclase de abc.Sequence , pero implementa
ambos métodos del protocolo de secuencia: __getitem__ y __len__. Vea el Ejemplo 11-4.
Ejemplo 11-4. Una baraja como una secuencia de cartas (igual que en el Ejemplo 1-1)
importar colecciones
clase FrenchDeck:
rangos = [str(n) for n in range(2, 11)] + list('JQKA') palos = 'picas
diamantes tréboles corazones'.split()
def __init__(self):
self._cards = [Carta(rango, palo) para palo en self.palos para rango
en self.ranks]
def __len__(self):
return len(self._cards)
Una buena parte de las demostraciones en el Capítulo 1 funcionan debido al tratamiento especial que Python
le da a cualquier cosa que se parezca vagamente a una secuencia. La iteración en Python representa una
forma extrema de digitación pato: el intérprete prueba dos métodos diferentes para iterar sobre los
objetos.
Ahora estudiemos otro ejemplo que enfatiza la naturaleza dinámica de los protocolos.
La clase FrenchDeck del ejemplo 11-4 tiene un gran defecto: no se puede barajar. Hace años, cuando
escribí por primera vez el ejemplo de FrenchDeck , implementé un método aleatorio .
Más tarde tuve una idea de Pythonic: si un FrenchDeck actúa como una secuencia, entonces no
necesita su propio método de barajar porque ya existe random.shuffle, documentado como "Barajar la
secuencia x en su lugar".
Sin embargo, si tratamos de barajar una instancia de FrenchDeck , obtenemos una excepción, como
en el Ejemplo 11-5.
El mensaje de error es bastante claro: "El objeto 'FrenchDeck' no admite la asignación de elementos".
El problema es que la reproducción aleatoria opera intercambiando elementos dentro de la colección,
y FrenchDeck solo implementa el protocolo de secuencia inmutable . Las secuencias mutables también
deben proporcionar un método __setitem__ .
Debido a que Python es dinámico, podemos arreglar esto en tiempo de ejecución, incluso en la consola interactiva.
El ejemplo 11-6 muestra cómo hacerlo.
Ejemplo 11-6. Monkey parcheando FrenchDeck para hacerlo mutable y compatible con random.shuffle (continuando
con el Ejemplo 11-5)
Cree una función que tome el mazo, la posición y la carta como argumentos.
Asigne esa función a un atributo llamado __setitem__ en la clase FrenchDeck . La cubierta ahora se
puede ordenar porque FrenchDeck ahora implementa el método necesario del protocolo de secuencia
mutable.
La firma del método especial __setitem__ se define en The Python Language Reference en “3.3.6. Emulación de
tipos de contenedores”. Aquí llamamos a los argumentos mazo, posición, carta, y no yo, clave, valor como en la
referencia del lenguaje, para mostrar que cada método de Python comienza como una función simple, y nombrar
el primer argumento yo es simplemente una convención. Esto está bien en una sesión de consola, pero en un
archivo fuente de Python es mucho mejor usar self, key y value como se documenta.
El truco es que set_card sabe que el objeto del mazo tiene un atributo llamado _cards, y _cards debe ser una
secuencia mutable. Luego, la función set_card se adjunta a la clase FrenchDeck como el método especial
__setitem__ . Este es un ejemplo de parches de mono: cambiar una clase o módulo en tiempo de ejecución, sin
tocar el código fuente.
El parcheo mono es poderoso, pero el código que hace el parcheo real está estrechamente relacionado con el
programa que se va a parchear, a menudo manejando partes privadas e indocumentadas.
Además de ser un ejemplo de parcheo mono, el ejemplo 11-6 destaca que los protocolos son dinámicos: a
random.shuffle no le importa qué tipo de argumento recibe, solo necesita que el objeto implemente parte del
protocolo de secuencia mutable. Ni siquiera importa si el objeto “nació” con los métodos necesarios o si de alguna
manera se adquirieron más tarde.
El tema de este capítulo hasta ahora ha sido “duck typing”: operar con objetos sin importar su tipo, siempre y
cuando implementen ciertos protocolos.
Cuando presentamos diagramas con ABC, la intención era mostrar cómo los protocolos se relacionan
con las interfaces explícitas documentadas en las clases abstractas, pero hasta ahora no heredamos
de ningún ABC.
En las siguientes secciones, aprovecharemos los ABC directamente, y no solo como documentación.
Después de revisar las interfaces de estilo de protocolo habituales de Python, pasamos al ABC. Pero
antes de sumergirse en ejemplos y detalles, Alex Martelli explica en un ensayo invitado por qué ABC
fue una gran adición a Python.
me ha acreditado en Wikipedia por ayudar a difundir el útil meme y el fragmento de sonido "escribir pato" (es
decir, ignorar el tipo real de un objeto, enfocándose en cambio en garantizar que el objeto implemente los
nombres de método, las firmas y la semántica requerida para su uso previsto).
En Python, esto se reduce principalmente a evitar el uso de isinstance para verificar el tipo del objeto (sin
mencionar el enfoque aún peor de verificar, por ejemplo, si el tipo (foo) es bar, lo cual es un anatema, ya que
inhibe incluso el más simple ). formas de herencia!).
El enfoque general de tipificación de patos sigue siendo bastante útil en muchos contextos y, sin embargo, en
muchos otros, uno a menudo preferible ha evolucionado con el tiempo. Y aquí yace una historia…
En las últimas generaciones, la taxonomía de género y especie (que incluye, entre otros, la familia de aves
acuáticas conocida como Anatidae) se ha visto impulsada principalmente por la fenética, un enfoque centrado
en las similitudes de morfología y comportamiento... principalmente, en los rasgos observables . La analogía
con "escribir pato" era fuerte.
Sin embargo, la evolución paralela a menudo puede producir rasgos similares, tanto morfológicos como de
comportamiento, entre especies que en realidad no están relacionadas, pero que simplemente evolucionaron
en nichos ecológicos similares, aunque separados. También ocurren “similitudes accidentales” similares en la
programación; por ejemplo, considere el ejemplo clásico de programación orientada a objetos:
clase Artista:
def draw(self): ...
clase Pistolero:
def draw(self): ...
lotería de
clase : def sorteo (auto): ...
Claramente, la mera existencia de un método llamado dibujar, invocable sin argumentos, está lejos
de ser suficiente para asegurarnos que dos objetos x e y tales que x.draw() e y.draw() pueden ser
llamados son de alguna manera intercambiables o abstractamente equivalente: no se puede inferir
nada sobre la similitud de la semántica resultante de tales llamadas. Más bien, ¡necesitamos un
programador experto para afirmar positivamente de alguna manera que tal equivalencia se mantiene
en algún nivel!
En biología (y otras disciplinas), este problema ha llevado al surgimiento (y, en muchas facetas, al
predominio) de un enfoque que es una alternativa a la fenética, conocido como cladística, que enfoca
las elecciones taxonómicas en características que se heredan de ancestros comunes, en lugar de
que los que evolucionan independientemente. (La secuenciación de ADN barata y rápida puede hacer
que la cladística sea muy práctica en muchos más casos, en los últimos años).
Por ejemplo, los gansos (una vez clasificados como más cercanos a otros gansos) y los tarros (una
vez clasificados como más cercanos a otros patos) ahora se agrupan dentro de la subfamilia
Tadornidae (lo que implica que están más cerca entre sí que con cualquier otro Anatidae, como
comparten un ancestro común más cercano). Además, el análisis de ADN ha demostrado, en
particular, que el pato de madera de ala blanca no es tan parecido al pato de Berbería (este último es
un tarro blanco) como se había sugerido durante mucho tiempo por la similitud en el aspecto y el
comportamiento, por lo que el pato de madera se reclasificó en su propio género, y completamente fuera de la subfamilia!
¿Importa esto? ¡Depende del contexto! Para fines tales como decidir cuál es la mejor manera de
cocinar un ave acuática una vez que la haya embolsado, por ejemplo, rasgos observables específicos
(no todos; el plumaje, por ejemplo, es de minimis en tal contexto), principalmente textura y sabor
(antiguo - ¡fenética de moda!), puede ser mucho más relevante que la cladística. Pero para otros
problemas, como la susceptibilidad a diferentes patógenos (ya sea que esté tratando de criar aves
acuáticas en cautiverio o preservarlas en la naturaleza), la cercanía del ADN puede ser muy importante.
más…
Entonces, por una analogía muy vaga con estas revoluciones taxonómicas en el mundo de las aves
acuáticas, recomiendo complementar (no reemplazar por completo; en ciertos contextos aún servirá)
la tipificación de pato con... ¡tipificación de ganso!
Lo que significa escribir ganso es: isinstance(obj, cls) ahora está bien... siempre que cls sea una clase
base abstracta; en otras palabras, la metaclase de cls es abc.ABCMeta.
Puede encontrar muchas clases abstractas existentes útiles en collections.abc (y otras adicionales
en el módulo de números de The Python Standard Library).3 Entre las muchas ventajas conceptuales
de ABC sobre las clases concretas (p. sea abstracto”; consulte el artículo 33 de su libro, C++ más
efectivo), el ABC de Python agrega una gran ventaja práctica: el método de registro de clases, que
permite que el código del usuario final “declare” que una determinada clase se convierte en una
subclase “virtual” de un ABC (para este propósito, la clase registrada debe cumplir con los requisitos
de nombre y firma del método de ABC y, lo que es más importante, el contrato semántico
subyacente, pero no es necesario que se haya desarrollado con conocimiento de ABC y, en
particular, ¡no necesita heredar de él!) . Esto contribuye en gran medida a romper la rigidez y el
fuerte acoplamiento que hacen que la herencia sea algo para usar con mucha más precaución de
lo que normalmente practican la mayoría de los programadores OOP...
¡A veces ni siquiera necesita registrar una clase para que un ABC la reconozca como una subclase!
Ese es el caso de los ABC, cuya esencia se reduce a unos pocos métodos especiales. Por ejemplo:
Como puede ver, abc.Sized reconoce a Struggle como "una subclase", sin necesidad de registro,
ya que implementar el método especial llamado __len__ es todo lo que se necesita (se supone que
debe implementarse con la sintaxis adecuada, invocable sin argumentos, y la semántica: devolver
un entero no negativo que denota la "longitud" de un objeto; cualquier código que implemente un
método con un nombre especial, como __len__, con una sintaxis y una semántica arbitrarias y no
conformes tiene problemas mucho peores de todos modos).
Entonces, aquí está mi despedida: cada vez que implemente una clase que incorpore cualquiera de
los conceptos representados en el ABC en números, colecciones.abc u otro marco que pueda estar
usando, asegúrese (si es necesario) de subclasificarlo o registrarlo en el ABC correspondiente. Al
comienzo de sus programas utilizando alguna biblioteca o marco de definición de clases que han
omitido hacer eso, realice los registros usted mismo; luego, cuando deba verificar (por lo general)
que un argumento sea, por ejemplo, "una secuencia", verifique si:
isinstance(the_arg, colecciones.abc.secuencia)
3. También puede, por supuesto, definir su propio ABC, pero desaconsejaría a todos, excepto a los pitonistas más avanzados,
que siguieran ese camino, al igual que los disuadiría de definir sus propias metaclases personalizadas... e incluso para
dichos "pytonistas más avanzados". ”, aquellos de nosotros que poseemos un profundo dominio de cada pliegue y pliegue
del lenguaje, estas no son herramientas de uso frecuente: tal “metaprogramación profunda”, si alguna vez es apropiada,
está destinada a autores de amplios marcos destinados a ser ampliados de forma independiente en un gran número de
equipos de desarrollo separados... ¡menos del 1% de los "pythonistas más avanzados" pueden necesitar eso! — AM
Además de acuñar el "tipo de ganso", Alex señala que heredar de un ABC es más que implementar
los métodos requeridos: también es una clara declaración de intenciones por parte del desarrollador.
Esa intención también se puede hacer explícita mediante el registro de una subclase virtual.
Además, el uso de isinstance e issubclass se vuelve más aceptable para probar contra ABC. En el
pasado, estas funciones funcionaban en contra de la tipificación de patos, pero con ABC se vuelven
más flexibles. Después de todo, si un componente no implementa un ABC mediante la
subclasificación, siempre se puede registrar después del hecho para que pase esas comprobaciones
de tipo explícitas.
Sin embargo, incluso con ABC, debe tener en cuenta que el uso excesivo de verificaciones de
instancias puede ser un olor a código, un síntoma de un mal diseño OO. Por lo general, no está
bien tener una cadena de if/elif/elif con verificaciones de instancias que realicen diferentes acciones
según el tipo de objeto: debe usar polimorfismo para eso, es decir, diseñar sus clases para que el
intérprete envíe llamadas a la adecuada métodos, en lugar de codificar la lógica de despacho en
bloques if/elif/elif .
Por otro lado, generalmente está bien realizar una verificación de instancia contra un ABC si debe
hacer cumplir un contrato de API: "Amigo, tiene que implementar esto si quiere llamarme", como lo
expresó el revisor técnico Lennart Regebro. Eso es particularmente útil en sistemas que tienen una
arquitectura de plug-in. Fuera de los marcos, la tipificación de pato suele ser más simple y más
flexible que las verificaciones de tipo.
4. Desafortunadamente, en Python 3.4, no hay ABC que ayude a distinguir una str de una tupla u otras secuencias
inmutables, por lo que debemos probar contra str. En Python 2, el tipo basestr existe para ayudar con pruebas como estas.
No es un ABC, pero es una superclase de str y unicode; sin embargo, en Python 3, basestr se ha ido.
Curiosamente, en Python 3 hay un tipo collections.abc.ByteString, pero solo ayuda a detectar bytes y bytearray.
Por ejemplo, en varias clases de este libro, cuando necesitaba tomar una secuencia de elementos y
procesarlos como una lista, en lugar de requerir un argumento de lista mediante verificación de tipo,
simplemente tomé el argumento e inmediatamente construí una lista a partir de él: que manera puedo aceptar
cualquier iterable, y si el argumento no es iterable, la llamada fallará lo suficientemente pronto con un mensaje
muy claro. Un ejemplo de este patrón de código se encuentra en el método __init__ del Ejemplo 11-13, más
adelante en este capítulo. Por supuesto, este enfoque no funcionaría si el argumento de la secuencia no se
debe copiar, ya sea porque es demasiado grande o porque mi código necesita cambiarlo en su lugar.
Entonces una instancia (x, abc.MutableSequence) sería mejor. Si cualquier iterable es aceptable, llamar a
iter(x) para obtener un iterador sería el camino a seguir, como veremos en “Por qué las secuencias son
iterables: la función iter” en la página 404.
Otro ejemplo es cómo puede imitar el manejo del argumento field_names en collections.namedtuple:
field_names acepta una sola cadena con identificadores separados por espacios o comas, o una secuencia
de identificadores. Puede ser tentador usar isinstance, pero el Ejemplo 11-7 muestra cómo lo haría usando el
tipo pato.5
Ejemplo 11-7. Duck escribiendo para manejar una cadena o una iteración de cadenas
pasar
nombres_de_campo = tupla(nombres_de_campo)
Supongamos que es una cadena (EAFP = es más fácil pedir perdón que permiso).
Lo siento, field_names no grazna como una cadena... o no hay .replace, o devuelve algo que no
podemos .split.
Para asegurarse de que sea iterable y mantener nuestra propia copia, cree una tupla a partir de lo
que tenemos.
Finalmente, en su ensayo, Alex refuerza más de una vez la necesidad de moderación en la creación de ABC.
Una epidemia ABC sería desastrosa, imponiendo una ceremonia excesiva en un lenguaje que se hizo popular
porque es práctico y pragmático. Durante el proceso de revisión de Fluent Python , Alex escribió:
Los ABC están destinados a encapsular conceptos muy generales, abstracciones, introducidos por un
marco, cosas como "una secuencia" y "un número exacto". Lo más probable es que [los lectores] no
necesiten escribir ningún ABC nuevo, solo usen los existentes correctamente, para obtener el 99,9 % de los
beneficios sin un riesgo grave de error de diseño.
Subclasificación de un ABC
importar colecciones
clase FrenchDeck2(colecciones.MutableSequence):
rangos = [str(n) for n in range(2, 11)] + list('JQKA') palos = 'picas
diamantes tréboles corazones'.split()
def __init__(self):
self._cards = [Carta(rango, palo) para palo en self.palos para
rango en self.ranks]
def __len__(self):
return len(self._cards)
Pero subclasificar MutableSequence nos obliga a implementar __delitem__, un método abstracto de ese
ABC.
Python no verifica la implementación de los métodos abstractos en el momento de la importación (cuando el módulo
frenchdeck2.py está cargado y compilado), sino solo en el tiempo de ejecución cuando intentamos instanciar
FrenchDeck2. Luego, si no implementamos ningún método abstracto, obtenemos una excepción TypeError con un
mensaje como "No se puede crear una instancia".
Como muestra la figura 11-2 , no todos los métodos del ABC de Sequence y MutableSequence son
abstractos.
Figura 11-2. Diagrama de clases UML para MutableSequence ABC y sus superclases de
collections.abc (las flechas de herencia apuntan desde las subclases a los ancestros; los nombres en
cursiva son clases abstractas y métodos abstractos)
Como codificador de una subclase concreta, es posible que pueda anular los
métodos heredados de ABC con implementaciones más eficientes. Por
ejemplo, __contains__ funciona haciendo un escaneo completo de la
secuencia, pero si su secuencia concreta mantiene sus elementos ordenados,
puede escribir un __contains__ más rápido que realiza una búsqueda binaria
utilizando la función bisect (consulte “Administrar secuencias ordenadas con
bisect” en la página 44 ).
Para usar bien ABC, necesita saber lo que está disponible. Repasaremos las colecciones ABC
Siguiente.
ABC en colecciones.abc
La figura 11-3 es un diagrama de clases UML resumido (sin nombres de atributos) de los
16 ABC definidos en collections.abc a partir de Python 3.4. La documentación oficial de
collections.abc tiene una bonita tabla que resume los ABC, sus relaciones y sus métodos
abstractos y concretos (llamados "métodos mixin"). Hay mucha herencia múltiple en la
figura 11-3. Dedicaremos la mayor parte del Capítulo 12 a la herencia múltiple, pero por
ahora es suficiente decir que normalmente no es un problema cuando se trata de ABC.6
6. La herencia múltiple se consideró dañina y se excluyó de Java, a excepción de las interfaces: interfaces de Java
puede extender múltiples interfaces, y las clases de Java pueden implementar múltiples interfaces.
MappingView
En Python 3, los objetos devueltos por los métodos de asignación .items(), .keys() y .values()
se heredan de ItemsView, ValuesView y ValuesView, respectivamente.
Los dos primeros también heredan la rica interfaz de Set, con todos los operadores que vimos en
“Operaciones de Set” en la página 82.
Llamable y hashable
Estos ABC no están tan estrechamente relacionados con las colecciones, pero collections.abc fue el
primer paquete que definió ABC en la biblioteca estándar, y estos dos se consideraron lo
suficientemente importantes como para incluirlos. Nunca he visto subclases de Callable o
hashable. Su uso principal es admitir la instancia integrada como una forma segura de determinar
si un objeto es invocable o hashable.7
iterador
Tenga en cuenta que las subclases de iterador Iterable. Discutiremos esto más adelante en el Capítulo 14.
Después del paquete collections.abc , el paquete más útil de ABC en la biblioteca estándar es numbers, que se trata a
continuación.
El paquete de números define la llamada "torre numérica" (es decir, esta jerarquía lineal de ABC), donde
Número es la superclase superior, Complejo es su subclase inmediata, y así sucesivamente, hasta
Integral:
• Número
• Complejo
• Bienes
• Racional
• Integrales
Entonces, si necesita buscar un número entero, use isinstance(x, numbers.Integral) para aceptar int,
bool (cuyas subclases son int) u otros tipos de números enteros que pueden proporcionar bibliotecas
externas que registran sus tipos con los números ABC. Y para cumplir con su verificación, usted o los
usuarios de su API siempre pueden registrar cualquier tipo compatible como una subclase virtual de
números. Integral.
Si, por otro lado, un valor puede ser del tipo de punto flotante, escribe isinstance(x, numbers.Real), y tu
código aceptará felizmente bool, int, float, fracciones.Fracción o cualquier otro valor numérico no
complejo . tipo proporcionado por una biblioteca externa, como NumPy, que está debidamente registrada.
7. Para la detección de llamadas, existe la función integrada callable(), pero no hay una función hashable()
equivalente, por lo que isinstance(my_obj, Hashable) es la forma preferida de probar un objeto hashable.
Tomando una pista de "pila" y "cola" (que describen interfaces abstractas en términos de arreglos
físicos de objetos), usaré una metáfora del mundo real para nombrar nuestro ABC: las jaulas de bingo
y los sopladores de lotería son máquinas diseñadas para elegir artículos en al azar de un conjunto
finito, sin repetir, hasta agotar el conjunto.
El ABC se llamará Tombola, por el nombre italiano del bingo y el contenedor giratorio que mezcla los
números.9
La Tómbola ABC tiene cuatro métodos. Los dos métodos abstractos son:
• .inspect(): devuelve una tupla ordenada construida a partir de los elementos actualmente en el
contenedor, sin cambiar su contenido (no se conserva su orden interno).
8. Tal vez el cliente necesite auditar el aleatorizador; o la agencia quiere proporcionar uno amañado. Tu nunca
saber…
9. El Oxford English Dictionary define la tómbola como “una especie de lotería que se parece a la lotería”.
Figura 11-4. Diagrama UML para un ABC y tres subclases. El nombre de la Tómbola ABC y sus
métodos abstractos están escritos en cursiva, según las convenciones de UML. La flecha
discontinua se usa para la implementación de la interfaz, aquí la estamos usando para mostrar que
TomboÿList es una subclase virtual de Tombola porque está registrada, como veremos más
adelante en este capítulo.10
Ejemplo 11-9. tombola.py: Tombola es un ABC con dos métodos abstractos y dos métodos
concretos
importar abc
clase Tómbola(abc.ABC):
@abc.abstractmethod
def load(self, iterable):
"""Agregar elementos de un iterable."""
@abc.abstractmethod
def pick(self): """Eliminar
elemento al azar y devolverlo.
10. "registrado" y "subclase virtual" no son palabras estándar de UML. Los estamos usando para representar una clase.
relación que es específica de Python.
def loading(self):
"""Retorna `True` si hay al menos 1 elemento, `False` de lo contrario.""" return
bool(self.inspect())
def inspect(self):
"""Retorna una tupla ordenada con los elementos actualmente dentro.""" items
= [] while True:
prueba: items.append(self.pick())
excepto LookupError: break
self.load(elementos)
return tuple(ordenados(elementos))
Un método abstracto se marca con el decorador @abstractmethod y, a menudo, su cuerpo está vacío
excepto por una cadena de documentación.11 La cadena de documentación indica a los
Los métodos concretos en un ABC deben basarse únicamente en la interfaz definida por el ABC (es
decir, otros métodos o propiedades concretos o abstractos del ABC).
No podemos saber cómo las subclases concretas almacenarán los elementos, pero podemos generar
El método .inspect() del Ejemplo 11-9 es quizás un ejemplo tonto, pero muestra que, dados .pick() y .load(...)
podemos inspeccionar lo que hay dentro de la tómbola seleccionando todos los elementos y volviéndolos a
cargar. El objetivo de este ejemplo es resaltar que está bien proporcionar métodos concretos en ABC, siempre
que solo dependan de otros métodos en la interfaz. Ser consciente de sus estructuras de datos internas,
subclases concretas de Tom
11. Antes de que existieran los ABC, los métodos abstractos usaban la declaración raise NotImplementedError para señalar
que las subclases eran responsables de su implementación.
bola siempre puede anular .inspect() con una implementación más inteligente, pero no lo hacen
tengo que.
El método .loaded() del Ejemplo 11-9 puede no ser tan tonto, pero es costoso:
llama a .inspect() para construir la tupla ordenada solo para aplicarle bool() . Esto funciona, pero un
subclase concreta puede hacerlo mucho mejor, como veremos.
Tenga en cuenta que nuestra implementación indirecta de .inspect() requiere que capturemos un
LookupError lanzado por self.pick(). El hecho de que self.pick() pueda generar LookupEr
ror también es parte de su interfaz, pero no hay forma de declarar esto en Python, excepto en
la documentación (consulte la cadena de documentación para el método de selección abstracta en el ejemplo 11-9).
BaseException
ÿÿÿ SistemaSalir
ÿÿÿ Interrupción de teclado
ÿÿÿ GeneradorSalir
ÿÿÿ
Excepción
ÿÿÿ Detener iteración
ÿÿÿ Error aritmético
ÿ ÿÿÿ Error de punto flotante
ÿ ÿÿÿ Error de desbordamiento
ÿ ÿÿÿ Error de división cero
ÿÿÿ Error de afirmación
ÿÿÿ Error de atributo
ÿÿÿ Error de búfer
ÿÿÿ EOFError
ÿÿÿ Error de importación
ÿÿÿ Error de búsqueda
ÿ ÿÿÿ Error de índice ÿ
ÿÿÿ
KeyError
ÿÿÿ MemoryError
... etc.
KeyError se genera cuando usamos una clave inexistente para obtener un elemento de un mapeo.
Ahora tenemos nuestra propia Tómbola ABC. Para presenciar la verificación de la interfaz realizada por un
ABC, intentemos engañar a Tombola con una implementación defectuosa en el Ejemplo 11-11.
TypeError se genera cuando intentamos crear una instancia de Fake. El mensaje es muy claro:
Fake se considera abstracto porque no implementó la carga, uno de los métodos abstractos
declarados en la Tómbola ABC.
Así que tenemos nuestro primer ABC definido y lo ponemos a trabajar validando una clase. Pronto
subclasificaremos la tómbola ABC, pero primero debemos cubrir algunas reglas de codificación ABC.
Detalles de la sintaxis de
ABC La mejor forma de declarar un ABC es subclasificar abc.ABC o cualquier otro ABC.
Sin embargo, la clase abc.ABC es nueva en Python 3.4, por lo que si está utilizando una versión anterior
de Python, y no tiene sentido crear una subclase de otra ABC existente, debe usar la palabra clave
metaclass= en la declaración de clase , señalando a abc.ABCMeta (no abc.ABC). En el Ejemplo 11-9,
escribiríamos:
clase Tómbola(metaclase=abc.ABCMeta):
# ...
El argumento de la palabra clave metaclass= se introdujo en Python 3. En Python 2, debe usar el atributo
de clase __metaclass__ :
Explicaremos las metaclases en el Capítulo 21. Por ahora, aceptemos que una metaclase es un tipo
especial de clase, y aceptemos que ABC es un tipo especial de clase; por ejemplo, las clases "normales"
no verifican las subclases, por lo que este es un comportamiento especial de ABC.
class MyABC(abc.ABC):
@classmethod
@abc.abstractmethod def
an_abstract_classmethod(cls, ...):
pasar
Ahora que hemos cubierto estos problemas de sintaxis ABC, pongamos a Tombola en uso
implementando algunos descendientes concretos de él.
Tómbola ABC, ahora desarrollaremos dos subclases concretas que satisfagan su interfaz. Estas clases
se representaron en la figura 11-4, junto con la subclase virtual que se analizará en la siguiente sección.
La clase BingoCage del Ejemplo 11-12 es una variación del Ejemplo 5-8 que usa un mejor aleatorizador.
Este BingoCage implementa los métodos abstractos requeridos load y pick, hereda la carga de Tombola,
anula la inspección y agrega __call__.
importar al azar
clase BingoCage(Tómbola):
def __call__(self):
self.pick()
Imagina que usaremos esto para juegos en línea. random.SystemRandom implementa la API
aleatoria además de la función os.urandom(...) , que proporciona bytes aleatorios "adecuados
para uso criptográfico" de acuerdo con los documentos del módulo os .
__call__ también es del Ejemplo 5-8. No es necesario para satisfacer la interfaz de Tombola ,
pero no hay nada de malo en agregar métodos adicionales.
BingoCage hereda los costosos métodos de inspección cargados y tontos de Tombo la. Ambos podrían
anularse con frases ingeniosas mucho más rápidas, como en el ejemplo 11-13. El punto es: podemos
ser perezosos y simplemente heredar los métodos concretos subóptimos de un ABC.
Los métodos heredados de Tombola no son tan rápidos como podrían ser para BingoCage, pero
proporcionan resultados correctos para cualquier subclase de Tombola que implemente correctamente
la selección y carga.
El ejemplo 11-13 muestra una implementación muy diferente pero igualmente válida de la interfaz Tombo
la . En lugar de barajar las "bolas" y hacer estallar la última, LotteryBlower salta desde una posición
aleatoria.
Ejemplo 11-13. lotto.py: LotteryBlower es una subclase concreta que anula los métodos de inspección
y carga de Tombola
importar al azar
clase LotteryBlower(Tómbola):
def cargado(auto):
return bool(auto._bolas)
def inspeccionar(self):
return tuple(sorted(self._balls))
El inicializador acepta cualquier iterable: el argumento se usa para construir una lista.
Anular cargado para evitar llamar a inspeccionar (como lo hace Tombola.loaded en el Ejemplo
11-9). Podemos hacerlo más rápido trabajando con self._balls directamente, sin necesidad de
construir una tupla ordenada completa.
El ejemplo 11-13 ilustra una expresión que vale la pena mencionar: en __init__, self._balls almacena
list(iterable) y no solo una referencia a iterable (es decir, no asignamos simplemente iterable a
self._balls). Como se mencionó anteriormente,13 esto hace que nuestro LotteryBlower sea flexible
porque el argumento iterable puede ser de cualquier tipo iterable. Al mismo tiempo, nos aseguramos
de almacenar sus elementos en una lista para que podamos sacar elementos. E incluso si siempre
obtenemos listas como argumento iterable , list(iterable) produce una copia del argumento,
13. Di esto como un ejemplo de mecanografía de patos después de “Waterfowl and ABCs” de Martelli en la página 314.
lo cual es una buena práctica teniendo en cuenta que eliminaremos elementos de él y es posible que
el cliente no espere que se cambie la lista de elementos que proporcionó . .
Esto se hace llamando a un método de registro en el ABC. La clase registrada entonces se convierte
en una subclase virtual de ABC, y será reconocida como tal por funciones como issubclass e isinstance,
pero no heredará ningún método o atributo de ABC.
El método de registro generalmente se invoca como una función simple (consulte “Uso de registro en
la práctica” en la página 338), pero también se puede usar como decorador. En el ejemplo 11-14,
usamos la sintaxis del decorador e implementamos TomboList, una subclase virtual de Tombola
representada en la figura 11-5.
TomboList funciona como se anuncia, y las pruebas documentales que lo prueban se describen en “Cómo
se probaron las subclases de Tombola” en la página 335.
14. "Programación defensiva con parámetros mutables" en la página 232 en el Capítulo 8 se dedicó a la creación de alias
Figura 11-5. Diagrama de clase UML para TomboList, una subclase real de lista y una virtual
subclase de tómbola
@Tombola.register # clase
TomboList(lista): #
def elegir(auto):
if self: # posición
= randrange(len(self))
return self.pop(posición) # else:
cargar = lista.extender #
def cargado(auto):
volver bool(uno mismo) #
def inspeccionar(auto):
return tuple(ordenado(auto))
# Tombola.registro(TomboList) #
Tombolist hereda __bool__ de la lista, y eso devuelve True si la lista no está vacía.
Nuestra selección llama a self.pop, heredado de la lista, pasando un índice de elementos aleatorios.
Tenga en cuenta que debido al registro, las funciones issubclass e isinstance actúan como si
TomboList fuera una subclase de Tombola:
Sin embargo, la herencia está guiada por un atributo de clase especial denominado __mro__, el orden de
resolución de métodos. Básicamente, enumera la clase y sus superclases en el orden que usa Python para
buscar métodos.16 Si inspecciona el __mro__ de TomboList, verá que enumera solo las superclases “reales”:
lista y objeto:
>>> TomboList.__mro__
(<clase 'tombolista.TomboList'>, <clase 'lista'>, <clase 'objeto'>)
Tombola no está en Tombolist.__mro__, por lo que Tombolist no hereda ningún método de Tombola.
15. El mismo truco que usé con carga no funciona con cargada, porque el tipo de lista no implementa __bool__, el
método que tendría que vincular a cargada. Por otro lado, la función incorporada bool no necesita __bool__ para
funcionar porque también puede usar __len__. Ver “4.1. Prueba del valor de verdad” en el capítulo “Tipos integrados”.
16. Hay una sección completa que explica el atributo de clase __mro__ en “Herencia múltiple y orden de resolución de
métodos” en la página 351. En este momento, esta breve explicación servirá.
Mientras codificaba diferentes clases para implementar la misma interfaz, quería una forma de enviarlas todas al
mismo conjunto de pruebas de documentos. La siguiente sección muestra cómo aproveché la API de clases
regulares y ABC para hacerlo.
Método que devuelve una lista de las subclases inmediatas de la clase. La lista no incluye
subclases virtuales.
_abc_registry
Atributo de datos, disponible solo en ABC, que está vinculado a un WeakSet con referencias débiles a
subclases virtuales registradas de la clase abstracta.
Para probar todas las subclases de Tombola , escribí un script para iterar sobre una lista creada a partir de Tombo
la.__subclasses__() y Tombola._abc_registry, y vinculé cada clase con el nombre ConcreteTombola utilizado en
las pruebas de documentos.
$ python3 tombola_runner.py
23 pruebas, 0 falló - OK 23
BingoCage LotteryBlower
pruebas, 0 falló - OK 23 pruebas,
TumblingDrum TomboList
0 falló - OK 23 pruebas, 0 falló -
OK
El script de prueba es el Ejemplo 11-15 y las pruebas documentales están en el Ejemplo 11-16.
importar documento
TEST_FILE = 'tombola_tests.rst'
TEST_MSG = '{0:16} {1.attempted:2} pruebas, {1.failed:2} falló - {2}'
def main(argv):
verbose = '-v' in argv
real_subclasses = Tombola.__subclasses__()
virtual_subclasses = list(Tombola._abc_registry)
res = doctest.testfile(
TEST_FILE,
globs={'ConcreteTombola': cls},
verbose=verbose,
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE) etiqueta
= 'FAIL' si res.failed else 'OK' print(TEST_MSG.format(cls.__name__, res,
tag))
Importe módulos que contengan subclases reales o virtuales de Tombola para realizar
pruebas. __subclasses__() enumera los descendientes directos que están vivos en la memoria.
Es por eso que importamos los módulos para probar, incluso si no hay más mención de ellos en
el código fuente: para cargar las clases en la memoria.
Cree una lista a partir de _abc_registry (que es un WeakSet) para que podamos concatenarla
con el resultado de __subclasses__().
Iterar sobre las subclases encontradas, pasando cada una a la función de prueba .
El argumento cls , la clase que se probará, está vinculado al nombre ConcreteTom bola en el
espacio de nombres global proporcionado para ejecutar el doctest.
Verdadero >>>
globo.inspeccionar() (0, 1, 2)
>>> selecciones
= [] >>> selecciones.append(globo.pick())
>>> selecciones.append(globo.pick())
>>> selecciones.append(globo.pick())
>>> globo.loaded()
Falso
>>> ordenado(selecciona) == bolas
Verdadero
Recargar::
Nota: las 2 pruebas anteriores tienen una probabilidad *muy* pequeña de fallar
incluso si la implementación es correcta. La probabilidad de que salgan las 100
bolas, por casualidad, en el orden en que fueron inspeccionadas es 1/100!, o
aproximadamente 1.07e-158. Es mucho más fácil ganar la lotería o convertirse en
multimillonario trabajando como programador.
EL FIN
Esto concluye nuestro estudio de caso de Tómbola ABC. En la siguiente sección, abordaremos cómo
se usa la función de registro ABC en la naturaleza.
Sin embargo, incluso si ahora se puede usar el registro como decorador, se implementa más
ampliamente como una función para registrar clases definidas en otro lugar. Por ejemplo, en el código
fuente del módulo collections.abc , los tipos incorporados tupla, str, range y memoryview se registran
como subclases virtuales de Sequence como esta:
Secuencia.registrar(tupla)
Secuencia.registrar(str)
Secuencia.registrar(rango)
Secuencia.registrar(vista de memoria)
Varios otros tipos integrados están registrados en ABC en _colecciones_abc.py. Esos registros
ocurren solo cuando se importa ese módulo, lo cual está bien porque tendrá que importarlo de todos
modos para obtener el ABC: necesita acceso a MutableMapping para poder escribir isinstance
(my_dict, MutableMapping).
Finalizaremos este capítulo explicando un poco de magia ABC que Alex Martelli interpretó en “Aves
acuáticas y ABC” en la página 314.
En su ensayo Waterfowl and ABCs , Alex muestra que una clase puede reconocerse como una
subclase virtual de un ABC incluso sin registrarse. Aquí está su ejemplo nuevamente, con una prueba
adicional usando issubclass:
Class Struggle se considera una subclase de abc.Sized por la función issubclass (y, en
consecuencia, también por isinstance ) porque abc.Sized implementa un método de clase especial
llamado __subclasshook__. Vea el Ejemplo 11-17.
Ejemplo 11-17. Definición de tamaño del código fuente de Lib/ _collections_abc.py (Python 3.4)
__ranuras__ = ()
@abstractmethod
def __len__(self):
devuelve 0
@classmethod
def __subclasshook__(cls, C): si cls
tiene tamaño:
si alguno("__len__" en B.__dict__ para B en C.__mro__): #
devolver verdadero
# devolver no implementado #
Si está interesado en los detalles de la comprobación de subclases, consulte el código fuente del
método ABCMeta.__subclasscheck__ en Lib/ abc.py. Cuidado: tiene muchos ifs y dos llamadas
recursivas.
¿Es una buena idea implementar __subclasshook__ en nuestro propio ABC? Probablemente no.
Todas las implementaciones de __subclasshook__ que he visto en el código fuente de Python
están en ABC como Sized que declaran solo un método especial, y simplemente buscan ese
nombre de método especial. Dado su estado "especial", puede estar bastante seguro de que
cualquier método llamado __len__ hace lo que espera. Pero incluso en el ámbito de los métodos
especiales y los ABC fundamentales, puede ser arriesgado hacer tales suposiciones. Por ejemplo,
las asignaciones implementan __len__, __getitem__ y __iter__ , pero con razón no se consideran
un subtipo de Secuencia, porque no puede recuperar elementos utilizando un desplazamiento de
entero y no ofrecen garantías sobre el orden de los elementos, excepto, por supuesto, para
OrderedDict, que conserva el orden de inserción, pero tampoco admite la recuperación de elementos por desplazamien
Para ABCs que usted y yo podemos escribir, un __subclasshook__ sería aún menos confiable.
No estoy listo para creer que cualquier clase llamada Spam que implemente o herede
cargar, seleccionar, inspeccionar y cargar se comporte como una tómbola. Es mejor dejar
que el programador lo afirme subclasificando Spam de Tombola, o al menos registrándolo:
Tombo la.register(Spam). Por supuesto, su __subclasshook__ también podría verificar las
firmas de métodos y otras funciones, pero no creo que valga la pena.
Con el Ejemplo 11-3, observamos cuán profundamente admite Python el protocolo de secuencia.
Si una clase implementa __getitem__ y nada más, Python logra iterar sobre ella y el operador in
simplemente funciona. Luego volvimos al antiguo ejemplo de FrenchDeck del Capítulo 1 para
admitir la reproducción aleatoria agregando dinámicamente un método. Esto ilustró el parcheo de
monos y enfatizó la naturaleza dinámica de los protocolos. Una vez más, vimos cómo un protocolo
parcialmente implementado puede ser útil: solo agregar __setitem__ del protocolo de secuencia
mutable nos permitió aprovechar una función lista para usar de la biblioteca estándar: random.shuffle.
Conocer los protocolos existentes nos permite aprovechar al máximo la rica biblioteca estándar de
Python.
Alex Martelli luego introdujo el término “tipo de ganso”17 para describir un nuevo estilo de programación
en Python. Con la "tipificación de ganso", los ABC se utilizan para hacer que las interfaces sean
explícitas y las clases pueden pretender implementar una interfaz subclasificando un ABC o
registrándose con él, sin requerir el vínculo fuerte y estático de una relación de herencia.
El ejemplo de FrenchDeck2 dejó en claro los principales inconvenientes y ventajas de los ABC
explícitos. La herencia de abc.MutableSequence nos obligó a implementar dos métodos que realmente
no necesitábamos: insert y __delitem__. Por otro lado, incluso un novato en Python puede ver
FrenchDeck2 y ver que es una secuencia mutable. Y, como beneficio adicional, heredamos 11 métodos
listos para usar de abc.MutableSequence (cinco indirectamente de abc.Sequence).
Después de una vista panorámica de los ABC existentes de collections.abc en la figura 11-3, escribimos
un ABC desde cero. Doug Hellmann, creador del genial PyMOTW.com (Módulo de la semana de
Python) explica la motivación:
Al definir una clase base abstracta, se puede establecer una API común para un conjunto de subclases.
Esta capacidad es especialmente útil en situaciones en las que alguien menos
familiarizado con el código fuente de una aplicación va a proporcionar extensiones de
plug-in...18 Poniendo a trabajar el ABC de Tómbola , creamos tres subclases concretas: dos heredadas
de Tómbola, la otra una subclase virtual registrados con él, todos pasando el mismo conjunto de
pruebas.
Al concluir el capítulo, mencionamos cómo varios tipos incorporados se registran en ABC en el módulo
collections.abc para que pueda preguntar isinstance (memoryview, abc.Sequence) y obtener True,
incluso si memoryview no hereda de abc.Sequence . Y finalmente repasamos la magia
__subclasshook__ , que permite que un ABC reconozca cualquier clase no registrada como una
subclase, siempre que pase una prueba que puede ser tan simple o tan compleja como desee: los
ejemplos en la biblioteca estándar simplemente verifican el método. nombres
Para resumir, me gustaría reafirmar la advertencia de Alex Martelli de que debemos abstenernos de
crear nuestros propios ABC, excepto cuando estemos construyendo marcos extensibles para el usuario,
lo que la mayoría de las veces no hacemos. Diariamente, nuestro contacto con los ABC debe ser la
subclasificación o el registro de clases con los ABC existentes. Con menos frecuencia que subclasificar
o registrar, podríamos usar ABC para verificaciones de instancias . Y aún más raramente, si es que
alguna, encontramos la ocasión de escribir un nuevo ABC desde cero.
Después de 15 años de Python, la primera clase abstracta que escribí que no es un ejemplo didáctico
fue la clase Board del proyecto Pingo . Los controladores que admiten diferentes controladores y
computadoras de una sola placa son subclases de Board, por lo que comparten la misma interfaz. En
17. Alex acuñó la expresión "ganso escribiendo" y esta es la primera vez que aparece en un libro.
18. PyMOTW, página del módulo abc, sección "¿Por qué usar clases base abstractas?"
realidad, aunque concebida e implementada como una clase abstracta, la clase pingo.Board no subclasifica
abc.ABC mientras escribo esto.19 Tengo la intención de hacer de Board un ABC explícito eventualmente, pero hay
cosas más importantes que hacer en el proyecto.
Aunque ABC facilita la verificación de tipos, no es algo que deba usar en exceso en un programa.
En esencia, Python es un lenguaje dinámico que le brinda una gran flexibilidad. Tratar de hacer
cumplir las restricciones de tipo en todas partes tiende a dar como resultado un código que es más
complicado de lo que debe ser. Debería adoptar la flexibilidad de Python.20
O, como escribió el revisor técnico Leonardo Rochael: “Si se siente tentado a crear un ABC personalizado, primero
intente resolver su problema mediante el tipeo regular”.
Otras lecturas
Python Cookbook de Beazley and Jones , 3.ª edición (O'Reilly) tiene una sección sobre la definición de un ABC
(Receta 8.12). El libro se escribió antes de Python 3.4, por lo que no usan la sintaxis ahora preferida al declarar
ABC mediante subclases de abc.ABC en lugar de usar la palabra clave metaclase . Aparte de este pequeño detalle,
la receta cubre muy bien las principales características del ABC y termina con los valiosos consejos citados al final
de la sección anterior.
La biblioteca estándar de Python por ejemplo de Doug Hellmann (Addison-Wesley), tiene un capítulo sobre el
módulo abc . También está disponible en la Web en el excelente Py-MOTW de Doug: Python Module of the Week.
Tanto el libro como el sitio se enfocan en Python 2; por lo tanto, se deben realizar ajustes si está utilizando Python
3. Y para Python 3.4, recuerde que el único decorador de método ABC recomendado es @abstractmethod ; los
demás quedaron en desuso. La otra cita sobre ABC en el resumen del capítulo es del sitio y libro de Doug.
Cuando se usan ABC, la herencia múltiple no solo es común sino prácticamente inevitable, porque cada una de las
colecciones fundamentales ABC (secuencia, mapeo y conjunto) extiende múltiples ABC (consulte la figura 11-3).
Por lo tanto, el Capítulo 12 es una continuación importante de este.
PEP 3119: Introducción a las clases base abstractas brinda la justificación del ABC, y PEP 3141: una jerarquía de
tipos para números presenta el ABC del módulo de números .
19. También encontrarás eso en la biblioteca estándar de Python: clases que de hecho son abstractas pero nadie las creó
explícitamente así.
20. Python Cookbook, 3.ª edición (O'Reilly), “Receta 8.12. Definición de una interfaz o clase base abstracta”, p. 276.
Para una discusión de los pros y los contras de la tipificación dinámica, vea la entrevista de
Guido van Rossum a Bill Venners en “Contracts in Python: A Conversation with Guido van
Rossum, Part IV”.
Plataforma improvisada
Sugerencias de tipo
Probablemente, la noticia más importante en el mundo de Python en 2014 fue que Guido van Rossum dio luz verde
a la implementación de la verificación de tipo estático opcional mediante anotaciones de función, similar a lo que
hace el verificador Mypy . Esto sucedió en la lista de correo de Python-ideas el 15 de agosto. El mensaje es Escritura
estática opcional: la encrucijada. Al mes siguiente, se publicó como borrador PEP 484 - Type Hints , escrito por
Guido.
La idea es permitir que los programadores usen opcionalmente anotaciones para declarar parámetros y tipos de
retorno en definiciones de funciones. La palabra clave aquí es opcionalmente. Solo agregaría tales anotaciones si
desea los beneficios y las limitaciones que vienen con ellas, y podría colocarlas en algunas funciones pero no en
otras.
En la superficie, esto puede sonar como lo que hizo Microsoft con TypeScript, su superconjunto de Java Script,
excepto que TypeScript va mucho más allá: agrega nuevas construcciones de lenguaje (por ejemplo, módulos,
clases, interfaces explícitas, etc.), permite declaraciones de variables tipeadas, y en realidad se compila en
JavaScript simple. En el momento de escribir este artículo, los objetivos de la tipificación estática opcional en Python
son mucho menos ambiciosos.
Para entender el alcance de esta propuesta, hay un punto clave que Guido hace en el histórico correo electrónico
del 15 de agosto de 2014:
Voy a hacer una suposición adicional: los principales casos de uso serán linting, IDE y
generación de documentos. Todos estos tienen una cosa en común: debería ser posible
ejecutar un programa aunque no pueda verificar el tipo. Además, agregar tipos a un
programa no debería entorpecer su desempeño (ni ayudará :-).
Entonces, parece que este no es un movimiento tan radical como parece al principio. PEP 482 - Descripción general
de la literatura para sugerencias de tipo está referenciado por PEP 484 - Sugerencias de tipo, y documenta
brevemente las sugerencias de tipo en herramientas Python de terceros y en otros idiomas.
Radicales o no, las sugerencias de tipo están sobre nosotros: es probable que el soporte para PEP 484 en forma de
un módulo de escritura ya llegue a Python 3.5. La forma en que está redactada la propuesta y
implementado deja en claro que ningún código existente dejará de ejecutarse debido a la falta de sugerencias de tipo, o su
adición, para el caso.
También se debe enfatizar que Python seguirá siendo un lenguaje de escritura dinámica, y los autores
no tienen ningún deseo de hacer que las sugerencias de escritura sean obligatorias, incluso por convención.
Las discusiones sobre las disciplinas de mecanografía de idiomas a veces se confunden debido a la falta de una terminología
uniforme. Algunos escritores (como Bill Venners en la entrevista con Guido mencionada en “Lectura adicional” en la página
342), dicen que Python tiene una escritura débil, lo que lo coloca en la misma categoría de JavaScript y PHP. Una mejor manera
de hablar sobre la disciplina de mecanografía es considerar dos ejes diferentes: Mecanografía fuerte frente a débil Si el lenguaje
rara vez realiza una conversión implícita de tipos, se considera fuertemente tipado; si lo hace a menudo, está débilmente
La tipificación estática facilita que las herramientas (compiladores, IDE) analicen el código para detectar errores y brindar otros
servicios (optimización, refactorización, etc.). La escritura dinámica aumenta las oportunidades de reutilización, reduce el
número de líneas y permite que las interfaces surjan naturalmente como protocolos, en lugar de imponerse desde el principio.
21. Adaptado de JavaScript de Douglas Crockford: The Good Parts (O'Reilly), Apéndice B, p. 109.
Para resumir, Python usa escritura dinámica y fuerte. PEP 484: las sugerencias de tipo no cambiarán eso,
pero permitirán a los autores de API agregar anotaciones de tipo opcionales para que las herramientas
puedan realizar algunas comprobaciones de tipo estático.
Monkey Patching
Monkey patching tiene mala reputación. Si se abusa, puede conducir a sistemas que son difíciles de entender
y mantener. El parche suele estar estrechamente acoplado con su objetivo, lo que lo hace quebradizo. Otro
problema es que dos bibliotecas que aplican parches de mono pueden pisar los dedos de los pies, con la
segunda biblioteca para ejecutarse destruyendo parches de la primera.
Pero los parches mono también pueden ser útiles, por ejemplo, para hacer que una clase implemente un
protocolo en tiempo de ejecución. El patrón de diseño del adaptador resuelve el mismo problema al
implementar una clase completamente nueva.
Es fácil parchear el código de Python, pero existen limitaciones. A diferencia de Ruby y JavaÿScript, Python
no le permite parchear los tipos incorporados. De hecho, considero que esto es una ventaja, porque puede
estar seguro de que un objeto str siempre tendrá los mismos métodos. Esta limitación reduce la posibilidad
de que las bibliotecas externas intenten aplicar parches conflictivos.
Desde C++ 2.0 (1989), las clases abstractas se han utilizado para especificar interfaces en ese lenguaje. Los
diseñadores de Java optaron por no tener herencia múltiple de clases, lo que impidió el uso de clases
abstractas como especificaciones de interfaz, porque a menudo una clase necesita implementar más de una
interfaz. Pero agregaron la interfaz como una construcción del lenguaje, y una clase puede implementar más
de una interfaz, una forma de herencia múltiple. Hacer que las definiciones de interfaz sean más explícitas
que nunca fue una gran contribución de Java. Con Java 8, una interfaz puede proporcionar implementaciones
de métodos, denominados métodos predeterminados. Con esto, las interfaces de Java se acercaron más a
las clases abstractas en C++ y Python.
El lenguaje Go tiene un enfoque completamente diferente. En primer lugar, no hay herencia en Go. Puede
definir interfaces, pero no necesita (y en realidad no puede) decir explícitamente que cierto tipo implementa
una interfaz. El compilador lo determina automáticamente. Entonces, lo que tienen en Go podría llamarse
"tipado de pato estático", en el sentido de que las interfaces se verifican en el momento de la compilación,
pero lo que importa es qué tipos realmente implementan.
mento
En comparación con Python, es como si, en Go, cada ABC implementara el __subclasshook__ para verificar
nombres y firmas de funciones, y nunca subclasificó ni registró un ABC. Si quisiéramos que Python se
pareciera más a Go, tendríamos que realizar verificaciones de tipo en todos los argumentos de función. Parte
de la infraestructura está disponible (recuerde “Anotaciones de función” en la página 154). Guido ya ha dicho
que cree que está bien usar esas anotaciones para la verificación de tipos, al menos en las herramientas de
soporte. Consulte “Soapbox” en la página 163 en el Capítulo 5 para obtener más información al respecto.
Los rubyistas son firmes creyentes en el tipo de pato, y Ruby no tiene una forma formal de declarar una
interfaz o una clase abstracta, excepto hacer lo mismo que hicimos en Python antes de 2.6: aumentar
Mientras tanto, leí que Yukihiro "Matz" Matsumoto, creador de Ruby, dijo en un discurso de apertura en
septiembre de 2014 que la escritura estática puede estar en el futuro del lenguaje. Eso fue en Ruby Kaigi en
Japón, una de las conferencias de Ruby más importantes cada año. Mientras escribo esto, no he visto una
transcripción, pero Godfrey Chan lo publicó en su blog: “Ruby Kaigi 2014: Día 2”. Según el informe de Chan,
parece que Matz se centró en las anotaciones de funciones.
Incluso se mencionan las anotaciones de funciones de Python.
Me pregunto si las anotaciones de funciones serían realmente buenas sin ABC para agregar estructura al
sistema de tipos sin perder flexibilidad. Entonces, tal vez las interfaces formales también estén en el futuro de
Ruby.
Creo que Python ABCs, con la función de registro y __subclasshook__, trajo interfaces formales al lenguaje
sin desechar las ventajas de la tipificación dinámica.
interfaces Una metáfora fomenta la comprensión al aclarar las limitaciones. Ese es el valor de las palabras
"pila" y "cola" al describir esas estructuras de datos fundamentales: aclaran cómo se pueden agregar o eliminar
elementos. Por otro lado, Alan Cooper escribe en About Face, 4E (Wiley):
About Face es, con mucho, el mejor libro sobre diseño de interfaz de usuario que he leído, y he leído algunos.
Dejar de lado las metáforas como paradigma de diseño y reemplazarlas con "interfaces idiomáticas" fue lo
más valioso que aprendí del trabajo de Cooper. Como se mencionó, Cooper no se ocupa de las API, pero
cuanto más pienso en sus ideas, más veo cómo se aplican a Python. Los protocolos fundamentales del
lenguaje son lo que Cooper llama "modismos".
Una vez que aprendemos qué es una "secuencia", podemos aplicar ese conocimiento en diferentes contextos.
Este es un tema principal de Fluent Python: resaltar los modismos fundamentales del lenguaje, para que su
código sea conciso, efectivo y legible, para un Pythonista fluido.
CAPÍTULO 12
[Nosotros] comenzamos a impulsar la idea de la herencia como una forma de permitir que los novatos construyan
marcos que solo pueden ser diseñados por expertos.1 .
— Alan Kay
La historia temprana de Smalltalk
Este capítulo trata sobre herencia y subclases, con énfasis en dos detalles que son muy específicos de
Python:
Muchos consideran que la herencia múltiple es más problemática de lo que vale. La falta de ella
ciertamente no perjudicó a Java; probablemente impulsó su adopción generalizada después de que
muchos quedaran traumatizados por el uso excesivo de la herencia múltiple en C++.
Sin embargo, el sorprendente éxito y la influencia de Java significa que muchos programadores llegan a
Python sin haber visto la herencia múltiple en la práctica. Esta es la razón por la que, en lugar de ejemplos
de juguetes, nuestra cobertura de la herencia múltiple se ilustrará con dos importantes proyectos de
Python: el kit de herramientas GUI de Tkinter y el marco web de Django.
Comenzaremos con el tema de las subclases incorporadas. El resto del capítulo cubrirá la herencia
múltiple con nuestros casos de estudio y discutirá buenas y malas prácticas al construir jerarquías de
clases.
1. Alan Kay, “La historia temprana de Smalltalk”, en SIGPLAN Not. 28, 3 (marzo de 1993), 69–95. También disponible
en línea. Gracias a mi amigo Christiano Anderson que compartió esta referencia mientras escribía este capítulo.
347
Machine Translated by Google
Una buena breve descripción del problema se encuentra en la documentación de PyPy, en “Diferencias
entre PyPy y CPython”, sección Subclases de tipos incorporados:
Ejemplo 12-1. Nuestra anulación de __setitem__ es ignorada por los métodos __init__ y __update__ del
dict incorporado
>>> clase DoppelDict(dict):
... def __setitem__(auto, clave, valor):
... super().__setitem__(clave, [valor] * 2) #
...
>>> dd = DoppelDict(uno=1) # >>>
dd {'uno': 1} >>> dd['dos'] = 2 # >>>
dd {'uno': 1, 'dos': [2, 2]} >>>
dd.update(tres=3) # >>> dd {'tres':
3, 'uno': 1, 'dos': [2, 2]}
DoppelDict.__setitem__ duplica los valores al almacenar (sin una buena razón, solo para tener un
efecto visible). Funciona delegando a la superclase.
El método __init__ heredado de dict claramente ignoró que __setitem__ fue anulado: el valor de
'uno' no está duplicado.
El operador [] llama a nuestro __setitem__ y funciona como se esperaba: 'dos' se asigna al valor
duplicado [2, 2].
El método de actualización de dict tampoco usa nuestra versión de __setitem__ : el valor de 'tres'
no se duplicó.
Este comportamiento incorporado es una violación de una regla básica de la programación orientada a
objetos: la búsqueda de métodos siempre debe comenzar desde la clase de la instancia de destino (yo),
incluso cuando la llamada ocurre dentro de un método implementado en una superclase. En este triste estado de
El problema no se limita a las llamadas dentro de una instancia, ya sea que las llamadas self.get()
self.__getitem__()), pero también ocurre con métodos anulados de otras clases que
debe ser llamado por los métodos incorporados. El ejemplo 12-2 es un ejemplo adaptado del
Documentación PyPy.
>>> re = {}
>>> d.actualizar(anuncio)
# >>> d['a'] # 'foo'
>>> re
{'a': 'foo'}
Si crea una subclase de collections.UserDict en lugar de dict, los problemas expuestos en los ejemplos
12-1 y 12-2 son fijos. Vea el Ejemplo 12-3.
>>>
>>> class AnswerDict2(colecciones.UserDict): def
... __getitem__(self, key): return 42
...
...
>>> anuncio = AnswerDict2(a='foo')
>>> anuncio['a'] 42 >>> d = {} >>>
d.update(anuncio) >>> d['a'] 42
>>>
d {'a': 42}
Como experimento para medir el trabajo adicional requerido para crear una subclase
integrada, reescribí la clase StrKeyDict del Ejemplo 3-8. La versión original se heredó de las
colecciones. UserDict e implementó solo tres métodos: __missing__, __contains__ y
__setitem__. El StrKeyDict experimental subclasificó dict directamente e implementó los
mismos tres métodos con ajustes menores debido a la forma en que se almacenaron los datos.
Pero para que pasara el mismo conjunto de pruebas, tuve que implementar __init__, get y
update porque las versiones heredadas de dict se negaron a cooperar con los anulados
__missing__, __contains__ y __setitem__. La subclase UserDict del Ejemplo 3-8 tiene 16
líneas, mientras que la subclase dict experimental terminó con 37 líneas.2
Para resumir: el problema descrito en esta sección se aplica solo a la delegación de métodos
dentro de la implementación del lenguaje C de los tipos integrados, y solo afecta a las clases
definidas por el usuario derivadas directamente de esos tipos. Si crea una subclase de una
clase codificada en Python, como UserDict o MutableMapping, esto no le preocupará.3
2. Si tiene curiosidad, el experimento se encuentra en el archivo strkeydict_dictsub.py en el repositorio de código de Fluent Python .
3. Por cierto, en este sentido, PyPy se comporta más “correctamente” que CPython, a costa de introducir un menor
incompatibilidad. Consulte "Diferencias entre PyPy y CPython" para obtener más información.
Otro asunto relacionado con la herencia, particularmente con la herencia múltiple, es: ¿cómo decide
Python qué atributo usar si las superclases de ramas paralelas definen atributos con el mismo nombre?
La respuesta es la siguiente.
Figura 12-1. Izquierda: diagrama de clases UML que ilustra el "problema del diamante". Derecha: las
flechas discontinuas representan Python MRO (orden de resolución de métodos) para el ejemplo 12-4.
clase B(A):
def pong(self):
print('pong:', self)
clase C(A):
def pong(auto):
def ping(self):
super().ping()
print('post-ping:', self)
def pingpong(self):
self.ping()
super().ping()
self.pong()
super().pong()
C.pong (uno mismo)
Tenga en cuenta que ambas clases B y C implementan un método pong . La única diferencia es que
C.pong genera la palabra PONG en mayúsculas.
Si llama a d.pong() en una instancia de D, ¿qué método pong se ejecuta realmente? En C++, el
programador debe calificar las llamadas a métodos con nombres de clase para resolver esta ambigüedad.
Esto también se puede hacer en Python. Observe el Ejemplo 12-5.
Ejemplo 12-5. Dos formas de invocar el método pong en una instancia de clase D
Siempre puede llamar directamente a un método en una superclase, pasando la instancia como
un argumento explícito.
La ambigüedad de una llamada como d.pong() se resuelve porque Python sigue un orden específico al
atravesar el gráfico de herencia. Esa orden se llama MRO: Orden de Resolución de Método. Las clases
tienen un atributo llamado __mro__ que contiene una tupla de referencias a las superclases en orden
MRO, desde la clase actual hasta la clase de objeto . Para la clase D , este es el __mro__ (vea la Figura
12-1):
>>> D.__mro__
(<clase 'diamante.D'>, <clase 'diamante.B'>, <clase 'diamante.C'>, <clase
'diamante.A'>, <clase 'objeto'>)
El ejemplo 12-4 ilustra.4 . Sin embargo, también es posible, ya veces conveniente, omitir el MRO e
invocar directamente un método en una superclase. Por ejemplo, el método D.ping podría escribirse
como:
def ping(self):
A.ping(self) # en lugar de super().ping() print('post-
ping:', self)
Tenga en cuenta que cuando llama a un método de instancia directamente en una clase, debe pasar self
explícitamente, porque está accediendo a un método no vinculado.
Sin embargo, es más seguro y está más preparado para el futuro usar super(), especialmente al llamar
a métodos en un marco o cualquier jerarquía de clases que no controle. El ejemplo 12-6 muestra que
super() sigue al MRO cuando se invoca un método.
Ejemplo 12-6. Usando super() para llamar a ping (código fuente en el Ejemplo 12-4)
Ahora veamos qué sucede cuando se llama a pingpong en una instancia de D. Vea el Ejemplo 12-7.
Ejemplo 12-7. Las cinco llamadas realizadas por pingpong (código fuente en el Ejemplo 12-4)
La llamada #1 es self.ping(), que ejecuta el método ping de D, que genera esta línea y la
siguiente.
La llamada #2 es super.ping(), que pasa por alto el ping en D y encuentra el método de ping en
A.
El MRO tiene en cuenta no solo el gráfico de herencia, sino también el orden en que se enumeran las
superclases en una declaración de subclase. En otras palabras, si en diamond.py (Ejemplo 12-4) la
clase D fuera declarada como clase D(C, B):, el __mro__ de la clase D sería diferente: se buscaría C
antes que B.
A menudo reviso el __mro__ de las clases de forma interactiva cuando las estoy estudiando.
El ejemplo 12-8 tiene algunos ejemplos que usan clases familiares.
Para concluir esta discusión del MRO, la Figura 12-2 ilustra parte del complejo gráfico de herencia
múltiple del juego de herramientas GUI de Tkinter de la biblioteca estándar de Python.
Para estudiar la imagen, comience en la clase Texto en la parte inferior. La clase Text implementa un
widget de texto editable de varias líneas con todas las funciones. Tiene una rica funcionalidad propia,
pero también hereda muchos métodos de otras clases. El lado izquierdo muestra un diagrama de
clases UML simple. A la derecha, está decorado con flechas que muestran el MRO, como se muestra
aquí con la ayuda de la función de conveniencia print_mro definida en el Ejemplo 12-8:
Figura 12-2. Izquierda: diagrama de clases UML de la clase de widget Tkinter Text y sus superclases.
Derecha: las flechas discontinuas representan Text.mro.
En la siguiente sección, discutiremos los pros y los contras de la herencia múltiple, con ejemplos de
marcos reales que la usan.
5. Como se mencionó anteriormente, Java 8 también permite que las interfaces proporcionen implementaciones de métodos. La nueva
función se llama Métodos predeterminados en el tutorial oficial de Java.
Figura 12-3. Resumen del diagrama UML para la jerarquía de clases de la GUI de Tkinter; Las clases
etiquetadas como «mixin» están diseñadas para proporcionar métodos concretos a otras clases a través de
herencia múltiple.
Tkinter tiene 20 años cuando escribo esto y no es un ejemplo de las mejores prácticas actuales.
Pero muestra cómo se usaba la herencia múltiple cuando los codificadores no apreciaban sus inconvenientes.
Y servirá como contraejemplo cuando cubramos algunas buenas prácticas en la siguiente sección.
ÿ Widget: La superclase de cada objeto visible que se puede colocar en una ventana.
Aquí están los MRO de esas clases, mostrados por la función print_mro de
Ejemplo 12-8:
>>> importar tkinter
>>> print_mro(tkinter.Toplevel)
Nivel superior, BaseWidget, Varios, Wm, objeto
>>> print_mro(tkinter.Widget)
Widget, BaseWidget, Misceláneo, Paquete, Lugar, Cuadrícula,
objeto >>> print_mro(tkinter.Button)
Botón, Widget, BaseWidget, Varios, Paquete, Lugar, Cuadrícula, objeto
>>> print_mro(tkinter.Entry)
Entrada, Widget, BaseWidget, Varios, Paquete, Lugar, Cuadrícula, XView, objeto
>>> print_mro(tkinter.Text)
Texto, Widget, BaseWidget, Varios, Paquete, Lugar, Cuadrícula, XView, YView, objeto
Cosas a tener en cuenta sobre cómo estas clases se relacionan con otras:
• Toplevel es la única clase gráfica que no se hereda de Widget, porque es la ventana de nivel
superior y no se comporta como un widget; por ejemplo, no se puede adjuntar a una ventana
oa un marco. Toplevel hereda de Wm, que proporciona funciones de acceso directo del
administrador de ventanas del host, como establecer el título de la ventana y configurar sus
bordes.
• Widget hereda directamente de BaseWidget y de Pack, Place y Grid. Estas tres últimas clases
son administradores de geometría: son responsables de organizar los widgets dentro de una
ventana o marco. Cada uno encapsula una estrategia de diseño diferente y una API de
ubicación de widgets.
• Button, como la mayoría de los widgets, desciende solo de Widget, pero indirectamente de
Misc, que proporciona docenas de métodos para cada widget.
Ahora discutiremos algunas buenas prácticas de herencia múltiple y veremos si Tkinter las acepta.
— Alan Kay
La historia temprana de Smalltalk
Como escribió Alan Kay, la herencia se usa por diferentes motivos, y la herencia múltiple agrega
alternativas y complejidad. Es fácil crear diseños incomprensibles y quebradizos usando la herencia
múltiple. Debido a que no tenemos una teoría integral, aquí hay algunos consejos para evitar los
gráficos de clase de espagueti.
En la práctica, ambos usos suelen ser simultáneos, pero siempre que pueda aclarar la intención,
hágalo. La herencia para la reutilización de código es un detalle de implementación y, a menudo, se
puede reemplazar por composición y delegación. Por otro lado, la herencia de interfaz es la columna
vertebral de un marco.
ABC En Python moderno, si una clase está diseñada para definir una interfaz, debería ser un ABC
explícito. En Python ÿ 3.4, esto significa: subclase abc.ABC u otro ABC (consulte “Detalles de la sintaxis
ABC” en la página 328 si necesita compatibilidad con versiones anteriores de Python).
Si una clase está diseñada para proporcionar implementaciones de métodos para su reutilización por
varias subclases no relacionadas, sin implicar una relación "es-un", debe ser una clase mixin explícita.
Conceptualmente, un mixin no define un nuevo tipo; simplemente agrupa métodos para su reutilización.
Nunca se debe crear una instancia de un mixin, y las clases concretas no deben heredar solo de un
mixin. Cada mixin debe proporcionar un solo comportamiento específico, implementando pocos métodos
muy relacionados.
nombrando No existe una forma formal en Python de declarar que una clase es un mixin, por lo que
se recomienda enfáticamente que se nombren con el sufijo …Mixin . Tkinter no sigue este consejo, pero
si lo hiciera, XView sería XViewMixin, Pack sería PackMixin, y así sucesivamente con todas las clases
donde puse la etiqueta «mixin» en la Figura 12-3.
Debido a que un ABC puede implementar métodos concretos, también funciona como una
mezcla. Un ABC también define un tipo, lo que no hace un mixin. Y un ABC puede ser la única
clase base de cualquier otra clase, mientras que un mixin nunca debe subclasificarse solo,
excepto por otro mixin más especializado, lo que no es un arreglo común en el código real.
Una restricción se aplica a los ABC y no a los mixins: los métodos concretos implementados en
un ABC solo deben colaborar con métodos del mismo ABC y sus superclases.
Esto implica que los métodos concretos en un ABC siempre son por conveniencia, porque todo lo
que hacen, un usuario de la clase también puede hacerlo llamando a otros métodos del ABC.
Si alguna combinación de ABC o mixins es particularmente útil para el código del cliente,
proporcione una clase que los reúna de manera sensata. Grady Booch llama a esto una clase
agregada. 7
El cuerpo de Widget está vacío, pero la clase brinda un servicio útil: reúne cuatro superclases
para que cualquiera que necesite crear un nuevo widget no necesite recordar todos esos mixins,
o preguntarse si necesitan ser declarados en un cierto mandar entrar
6. En “Aves acuáticas y ABC” en la página 314, Alex Martelli cita el C++ más efectivo de Scott Meyer , que va más allá:
“todas las clases que no son hojas deben ser abstractas” (es decir, las clases concretas no deben tener superclases
concretas en absoluto). ).
7. “Una clase que se construye principalmente al heredar de mixins y no agrega su propia estructura o comportamiento se
denomina clase agregada”, Grady Booch et al., Object Oriented Analysis and Design, 3E (Addison-Wesley, 2007) , pags.
109.
una declaración de clase . Un mejor ejemplo de esto es la clase ListView de Django , de la que hablaremos en
breve, en “Un ejemplo moderno: Mixins en vistas genéricas de Django” en la página 362.
Esta cita viene directamente del libro Design Patterns ,8 y es el mejor consejo que puedo ofrecer aquí. Una
vez que te sientes cómodo con la herencia, es demasiado fácil abusar de ella. Colocar objetos en una ordenada
jerarquía apela a nuestro sentido del orden; los programadores lo hacen solo por diversión.
Sin embargo, favorecer la composición conduce a diseños más flexibles. Por ejemplo, en el caso de la clase
tkinter.Widget , en lugar de heredar los métodos de todos los administradores de geometría, las instancias de
widget podrían contener una referencia a un administrador de geometría e invocar sus métodos. Después de
todo, un Widget no debería “ser” un administrador de geometría, sino que podría usar los servicios de uno por
delegación. Luego, podría agregar un nuevo administrador de geometría sin tocar la jerarquía de clases de
widgets y sin preocuparse por los conflictos de nombres. Incluso con la herencia única, este principio mejora la
flexibilidad, porque la creación de subclases es una forma de acoplamiento estrecho y los árboles de herencia
altos tienden a ser frágiles.
La composición y la delegación pueden reemplazar el uso de mixins para hacer que los comportamientos
estén disponibles para diferentes clases, pero no pueden reemplazar el uso de herencia de interfaz para definir
una jerarquía de tipos.
Tkinter no sigue la mayoría de los consejos de la sección anterior, siendo el #7 una notable excepción. Incluso
entonces, no es un gran ejemplo, porque la composición probablemente funcionaría mejor para integrar los
administradores de geometría en Widget, como se discutió en el #8.
La cadena de documentación de tkinter.Widget comienza con las palabras "clase interna". Esto sugiere que
Widget probablemente debería ser un ABC. Aunque Widget no tiene métodos propios, sí define una interfaz.
Su mensaje es: “Puede contar con que cada widget de Tkinter proporcione métodos de widget básicos
(__init__, destroy y docenas de funciones de la API de Tk), en
8. Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides, Patrones de diseño: Elementos de software
orientado a objetos reutilizable, Introducción, pág. 20
además de los métodos de los tres administradores de geometría”. Podemos estar de acuerdo en que
esta no es una gran definición de interfaz (es demasiado amplia), pero es una interfaz, y Widget la
“define” como la unión de las interfaces de sus superclases.
La clase Tk , que encapsula la lógica de la aplicación GUI, hereda de Wm y Misc, ninguno de los cuales
es abstracto o mixto (Wm no es una mezcla adecuada porque las subclases TopLevel solo provienen
de él). El nombre de la clase Misc es, por sí mismo, un olor a código muy fuerte . Misc tiene más de 100
métodos y todos los widgets se heredan de él. ¿Por qué es necesario que cada widget tenga métodos
para el manejo del portapapeles, la selección de texto, la gestión del temporizador y similares?
Realmente no puede pegar en un botón o seleccionar texto de una barra de desplazamiento. Misc debe
dividirse en varias clases de mixin especializadas, y no todos los widgets deben heredar de cada uno
de esos mixins.
Para ser justos, como usuario de Tkinter, no necesita saber o usar la herencia múltiple en absoluto.
Es un detalle de implementación oculto detrás de las clases de widgets que instanciará o subclasificará
en su propio código. Pero sufrirá las consecuencias de una herencia múltiple excesiva cuando escriba
dir(tkinter.Button) e intente encontrar el método que necesita entre los 214 atributos enumerados.
A pesar de los problemas, Tkinter es estable, flexible y no necesariamente feo. Los widgets Tk
heredados (y predeterminados) no tienen un tema que coincida con las interfaces de usuario modernas,
pero el paquete tkinter.ttk proporciona widgets bonitos y de aspecto nativo, lo que hace que el desarrollo
de GUI profesional sea viable desde Python 3.1 (2009). Además, algunos de los widgets heredados,
como Canvas y Text, son increíblemente potentes. Con solo un poco de codificación, puede convertir
un objeto de Canvas en una simple aplicación de dibujo de arrastrar y soltar. Definitivamente vale la
pena echarle un vistazo a Tkinter y Tcl/Tk si está interesado en la programación de GUI.
Sin embargo, nuestro tema aquí no es la programación GUI, sino la práctica de la herencia múltiple.
Puede encontrar un ejemplo más actualizado con clases mixin explícitas en Django.
En Django, una vista es un objeto invocable que toma, como argumento, un objeto que representa una
solicitud HTTP y devuelve un objeto que representa una respuesta HTTP. Las diferentes respuestas
son lo que nos interesa en esta discusión. Pueden ser tan simples como una respuesta de
redireccionamiento, sin cuerpo de contenido, o tan complejos como una página de catálogo en una tienda en línea.
renderizado a partir de una plantilla HTML y enumerando múltiples productos con botones para comprar y
enlaces a páginas de detalles.
Sin embargo, las vistas genéricas originales eran funciones, por lo que no eran extensibles. Si necesitara
hacer algo similar pero no exactamente como una vista de lista genérica, tendría que empezar desde cero.
En Django 1.3, se introdujo el concepto de vistas basadas en clases, junto con un conjunto de clases de
vista genéricas organizadas como clases base, mezclas y clases concretas listas para usar.
Las clases base y los mixins están en el módulo base del paquete django.views.generic , que se muestra
en la Figura 12-4. En la parte superior del diagrama vemos dos clases que se encargan de responsabilidades
muy distintas: View y TemplateResponseMixin.
View es la clase base de todas las vistas (podría ser un ABC) y proporciona una
funcionalidad central como el método de envío , que delega métodos de "controlador"
como get, head, post, etc., implementados por subclases concretas para manejar el
diferentes verbos HTTP.9 La clase RedirectView hereda solo de View, y puede ver que
implementa get, head, post, etc.
Se supone que las subclases concretas de View implementan los métodos del controlador, entonces, ¿por
qué no son parte de la interfaz View ? La razón: las subclases son libres de implementar solo los
controladores que desean admitir. Un TemplateView se usa solo para mostrar contenido, por lo que solo
implementa get. Si se envía una solicitud HTTP POST a un TemplateView, el heredado
9. Los programadores de Django saben que el método de la clase as_view es la parte más visible de la interfaz View, pero
no es relevante para nosotros aquí.
TemplateResponseMixin proporciona una funcionalidad que es de interés solo para las vistas
que necesitan usar una plantilla. Un RedirectView, por ejemplo, no tiene cuerpo de contenido,
por lo que no necesita una plantilla y no se hereda de este mixin. TemplateResponseMixin
proporciona comportamientos a TemplateView y otras vistas de representación de plantillas,
como List View, DetailView, etc., definidas en otros módulos del paquete django.views.generic.
10. Si te gustan los patrones de diseño, notarás que el mecanismo de envío de Django es una variación dinámica del patrón del
método de plantilla. Es dinámico porque la clase View no obliga a las subclases a implementar todos los controladores, pero
envía comprobaciones en tiempo de ejecución si un controlador concreto está disponible para la solicitud específica.
años. La Figura 12-5 muestra el módulo django.views.generic.list y parte del módulo base .
Figura 12-5. Diagrama de clases UML para el módulo django.views.generic.list. Aquí las tres clases del
módulo base están colapsadas (vea la Figura 12-4). La clase ListView no tiene métodos ni atributos:
es una clase agregada.
Para los usuarios de Django, la clase más importante en la Figura 12-5 es ListView, que es una clase
agregada, sin ningún código (su cuerpo es solo una cadena de documentación). Cuando se crea una
instancia, ListView tiene un atributo de instancia object_list a través del cual la plantilla puede iterar
para mostrar el contenido de la página, generalmente como resultado de una consulta de base de
datos que devuelve varios objetos. Toda la funcionalidad relacionada con la generación de este iterable
de objetos proviene de MultipleObjectMixin. Ese mixin también proporciona la lógica de paginación
compleja: para mostrar parte de los resultados en una página y enlaces a más páginas.
Suponga que desea crear una vista que no represente una plantilla, pero que produzca una lista de
objetos en formato JSON. Es por eso que existe BaseListView . Proporciona un punto de extensión
fácil de usar que reúne la funcionalidad View y MultipleObjectMixin , sin la sobrecarga de la maquinaria
de plantillas.
La API de vistas basadas en clases de Django es un mejor ejemplo de herencia múltiple que Tkinter.
En particular, es fácil dar sentido a sus clases mixin: cada una tiene un propósito bien definido y todas
se nombran con el sufijo …Mixin .
Las vistas basadas en clases no fueron adoptadas universalmente por los usuarios de Django. Muchos
las usan de forma limitada, como cajas negras, pero cuando es necesario crear algo nuevo, muchas
de los codificadores de Django continúan escribiendo funciones de vista monolíticas que se encargan de
todas esas responsabilidades, en lugar de tratar de reutilizar las vistas base y los mixins.
Se necesita algo de tiempo para aprender a aprovechar las vistas basadas en clases y cómo extenderlas
para cumplir con las necesidades específicas de la aplicación, pero descubrí que valía la pena estudiarlas:
eliminan una gran cantidad de código repetitivo y facilitan la reutilización de soluciones. e incluso mejorar la
comunicación del equipo, por ejemplo, definiendo nombres estándar para las plantillas y para las variables
pasadas a los contextos de las plantillas. Las vistas basadas en clases son vistas de Django "sobre raíles".
Esto concluye nuestro recorrido por la herencia múltiple y las clases mixtas.
Lennart Regebro, un Pythonista muy experimentado y uno de los revisores técnicos de este libro, encuentra
confuso el diseño de la jerarquía de vistas mixtas de Django. Pero el también
escribió:
Los peligros y la maldad de la herencia múltiple son muy exagerados. De hecho, nunca
he tenido un gran problema con eso.
Al final, cada uno de nosotros puede tener opiniones diferentes sobre cómo usar la herencia múltiple
o si usarla en nuestros propios proyectos. Pero a menudo no tenemos elección: los marcos que
debemos usar imponen sus propias elecciones.
Otras lecturas
Cuando se usan ABC, la herencia múltiple no solo es común sino prácticamente inevitable, porque
cada uno de los ABC de colección más fundamentales (secuencia, mapeo y conjunto) extiende
múltiples ABC. El código fuente de collections.abc (Lib/ _collections_abc.py) es un buen ejemplo de
herencia múltiple con ABC, muchas de las cuales también son clases mixtas.
A pesar de los títulos de esas publicaciones, el problema no es realmente el súper incorporado, que
en Python 3 no es tan feo como en Python 2. El verdadero problema es la herencia múltiple, que es
inherentemente complicada y engañosa. Michele Simionato va más allá de criticar y, de hecho, ofrece
una solución en su obra Setting Multiple Inheritance Straight: implementa rasgos, una forma restringida
de mixins que se originó en el lenguaje Self. Simionato tiene una larga serie de publicaciones de blog
esclarecedoras sobre la herencia múltiple en Python, que incluyen Las maravillas de la herencia
cooperativa o el uso de super en Python 3; Mixins considerados nocivos, parte 1 y parte 2; y Cosas
que debe saber sobre Python Super, parte 1, parte 2 y parte 3. Las publicaciones más antiguas usan
la sintaxis de Python 2 super , pero siguen siendo relevantes.
Leí la primera edición de Object Oriented Analysis and Design, 3E de Grady Booch (Addison-Wesley,
2007), y la recomiendo mucho como un manual básico sobre el pensamiento orientado a objetos,
independientemente del lenguaje de programación. Es un libro raro que cubre la herencia múltiple sin
prejuicios.
Plataforma improvisada
La gran mayoría de los programadores escriben aplicaciones, no frameworks. Incluso aquellos que
escriben marcos probablemente dediquen mucho (si no la mayor parte) de su tiempo a escribir
aplicaciones. Cuando escribimos aplicaciones, normalmente no necesitamos codificar jerarquías de clases.
A lo sumo, escribimos clases que son subclases de ABC u otras clases proporcionadas por el marco.
Como desarrolladores de aplicaciones, es muy raro que necesitemos escribir una clase que actúe
como superclase de otra. Las clases que codificamos son casi siempre clases hoja (es decir, hojas
del árbol de herencia).
Si, mientras trabaja como desarrollador de aplicaciones, se encuentra creando jerarquías de clases de
varios niveles, es probable que se aplique una o más de las siguientes situaciones:
utilizando un marco mal diseñado. Ve a buscar una alternativa. • Está haciendo un exceso
También es posible que todo lo anterior se aplique a tu situación: te aburriste y decidiste reinventar la rueda
construyendo tu propio marco mal diseñado y sobrediseñado, lo que te obliga a codificar clase tras clase
para resolver problemas triviales.
Espero que te estés divirtiendo, o al menos que te paguen por ello.
Los tipos dict, list y str incorporados son componentes esenciales de Python, por lo que deben ser rápidos;
cualquier problema de rendimiento en ellos afectaría gravemente a casi todo lo demás. Es por eso que
CPython adoptó los atajos que hacen que sus métodos incorporados se comporten mal al no cooperar con
métodos anulados por subclases. Una posible salida a este dilema sería ofrecer dos implementaciones para
cada uno de esos tipos: una “interna”, optimizada para que la use el intérprete y una externa, fácilmente
extensible.
Pero espere, esto es lo que tenemos: UserDict, UserList y UserString no son tan rápidos como los
incorporados, pero son fácilmente extensibles. El enfoque pragmático adoptado por CPython significa que
también podemos usar, en nuestras propias aplicaciones, las implementaciones altamente optimizadas que
son difíciles de subclasificar. Lo cual tiene sentido, considerando que no es tan frecuente que necesitemos
una asignación, una lista o una cadena personalizadas, pero usamos dict, list y str todos los días. Sólo
tenemos que ser conscientes de las compensaciones involucradas.
Alan Kay acuñó el término "orientado a objetos" y Smalltalk tenía solo herencia única, aunque hay
bifurcaciones con varias formas de soporte de herencia múltiple, incluidos los dialectos modernos Squeak
y Pharo Smalltalk que admiten rasgos, una construcción de lenguaje que cumple el papel. de una clase
mixin, mientras se evitan algunos de los problemas con la herencia múltiple.
El primer lenguaje popular que implementó la herencia múltiple fue C++, y se abusó tanto de la característica
que Java, pensado como un reemplazo de C++, se diseñó sin soporte para la implementación de herencia
múltiple (es decir, sin clases mixtas). Es decir, hasta que Java 8 introdujo métodos predeterminados que
hacen que las interfaces sean muy similares a las clases abstractas utilizadas para definir interfaces en C+
+ y en Python. Excepto que las interfaces de Java no pueden tener estado, una distinción clave. Después
de Java, probablemente el lenguaje JVM más implementado es Scala, e implementa rasgos. Los rasgos de
soporte de otros idiomas son los últimos
versiones estables de PHP y Groovy, y los lenguajes en construcción Rust y Perl 6, por lo que es justo
decir que los rasgos están de moda mientras escribo esto.
Ruby ofrece una versión original de la herencia múltiple: no la admite, pero introduce mixins como una
característica del lenguaje. Una clase de Ruby puede incluir un módulo en su cuerpo, por lo que los
métodos definidos en el módulo se vuelven parte de la implementación de la clase. Esta es una forma
"pura" de mixin, sin herencia involucrada, y está claro que un mixin de Ruby no tiene influencia en el
tipo de clase donde se usa. Esto proporciona los beneficios de los mixins, al tiempo que evita muchos
de sus problemas habituales.
Dos lenguajes recientes que están ganando mucha tracción limitan severamente la herencia: Go y Julia.
Go no tiene herencia en absoluto, pero implementa interfaces de una manera que se parece a una
forma estática de tipeo de patos (consulte “Soapbox” en la página 343 para obtener más información al
respecto). Julia evita los términos "clases" y solo tiene "tipos". Julia tiene una jerarquía de tipos, pero los
subtipos no pueden heredar la estructura, solo los comportamientos y solo los tipos abstractos pueden
subtipificarse. Además, los métodos de Julia se implementan mediante el envío múltiple, una forma más
avanzada del mecanismo que vimos en “Funciones genéricas con envío único” en la página 202.
CAPÍTULO 13
Hay algunas cosas por las que me siento desgarrado, como la sobrecarga de operadores. Dejé de lado la
sobrecarga de operadores como una elección bastante personal porque había visto a mucha gente abusar
de ella en C++.1
—James Gosling
Creador de Java
La sobrecarga de operadores permite que los objetos definidos por el usuario interoperen con operadores
infijos como + y | u operadores unarios como - y ~. En términos más generales, la invocación de funciones
(()), el acceso a atributos (.) y el acceso/corte de elementos ([]) también son operadores en Python, pero
este capítulo cubre los operadores unarios e infijos.
• Cómo admite Python los operadores infijos con operandos de diferentes tipos • El uso
de tipos de pato o comprobaciones de tipos explícitos para manejar operandos de varios tipos • Cómo
un método de operador infijo debe señalar que no puede manejar un operando • El comportamiento
especial de los operadores de comparación enriquecidos (p. ej. , ==, >, <=, etc)
1. Fuente: "La familia de lenguajes C: entrevista con Dennis Ritchie, Bjarne Stroustrup y James Gosling".
371
Machine Translated by Google
Algunos operadores no se pueden sobrecargar: is, and, or, not (pero el bit a bit &, |, ~, puede).
En el Capítulo 10, ya teníamos un operador infijo en Vector: ==, compatible con el método __eq__ .
En este capítulo, mejoraremos la implementación de __eq__ para manejar mejor los operandos de
tipos que no sean Vector. Sin embargo, los operadores de comparación enriquecidos (==, !=, >, <, >=,
<=) son casos especiales en la sobrecarga de operadores, por lo que comenzaremos sobrecargando
cuatro operadores aritméticos en Vector: el unario - y +, seguido por el infijo + y *.
Operadores unarios
En Referencia del lenguaje Python, “6.5. Operaciones aritméticas unarias y bit a bit” enumera tres
operadores unarios, que se muestran aquí con sus métodos especiales asociados: - (__neg__)
+ (__pos__)
Aritmética unaria más. Por lo general , x == +x, pero hay algunos casos en los que eso no es
cierto. Consulte “Cuando x y +x no son iguales” en la página 373 si tiene curiosidad.
~ (__invertir__)
Inverso bit a bit de un entero, definido como ~x == -(x+1). Si x es 2 entonces ~x == -3.
El capítulo “Modelo de datos” de Referencia del lenguaje Python también enumera la función
integrada abs(…) como un operador unario. El método especial asociado es __abs__, como hemos
visto antes, comenzando con “Emulación de tipos numéricos” en la página 9.
Es fácil admitir los operadores unarios. Simplemente implemente el método especial apropiado, que
recibirá solo un argumento: self. Use cualquier lógica que tenga sentido en
su clase, pero apéguese a la regla fundamental de los operadores: siempre devolver un nuevo objeto.
En otras palabras, no se modifique a sí mismo, sino que cree y devuelva una nueva instancia de un tipo
adecuado.
En el caso de - y +, el resultado probablemente será una instancia de la misma clase que self; para +,
devolver una copia de sí mismo es el mejor enfoque la mayor parte del tiempo. Para abs(…), es difícil decir
el resultado debe ser un número escalar. Como para ~, cuál sería un resultado sensato si no se trata de bits
en un número entero, pero en un ORM podría tener sentido devolver la negación de una cláusula SQL
WHERE , por ejemplo.
Como prometimos antes, implementaremos varios operadores nuevos en la clase Vector del Capítulo 10. El
Ejemplo 13-1 muestra el método __abs__ que ya teníamos en el Ejemplo 10-16, y el método de operador
unitario __neg__ y __pos__ recientemente agregado.
def __abs__(self):
return math.sqrt(sum(x * x for x in self))
def __neg__(self):
return Vector(-x for x in self)
def __pos__(self):
return Vector(self)
Para calcular -v, construya un nuevo Vector con cada componente de self negado.
Para calcular +v, construya un nuevo Vector con cada componente de sí mismo.
Recuerde que las instancias de Vector son iterables, y Vector.__init__ toma un argumento iterable, por lo
que las implementaciones de __neg__ y __pos__ son cortas y sencillas.
No implementaremos __invert__, por lo que si el usuario prueba ~v en una instancia de Vector , Python
generará TypeError con un mensaje claro: "tipo de operando incorrecto para ~ unario: 'Vector'".
La siguiente barra lateral cubre una curiosidad que puede ayudarlo a ganar una apuesta sobre unario +
algún día. El siguiente tema importante es "Sobrecargar + para la suma de vectores" en la página 375.
se cambia la precisión del contexto y luego se evalúa +x . Vea el Ejemplo 13-2 para una demostración.
Ejemplo 13-2. Un cambio en la precisión del contexto aritmético puede hacer que x
difiera de +x
>>> import decimal
>>> ctx = decimal.getcontext() >>>
ctx.prec = 40 >>> un_tercio =
decimal.Decimal('1') / decimal.Decimal('3') >>> un_tercio Decimal
('0.33333333333333333333333333333333333333333') >>> un tercio ==
+un tercio Verdadero
>>> ctx.prec = 28
>>> un_tercio == +un_tercio
Falso >>> +un_tercio
Decimal('0.33333333333333333333333333333')
Baje la precisión a 28, el valor predeterminado para la aritmética decimal en Python 3.4.
El hecho es que cada aparición de la expresión +un_tercio produce una nueva instancia de Decimal
a partir del valor de un_tercio , pero usando la precisión del contexto aritmético actual.
Ejemplo 13-3. Unario + produce un nuevo Contador sin recuentos a cero o negativos
>>> ct = Contador('abracadabra')
>>> ct
Contador({'a': 5, 'r': 2, 'b': 2, 'd': 1, 'c': 1}) >>> ct['r'] = -3 >>>
ct ['d'] = 0
>>> ct
Contador ({'a': 5, 'b': 2, 'c': 1, 'd': 0, 'r': -3})
>>> +ct
Contador ({'a': 5, 'b': 2, 'c': 1})
Sumar dos vectores euclidianos da como resultado un nuevo vector en el que los componentes son
las sumas por pares de los componentes de los sumandos. Para ilustrar:
¿Qué sucede si tratamos de agregar dos instancias de Vector de diferentes longitudes? Podríamos
generar un error, pero considerando aplicaciones prácticas (como la recuperación de información), es
mejor completar el Vector más corto con ceros. Este es el resultado que queremos:
Dados estos requisitos básicos, la implementación de __add__ es breve y agradable, como se muestra
en el Ejemplo 13-4.
pares es un generador que producirá tuplas (a, b) donde a es de uno mismo y b es de otro. Si self y
other tienen longitudes diferentes, se usa fillvalue para proporcionar los valores que faltan para el
iterable más corto.
Un nuevo Vector se construye a partir de una expresión generadora que produce una suma para cada
elemento en pares.
Observe cómo __add__ devuelve una nueva instancia de Vector y no se afecta a sí mismo ni a los demás.
El Ejemplo 13-4 permite sumar Vector a un Vector2d, y Vector a una tupla o a cualquier iterable que
produzca números, como lo demuestra el Ejemplo 13-5 .
Ambas adiciones en el Ejemplo 13-5 funcionan porque __add__ usa zip_longest(...), que puede
consumir cualquier iterable, y la expresión del generador para construir el nuevo Vector simplemente
realiza a + b con los pares producidos por zip_longest(...), por lo que un iterable produce cualquier
número de artículos servirá.
Sin embargo, si intercambiamos los operandos (Ejemplo 13-6), las adiciones de tipo mixto fallan.
Para admitir operaciones que involucran objetos de diferentes tipos, Python implementa un mecanismo
de envío especial para los métodos especiales del operador infijo. Dada una expresión a + b, el intérprete
realizará estos pasos (ver también la Figura 13-1):
1. Si a tiene __add__, llame a.__add__(b) y devuelva el resultado a menos que no esté implementado.
Por lo tanto, para que las adiciones de tipo mixto del ejemplo 13-6 funcionen, necesitamos
implementar el método Vector.__radd__ , que Python invocará como respaldo si el operando
izquierdo no implementa __add__ o si lo hace pero devuelve NotImplemented para señalar que
no sabe cómo manejar el operando correcto.
No hay cambios en __add__ del Ejemplo 13-4; enumerados aquí porque __radd__ usa
eso.
A menudo, __radd__ puede ser tan simple como eso: simplemente invoque el operador adecuado,
por lo tanto, delegue a __add__ en este caso. Esto se aplica a cualquier operador conmutativo; +
es conmutativo cuando se trata de números o de nuestros vectores, pero no es conmutativo
cuando se concatenan secuencias en Python.
2. La documentación de Python usa ambos términos. El capítulo “Modelo de datos” usa “reflejado”, pero “9.1.2.2. Implementando las
operaciones aritméticas” en los documentos del módulo de números menciona los métodos “hacia adelante” y “hacia atrás”, y
creo que esta terminología es mejor, porque “hacia adelante” y “hacia atrás” claramente nombran cada una de las direcciones,
mientras que “reflejado” no lo hace. t tiene un opuesto obvio.
Los métodos del ejemplo 13-4 funcionan con objetos vectoriales o cualquier elemento iterable con
elementos numéricos, como Vector2d, una tupla de enteros o una matriz de flotantes. Pero si se le
proporciona un objeto no iterable, __add__ falla con un mensaje que no es muy útil, como en el
Ejemplo 13-8.
Se da otro mensaje inútil si un operando es iterable pero sus elementos no se pueden agregar a
los elementos flotantes en el Vector. Vea el Ejemplo 13-9.
Los problemas de los ejemplos 13-8 y 13-9 en realidad van más allá de los oscuros mensajes de
error: si un método especial de operador no puede devolver un resultado válido debido a la
incompatibilidad de tipos, debería devolver NotImplemented y no generar TypeError. Al devolver
No tImplemented, deja la puerta abierta para que el implementador del otro tipo de operando realice
la operación cuando Python intente la llamada al método inverso.
En el espíritu de tipificación pato, nos abstendremos de probar el tipo del otro operando, o el tipo
de sus elementos. Atraparemos las excepciones y devolveremos NotImplemented. Si el intérprete
aún no ha invertido los operandos, lo intentará. Si la llamada al método inverso devuelve
NotImplemented, entonces Python generará el problema TypeError con un mensaje de error
estándar como "tipos de operandos no compatibles para +: Vector y str".
En este punto, hemos sobrecargado de forma segura el operador + al escribir __add__ y __radd__. Ahora
abordaremos otro operador infijo: *.
Otro tipo de producto que involucra operandos vectoriales sería el producto punto de dos vectores, o la
multiplicación de matrices, si toma un vector como una matriz de 1 × N y el otro como una matriz de N × 1. La
práctica actual en NumPy y bibliotecas similares es no sobrecargar * con estos dos significados, sino usar *
solo para el producto escalar. Por ejemplo, en NumPy, numpy.dot() calcula el producto escalar.3
Volviendo a nuestro producto escalar, nuevamente comenzamos con los métodos __mul__ y __rmul__ más
simples que posiblemente podrían funcionar:
3. El signo @ se puede usar como un operador de producto de punto infijo a partir de Python 3.5. Más sobre esto en “The New
Operador @ Infix en Python 3.5” en la página 383.
Esos métodos funcionan, excepto cuando se proporcionan con operandos incompatibles. El argumento escalar
tiene que ser un número que, cuando se multiplica por un flotante , produzca otro flotante (porque nuestra
clase Vector usa una matriz de flotantes internamente). Por lo tanto, un número complejo no servirá, pero el
escalar puede ser un int, un bool (porque bool es una subclase de int) o incluso una fracción . Instancia de
fracción.
Podríamos usar la misma técnica de tipeo pato que usamos en el Ejemplo 13-10 y atrapar un TypeError en
__mul__, pero hay otra forma más explícita que tiene sentido en esta situación: tipeo goose. Usamos
isinstance() para verificar el tipo de escalar, pero en lugar de codificar algunos tipos concretos, lo comparamos
con los números. Real ABC, que cubre todos los tipos que necesitamos y mantiene nuestra implementación
abierta a futuros tipos numéricos que se declaran reales . o subclases virtuales de los números . Real ABC.
El ejemplo 13-11 muestra un uso práctico de la tipificación de ganso: una comprobación explícita frente a un
tipo abstracto; consulte el repositorio de código de Fluent Python_ para ver la lista completa.
clase vectorial:
código de tipo = 'd'
Si escalar es una instancia de una subclase de números. Real, cree un nuevo Vector con
valores de los componentes multiplicados.
Con el Ejemplo 13-11, podemos multiplicar Vectores por valores escalares de los habituales y no tan
tipos numéricos habituales:
Implementando + y * vimos los patrones más comunes para codificar operadores infijos.
Las técnicas que describimos para + y * son aplicables a todos los operadores enumerados en la Tabla 13-1
(los operadores en el lugar se tratarán en "Operadores de asignación aumentada" en la página
388).
Tabla 13-1. Nombres de métodos de operadores fijos (los operadores en el lugar se usan para aumentar
tarea de ted; los operadores de comparación están en la Tabla 13-2)
*
__mul__ __rmul__ __imul__ Multiplicación o repetición
División verdadera
/ __truediv__ __rtruediv__ __itruediv__
cociente de división y
módulo
bit a bit o
| __o__ __ror__ __ior__
a
pow toma un tercer argumento opcional, modulo: pow(a, b, modulo), también soportado por los métodos especiales cuando
invocado directamente (p. ej., a.__pow__(b, modulo)).
Los operadores de comparación enriquecidos son otra categoría de operadores infijos, que utilizan un
diferente conjunto de reglas. Los cubrimos en la siguiente sección principal: “Operaciones de comparación enriquecidas” .
La siguiente barra lateral opcional trata sobre el operador @ introducido en Python 3.5, no
Solo por diversión, después de compilar Python 3.5 desde la fuente, pude implementar y probar
el operador @ para el producto escalar vectorial .
clase vectorial:
# muchos métodos omitidos en la lista de libros
El manejo de los operadores de comparación enriquecidos ==, !=, >, <, >=, <= por parte de Python interÿ
preter es similar a lo que acabamos de ver, pero difiere en dos aspectos importantes:
• El mismo conjunto de métodos se utiliza en las llamadas de operador directas e inversas. Las normas
se resumen en la tabla 13-2. Por ejemplo, en el caso de ==, tanto el avance como el
las llamadas inversas invocan __eq__, solo intercambiando argumentos; y una llamada de reenvío a __gt__
es seguido por una llamada inversa a __lt__ con los argumentos intercambiados.
Tabla 13-2. Operadores de comparación enriquecidos: métodos inversos invocados cuando el método inicial
od call devuelve NotImplemented
Operador de grupo Infijo Llamada de método directo Llamada de método inverso Repliegue
Nuevo comportamiento
en Python 3 El paso alternativo para todos los operadores de
comparación cambió desde Python 2. Para __ne__, Python 3 ahora
devuelve el resultado negativo de __eq__. Para los operadores de
comparación de pedidos, Python 3 genera TypeError con un mensaje
como 'tipos no ordenados: int() < tuple()'. En Python 2, esas
comparaciones produjeron resultados extraños teniendo en cuenta los
tipos de objetos y las ID de alguna manera arbitraria. Sin embargo,
realmente no tiene sentido comparar un int con una tupla, por ejemplo,
por lo que generar TypeError en tales casos es una mejora real en el lenguaje.
Dadas estas reglas, revisemos y mejoremos el comportamiento del método Vector.__eq__ , que se
codificó de la siguiente manera en vector_v5.py (Ejemplo 10-16):
clase Vector:
# muchas líneas omitidas
Verdadero
Un Vector también se considera igual a una tupla o cualquier iterable con elementos
numéricos de igual valor.
El último de los resultados del ejemplo 13-12 probablemente no sea deseable. Realmente no tengo
una regla estricta sobre esto; depende del contexto de la aplicación. Pero el Zen de Python dice:
La excesiva liberalidad en la evaluación de los operandos puede conducir a resultados sorprendentes y los
programadores odian las sorpresas.
Tomando una pista de Python mismo, podemos ver que [1,2] == (1, 2) es Falso. Por lo tanto, seamos conservadores
y hagamos una verificación de tipos. Si el segundo operando es una instancia de Vector (o una instancia de una
subclase de Vector ), use la misma lógica que el __eq__ actual. De lo contrario, devuelva NotImplemented y deje que
Python maneje eso. Vea el Ejemplo 13-13.
Si el otro operando es una instancia de Vector (o de una subclase de Vector ), realice la comparación como
antes.
Si ejecuta las pruebas del Ejemplo 13-12 con el nuevo Vector.__eq__ del Ejemplo 13-13, lo que obtiene ahora se
muestra en el Ejemplo 13-14.
Ejemplo 13-14. Mismas comparaciones que en el Ejemplo 13-12: último resultado cambiado
Resultado diferente; esto es lo que queríamos. Pero ¿por qué funciona? sigue leyendo…
Entre los tres resultados del Ejemplo 13-14, el primero no es una noticia, pero los dos últimos fueron causados por
__eq__ que devolvió NotImplemented en el Ejemplo 13-13. Esto es lo que sucede en el ejemplo con un Vector y un
Vector2d, paso a paso:
En cuanto a la comparación entre Vector y tupla en el Ejemplo 13-14, los pasos reales
son:
4. tuple.__eq__(t3, va) no tiene idea de qué es un Vector , por lo que devuelve NotImplemented.
¿Qué tal ! =? No necesitamos implementarlo porque el comportamiento alternativo del __ne__ heredado
del objeto nos conviene: cuando __eq__ está definido y no devuelve NotImplemented, __ne__ devuelve
ese resultado negado.
En otras palabras, dados los mismos objetos que usamos en el Ejemplo 13-14, los resultados para != son
coherente:
>>> va != vb
Falso
>>> vc != v2d
Falso >>> va !
= (1, 2, 3)
Verdadero
El __ne__ heredado de object funciona como el siguiente código, excepto que el original está escrito en
C:4
más:
devuelve no eq_result
Error de documentación de
Python 3 Mientras escribo esto, la rica documentación del método de
comparación afirma: “La verdad de x==y no implica que x!=y sea falso.
En consecuencia, al definir __eq__(), también se debe definir __ne__()
para que los operadores se comporten como se espera”. Eso era cierto
para Python 2, pero en Python 3 no es un buen consejo, porque una
implementación útil predeterminada de __ne__ se hereda de la clase
de objeto y rara vez es necesario anularla. El nuevo comportamiento
está documentado en What's New in Python 3.0 de Guido, en la sección
"Operadores y métodos especiales". El error de documentación se
registra como problema 4395.
Después de cubrir los aspectos esenciales de la sobrecarga de operadores infijos, pasemos a una clase
diferente de operadores: los operadores de asignación aumentada.
Ejemplo 13-15. La asignación aumentada funciona con objetivos inmutables al crear nuevas instancias y
volver a vincular
>>> v1 = Vector([1, 2, 3]) >>>
v1_alias = v1 # >>> id(v1) #
4302860128
>>> v1_alias #
Vector([1.0, 2.0, 3.0]) >>> v1
*= 11 # >>> v1 # Vector([55.0,
77.0, 99.0]) >>> id(v1)
4302858336
Cree un alias para que podamos inspeccionar el objeto Vector([1, 2, 3]) más tarde.
Recuerde la ID del Vector inicial vinculado a v1.
El resultado esperado…
…pero se creó un nuevo Vector .
Si una clase no implementa los operadores en el lugar enumerados en la Tabla 13-1, los operadores de
asignación aumentada son simplemente azúcar sintáctico: a += b se evalúa exactamente como a = a +
b. Ese es el comportamiento esperado para los tipos inmutables, y si tiene __add__ entonces +=
funcionará sin código adicional.
Sin embargo, si implementa un método de operador en el lugar como __iadd__, se llama a ese método
para calcular el resultado de a += b. Como su nombre lo dice, se espera que esos operadores cambien
el operando de la izquierda en su lugar y no creen un nuevo objeto como resultado.
Para mostrar el código de un operador en el lugar, extenderemos la clase BingoCage del Ejemplo 11-12
para implementar __add__ y __iadd__.
Cree una instancia de globo con cinco elementos (cada una de las vocales).
Haga estallar uno de los elementos y verifique que sea una de las vocales.
Cree una tercera instancia agregando las dos anteriores. Esta instancia tiene siete elementos.
Intentar agregar un AddableBingoCage a una lista falla con TypeError. Ese mensaje de error es producido por
el intérprete de Python cuando nuestro método __add__ devuelve NotImplemented.
Debido a que AddableBingoCage es mutable, el Ejemplo 13-17 muestra cómo funcionará cuando implementemos
__iadd__.
Ejemplo 13-17. Un AddableBingoCage existente se puede cargar con += (continuando con el Ejemplo 13-16)
Cree un alias para que podamos verificar la identidad del objeto más tarde. globo tiene
Una instancia de AddableBingoCage puede recibir elementos de otra instancia de la misma clase.
Intentar agregar un elemento no iterable a AddableBingoCage falla con un mensaje de error adecuado.
Tenga en cuenta que el operador += es más liberal que + con respecto al segundo operando. Con +, queremos que
ambos operandos sean del mismo tipo (AddableBingoCage, en este caso), porque si aceptáramos diferentes tipos esto
podría causar confusión en cuanto al tipo de la
resultado. Con el +=, la situación es más clara: el objeto de la izquierda se actualiza en su lugar, por lo que
no hay duda sobre el tipo de resultado.
Ahora que tenemos claro el comportamiento deseado para AddableBingoCage, podemos ver su
implementación en el ejemplo 13-18.
importar itertools
clase AddableBingoCage(BingoCage):
volver No implementado
PEP 8: Guía de estilo para el código de Python recomienda codificar las importaciones desde el
biblioteca estándar por encima de las importaciones de sus propios módulos.
Si eso falla, genere una excepción que explique lo que debe hacer el usuario. Cuando sea
posible, los mensajes de error deben guiar explícitamente al usuario hacia la solución.
Muy importante: los métodos especiales de asignación aumentada deben devolver self.
Podemos resumir toda la idea de los operadores en el lugar contrastando las declaraciones de
devolución que producen resultados en __add__ y __iadd__ en el Ejemplo 13-18:
__add__
El resultado se produce llamando al constructor AddableBingoCage para construir una nueva
instancia.
__iadd__
El resultado se produce devolviendo self, después de haber sido modificado.
Para concluir este ejemplo, una observación final sobre el Ejemplo 13-18: por diseño, no se codificó
__radd__ en AddableBingoCage, porque no es necesario. El método directo __add__ solo se
ocupará de los operandos de la derecha del mismo tipo, por lo que si Python está tratando de
calcular a + b donde a es un AddableBingoCage yb no lo es, devolvemos NoImplementado , tal vez
la clase de b pueda hacer que funcione. Pero si la expresión es b + a y b no es AddableBingoCage,
y devuelve NotImplemented, entonces es mejor dejar que Python se dé por vencido y genere
TypeError porque no podemos manejar b.
5. La función integrada iter se tratará en el próximo capítulo. Aquí podría haber usado tupla(otro), y
funcionaría, pero a costa de construir una nueva tupla cuando todo lo que necesita el método .load(…) es
iterar sobre su argumento.
Nos pusimos manos a la obra con los operadores unarios, implementando __neg__ y __pos__. Luego vinieron los
operadores infijos, comenzando con +, compatibles con el método __add__ . Vimos que se supone que los operadores
unarios e infijos producen resultados al crear nuevos objetos y nunca deben cambiar sus operandos. Para admitir
operaciones con otros tipos, devolvemos el valor especial NotImplemented , no una excepción, lo que permite que el
intérprete intente nuevamente intercambiando los operandos y llamando al método especial inverso para ese operador
(por ejemplo, __radd__). El algoritmo que usa Python para manejar operadores infijos se resume en el diagrama de flujo
de la figura 13-1.
Mezclar tipos de operandos significa que necesitamos detectar cuándo obtenemos un operando que no podemos manejar.
En este capítulo, hicimos esto de dos maneras: en la forma de tipeo pato, simplemente continuamos y probamos la
operación, detectando una excepción TypeError si ocurría; luego, en __mul__, lo hicimos con una prueba isinstance
explícita . Estos enfoques tienen pros y contras: la tipificación pato es más flexible, pero la verificación explícita de tipos es
más predecible. Cuando usamos isinstance, tuvimos cuidado de evitar probar con una clase concreta, pero usamos los
números. Real ABC: isinstance (escalar, números. Real). Este es un buen compromiso entre flexibilidad y seguridad,
porque los tipos definidos por el usuario existentes o futuros se pueden declarar como subclases reales o virtuales de un
ABC, como vimos en el Capítulo 11.
El siguiente tema que cubrimos fue el de los operadores de comparación enriquecidos. Implementamos == con __eq__ y
descubrimos que Python proporciona una práctica implementación de != en el __ne__ heredado de la clase base del
objeto . La forma en que Python evalúa estos operadores junto con >, <, >= y <= es ligeramente diferente, con una lógica
diferente para elegir el método inverso y un manejo alternativo especial para == y !=, que nunca genera errores porque
Python compara los ID de objeto como último recurso.
En la última sección, nos enfocamos en los operadores de asignación aumentada. Vimos que Python los maneja por
defecto como una combinación de un operador simple seguido de una asignación, es decir: a += b se evalúa exactamente
como a = a + b. Eso siempre crea un nuevo objeto, por lo que funciona para tipos mutables o inmutables. Para objetos
mutables, podemos implementar métodos especiales en el lugar como __iadd__ para +=, y alterar el valor del operando
de la izquierda.
Para mostrar esto en el trabajo, dejamos atrás la clase Vector inmutable y trabajamos en la implementación de una
subclase BingoCage para admitir += para agregar elementos al grupo aleatorio, similar a la forma en que la lista integrada
admite += como acceso directo para el método list.extend() . Mientras hacíamos esto, discutimos cómo + tiende a ser más
estricto que += con respecto a los tipos que acepta. Para los tipos de secuencia, + generalmente requiere que ambos
operandos sean del mismo tipo, mientras que += a menudo acepta cualquier iterable como el operando de la derecha.
Otras lecturas
La sobrecarga de operadores es un área de la programación de Python donde las pruebas de instancia son comunes. En
general, las bibliotecas deben aprovechar la tipificación dinámica, para ser más flexibles, evitando pruebas de tipo
explícitas y simplemente probando operaciones y luego manejando las excepciones.
ciones, abriendo la puerta para trabajar con objetos independientemente de su tipo, siempre y cuando
soporten las operaciones necesarias. Pero los ABC de Python permiten una forma más estricta de digitación
de pato, denominada "tipificación de ganso" por Alex Martelli, que a menudo es útil cuando se escribe código
que sobrecarga a los operadores. Entonces, si se saltó el Capítulo 11, asegúrese de leerlo.
La referencia principal para los métodos especiales del operador es el capítulo "Modelo de datos". Es la
fuente canónica, pero en este momento está plagado de ese error flagrante mencionado en el error de
documentación de Python 3, que aconseja "al definir __eq__(), también se debe definir __ne__ ()". En
realidad, el __ne__ heredado de la clase de objeto en Python 3 cubre la gran mayoría de las necesidades,
por lo que implementar __ne__ rara vez es necesario en la práctica. Otra lectura relevante en la documentación
de Python es “9.1.2.2. Implementando las operaciones aritméticas” en el módulo de números de The Python
Standard Library.
Una técnica relacionada son las funciones genéricas, compatibles con el decorador @singledispatch en
Python 3 ("Funciones genéricas con envío único" en la página 202). En Python Cookbook, 3E (O'Reilly), de
David Beazley y Brian K. Jones, “Receta 9.20. Implementación de envíos múltiples con anotaciones de
funciones” utiliza algo de metaprogramación avanzada, que involucra una metaclase, para implementar el
envío basado en tipos con anotaciones de funciones. La segunda edición del Python Cookbook de Martelli,
Ravenscroft y Ascher tiene una receta interesante (2.13, de Erik Max Francis) que muestra cómo sobrecargar
el operador << para emular la sintaxis iostream de C++ en Python. Ambos libros tienen otros ejemplos con
sobrecarga de operadores, solo elegí dos recetas notables.
Si tiene curiosidad acerca del envío de métodos de operadores en lenguajes con escritura dinámica, dos
lecturas seminales son "Una técnica simple para manejar múltiples polimorfismos" de Dan Ingalls (miembro
del equipo original de Smalltalk) y "Aritmética y envío doble en Smalltalk-80" de Kurt J. Hebel y Ralph Johnson
(Johnson se hizo famoso como uno de los autores del libro original Design Patterns ). Ambos documentos
brindan una visión profunda del poder del polimorfismo en lenguajes con escritura dinámica, como Smalltalk,
Python y Ruby. Python no usa el envío doble para manejar operadores como se describe en esos artículos.
El algoritmo de Python que usa operadores directos e inversos es más fácil de admitir para las clases
definidas por el usuario que el envío doble, pero requiere un manejo especial por parte del intérprete. Por el
contrario, el despacho doble clásico es una técnica general que puede usar en Python o cualquier lenguaje
OO más allá del contexto específico de los operadores infijos y, de hecho, Ingalls, Hebel y Johnson usan
ejemplos muy diferentes para describirlo.
El artículo “The C Family of Languages: Interview with Dennis Ritchie, Bjarne Stroustrup, and James Gosling”
del que cité el epígrafe de este capítulo y otros dos fragmentos en “Soapbox” en la página 395, apareció en
Java Report, 5(7), julio de 2000 y C++
Report, 12(7), julio/agosto de 2000. Es una lectura increíble si te gusta el diseño de lenguajes
de programación.
Plataforma improvisada
James Gosling, citado al comienzo de este capítulo, tomó la decisión consciente de dejar fuera la
sobrecarga de operadores cuando diseñó Java. En esa misma entrevista (“The C Family of Languages:
Interview with Dennis Ritchie, Bjarne Stroustrup, and James Gosling”) dice:
Guido van Rossum escogió el camino intermedio para admitir la sobrecarga de operadores: no dejó la
puerta abierta para que los usuarios crearan nuevos operadores arbitrarios como <=> o :-), lo que evita
una Torre de Babel de operadores personalizados y permite que el analizador de Python ser simple.
Python tampoco le permite sobrecargar los operadores de los tipos integrados, otra limitación que
promueve la legibilidad y el rendimiento predecible.
Luego hay una comunidad de alrededor del 10 por ciento que en realidad ha utilizado la sobrecarga
de operadores de manera adecuada y que realmente se preocupa por ello, y para quienes es
realmente importante; se trata casi exclusivamente de personas que realizan trabajos numéricos,
donde la notación es muy importante para apelar a la intuición de las personas, porque llegan con
una intuición sobre lo que significa el + y la capacidad de decir "a + b" donde a y b son números
complejos o matrices o algo realmente tiene sentido.
El lado de la notación del problema no puede subestimarse. He aquí un ejemplo ilustrativo del ámbito
de las finanzas. En Python, puedes calcular el interés compuesto usando una fórmula escrita así:
Esa misma notación funciona independientemente de los tipos numéricos involucrados. Por lo tanto, si
está haciendo un trabajo financiero serio, puede asegurarse de que los períodos sean un número
entero, mientras que la tasa, el interés y el capital son números exactos, instancias de la clase Python
decimal.Deci mal , y esa fórmula funcionará exactamente como está escrita.
Pero en Java, si cambia de float a BigDecimal para obtener una precisión arbitraria, ya no puede usar
operadores infijos, porque solo funcionan con los tipos primitivos. Esta es la misma fórmula codificada
para trabajar con números BigDecimal en Java:
Está claro que los operadores infijos hacen que las fórmulas sean más legibles, al menos para la mayoría de nosotros.6
Y la sobrecarga de operadores es necesaria para admitir tipos no primitivos con notación de operadores infijos. Tener
la sobrecarga de operadores en un lenguaje fácil de usar y de alto nivel fue probablemente una razón clave de la
sorprendente penetración de Python en la computación científica en los últimos años.
Por supuesto, hay beneficios al no permitir la sobrecarga de operadores en un idioma. Podría decirse que es una
decisión acertada para los lenguajes de sistemas de nivel inferior donde el rendimiento y la seguridad son primordiales.
El lenguaje Go, mucho más nuevo, siguió el ejemplo de Java en este sentido y no admite la sobrecarga de operadores.
Pero los operadores sobrecargados, cuando se usan con sensatez, hacen que el código sea más fácil de leer y escribir.
Es una gran característica para tener en un lenguaje moderno de alto nivel.
Si observa detenidamente el rastreo en el Ejemplo 13-9, verá evidencia de la evaluación perezosa de las expresiones
del generador. El ejemplo 13-19 es el mismo rastreo, ahora con llamadas.
La llamada Vector obtiene una expresión generadora como argumento de sus componentes. No hay problema
en esta etapa.
Los componentes genex se pasan al constructor de arreglos. Dentro del constructor de arreglos, Python
intenta iterar sobre el genexp, causando la evaluación del primer elemento a + b. Ahí es cuando ocurre el
TypeError.
Esto muestra cómo se evalúa la expresión del generador en el último momento posible y no dónde se define en el
código fuente.
6. Mi amigo Mario Domenech Goulart, un desarrollador central del compilador CHICKEN Scheme, probablemente
no estoy de acuerdo con esto.
El capítulo 14 cubrirá las expresiones generadoras en detalle, pero no quería dejar pasar
desapercibida esta demostración accidental de su naturaleza perezosa.
PARTE V
Flujo de control
Machine Translated by Google
Machine Translated by Google
CAPÍTULO 14
Cuando veo patrones en mis programas, lo considero una señal de problemas. La forma de un programa
debe reflejar solo el problema que necesita resolver. Cualquier otra regularidad en el código es una señal,
al menos para mí, de que estoy usando abstracciones que no son lo suficientemente poderosas; a
menudo, estoy generando a mano las expansiones de alguna macro que necesito escribir.1
—Paul Graham
Lisp hacker y capitalista de riesgo
La iteración es fundamental para el procesamiento de datos. Y al escanear conjuntos de datos que no caben en la
memoria, necesitamos una forma de recuperar los elementos de forma perezosa, es decir, uno a la vez y bajo demanda.
De esto se trata el patrón Iterator. Este capítulo muestra cómo el patrón Iterator está integrado en el lenguaje Python
para que nunca necesite implementarlo a mano.
Python no tiene macros como Lisp (el lenguaje favorito de Paul Graham), por lo que abstraer el patrón Iterator
requería cambiar el lenguaje: la palabra clave yield se agregó en Python 2.2 (2001).2 La palabra clave yield permite
la construcción de generadores, que funcionan como iteradores.
2. Los usuarios de Python 2.2 podrían usar yield con la directiva de los generadores de importación __future__; yield estuvo disponible de forma
predeterminada en Python 2.3.
401
Machine Translated by Google
Python 3 usa generadores en muchos lugares. Incluso el rango () incorporado ahora devuelve un objeto
similar a un generador en lugar de listas completas como antes. Si debe crear una lista a partir de un rango,
debe ser explícito (p. ej., list(range(100))).
Cada colección en Python es iterable, y los iteradores se usan internamente para admitir:
• Bucles for •
llamadas a funciones
• Cómo se usa internamente la función incorporada iter(…) para manejar objetos iterables • Cómo
implementar el patrón clásico de Iterator en Python • Cómo funciona en detalle una función
generadora, con descripciones línea por línea • Cómo puede el Iterator clásico ser reemplazado por
de la nueva declaración yield from para combinar generadores • Un estudio de caso: uso de
funciones de generador en una utilidad de conversión de base de datos diseñada para trabajar con
grandes conjuntos de datos
• Por qué los generadores y las corrutinas se parecen pero en realidad son muy diferentes y deberían
no ser mezclado
Comenzaremos a estudiar cómo la función iter(...) hace que las secuencias sean iterables.
El ejemplo 14-1 muestra una clase Oración que extrae palabras de un texto por índice.
importar re
importar reprlib
RE_PALABRA = re.compilar('\w+')
oración de clase :
def __len__(auto):
return len(auto.palabras)
re.findall devuelve una lista con todas las coincidencias no superpuestas del regular
expresión, como una lista de cadenas.
venir
3. Primero usamos reprlib en “Vector Take #1: Vector2d Compatible” en la página 276.
la
morsa
dijo
>>> lista(s) #
['El', 'tiempo', 'ha', 'venir', 'el', 'morsa', 'dijo']
Al ser iterables, los objetos Sentence se pueden usar como entrada para crear listas y otros tipos
iterables.
En las páginas siguientes, desarrollaremos otras clases de oraciones que pasan las pruebas del ejemplo
14-2. Sin embargo, la implementación en el Ejemplo 14-1 es diferente de todas las demás porque también es
una secuencia, por lo que puede obtener palabras por índice:
>>> s[0]
'El'
>>> s[5]
'Morsa'
>>> s[-1]
'dijo'
Todo programador de Python sabe que las secuencias son iterables. Ahora veremos precisamente por qué.
Por qué las secuencias son iterables: la función iter Cada vez que
2. Si no se implementa __iter__ , pero se implementa __getitem__ , Python crea un iterador que intenta
recuperar los elementos en orden, comenzando desde el índice 0 (cero).
3. Si eso falla, Python lanza TypeError, generalmente diciendo "El objeto C no es iterable", donde
C es la clase del objeto de destino.
Es por eso que cualquier secuencia de Python es iterable: todas implementan __getitem__. De hecho, las
secuencias estándar también implementan __iter__, y la suya también debería, porque el manejo especial de
__getitem__ existe por razones de compatibilidad con versiones anteriores y puede desaparecer en el futuro
(aunque no está obsoleto mientras escribo esto).
Como se mencionó en "Python Digs Sequences" en la página 310, esta es una forma extrema de tipificación
pato: un objeto se considera iterable no solo cuando implementa el método especial
__iter__, pero también cuando implementa __getitem__, siempre que __getitem__ acepte
claves int a partir de 0.
>>> f = Foo()
>>> esinstancia(f, abc.Iterable)
Verdadero
Sin embargo, tenga en cuenta que nuestra clase Sentence inicial no pasa la prueba
issubclass(Sentence, abc.Iterable) , aunque es iterable en la práctica.
son siempre iterables; al igual que los objetos que implementan un método __getitem__ que toma
Índices basados en 0.
Aquí hay un bucle for simple iterando sobre una cadena. El str 'ABC' es iterable aquí. Tú
no lo veo, pero hay un iterador detrás de la cortina:
>>> s = 'ABC'
>>> para char en s:
... imprimir (char)
...
A
B
C
Si no hubiera declaración for y tuviéramos que emular la maquinaria for a mano con
un bucle while , esto es lo que tendríamos que escribir:
>>> s = 'ABC'
>>> it = iter(es) # >>>
while True:
... probar:
... print(next(it)) #
... excepto StopIteration: # del
... it # break #
...
...
A
B
C
StopIteration indica que el iterador está agotado. Esta excepción se maneja entre
finalmente en bucles for y otros contextos de iteración como listas de comprensión, desempaquetado de tuplas,
etc.
__Siguiente__
__iter__
Devuelve uno mismo; esto permite usar iteradores donde se espera un iterable, por
ejemplo, en un bucle for .
Esto se formaliza en collections.abc.Iterator ABC, que define el método abstracto __next__ , y las
subclases Iterable, donde se define el método abstracto __iter__ . Consulte la Figura 14-1.
Figura 14-1. El ABC de Iterable e Iterator. Los métodos en cursiva son abstractos. Un Iterable.iter
concreto debería devolver una nueva instancia de Iterator. Un iterador concreto debe implementarse a
continuación. El método Iterator.iter simplemente devuelve la instancia en sí.
El iterador ABC implementa __iter__ haciendo return self. Esto permite que se use un iterador donde
sea que se requiera un iterable. El código fuente de abc.Iterator está en el Ejemplo 14-3.
__ranuras__ = ()
@abstractmethod
def __next__(self):
'Devuelve el siguiente elemento del iterador. Cuando se agote, aumente StopIteration ' aumente
StopIteration
def __iter__(auto):
retornar auto
@classmethod
def __subclasshook__(cls, C):
si cls es iterador:
if (any("__next__" in B.__dict__ for B in C.__mro__) and any("__iter__"
in B.__dict__ for B in C.__mro__)):
volver verdadero
volver No implementado
El código fuente del módulo Lib/ types.py en Python 3.4 tiene un comentario que dice:
# Los iteradores en Python no son una cuestión de tipo sino de protocolo. Una gran # y
cambiante cantidad de tipos integrados implementan *algún* tipo de # iterador. ¡No
marques el tipo! Use hasattr para verificar los atributos # "__iter__" y "__next__" en su
lugar.
De hecho, eso es exactamente lo que hace el método __subclasshook__ del abc.Iterator ABC (vea
el Ejemplo 14-3).
Volviendo a nuestra clase Oración del Ejemplo 14-1, puede ver claramente cómo el iterador es
construido por iter(...) y consumido por next(...) usando la consola de Python:
'Pepper'
>>> next(it) #
Traceback (última llamada más reciente):
...
StopIteration
>>> list(it) # [] >>>
list(iter(s3)) #
No hay más palabras, por lo que el iterador genera una excepción StopIteration .
Una vez agotado, un iterador se vuelve inútil.
Debido a que los únicos métodos requeridos de un iterador son __next__ y __iter__, no hay forma de
verificar si quedan elementos, aparte de llamar a next() y capturar StopInteration. Además, no es posible
"restablecer" un iterador. Si necesita comenzar de nuevo, debe llamar a iter (...) en el iterable que creó el
iterador en primer lugar. Llamar a iter(...) en el iterador en sí no ayudará, porque, como se mencionó, Itera
tor.__iter__ se implementa devolviendo self, por lo que esto no restablecerá un iterador agotado.
ador
Cualquier objeto que implemente el método sin argumentos __next__ que devuelve el siguiente
elemento de una serie o genera StopIteration cuando no hay más elementos. Los iteradores de
Python también implementan el método __iter__ por lo que también son iterables .
Esta primera versión de Sentence era iterable gracias al tratamiento especial que el iter(…) incorporado
da a las secuencias. Ahora implementaremos el protocolo iterable estándar.
El ejemplo 14-4 muestra una implementación de una oración que es iterable porque implementa el método
especial __iter__ , que construye y devuelve un SentenceIterator.
Así es como se describe el patrón de diseño Iterator en el libro Design Patterns original.
Lo estamos haciendo de esta manera aquí solo para dejar en claro la distinción crucial entre un iterable y
un iterador y cómo están conectados.
RE_PALABRA = re.compilar('\w+')
oración de clase :
Devuelve la palabra.
Implementar self.__iter__.
El código del ejemplo 14-4 pasa las pruebas del ejemplo 14-2.
Tenga en cuenta que implementar __iter__ en SentenceIterator no es realmente necesario para que este
ejemplo funcione, pero es lo correcto: se supone que los iteradores implementan tanto __next__ como __iter__,
y al hacerlo, nuestro iterador pasa issubclass(SentenceInterator, abc. Iterador) prueba. Si hubiéramos
subclasificado SentenceIterator de abc.Iterator, heredaríamos el método concreto abc.Iterator.__iter__ .
Eso es mucho trabajo (para nosotros los perezosos programadores de Python, de todos modos). Tenga en
cuenta cómo la mayoría del código en SentenceIterator se ocupa de administrar el estado interno del iterador.
Pronto veremos cómo hacerlo más corto. Pero primero, un breve desvío para abordar un atajo de implementación
que puede ser tentador, pero que simplemente está mal.
común de errores en la construcción de iterables e iteradores es confundir los dos. Para ser claros: los iterables
tienen un método __iter__ que instancia un nuevo iterador cada vez.
Los iteradores implementan un método __next__ que devuelve elementos individuales y un método __iter__ que
se devuelve a sí mismo.
Por lo tanto, los iteradores también son iterables, pero los iterables no son iteradores.
Puede ser tentador implementar __next__ además de __iter__ en la clase Sentence , haciendo que cada
instancia de Sentence al mismo tiempo sea iterable e iterador sobre sí mismo. Pero esta es una idea terrible.
También es un antipatrón común, según Alex Martelli, que tiene mucha experiencia con las revisiones de código
de Python.
admitir recorridos múltiples de objetos agregados. • proporcionar una interfaz uniforme para
Para "soportar recorridos múltiples", debe ser posible obtener múltiples iteradores independientes de la misma
instancia iterable, y cada iterador debe mantener su propio estado interno, por lo que una implementación
adecuada del patrón requiere cada llamada a iter(my_iterable) para crear un nuevo, independiente, iterador. Por
eso necesitamos la clase SentenceItera tor en este ejemplo.
4. Gama et. al., Patrones de diseño: Elementos de software orientado a objetos reutilizable, pág. 259.
Ahora que el patrón clásico de Iterator está correctamente demostrado, podemos dejarlo ir. La
siguiente sección presenta una implementación más idiomática de Sentence.
RE_PALABRA = re.compilar('\w+')
oración de clase :
devolver
# ¡hecho!
separada!
Aquí nuevamente tenemos una implementación diferente de Oración que pasa las pruebas del
Ejemplo 14-2.
Cualquier función de Python que tenga la palabra clave yield en su cuerpo es una función generadora:
una función que, cuando se llama, devuelve un objeto generador. En otras palabras, una función
generadora es una fábrica generadora.
La única sintaxis que distingue una función simple de una función generadora es
el hecho de que esta última tiene una palabra clave yield en algún lugar de su
cuerpo. Algunos argumentaron que debería usarse una nueva palabra clave
como gen para funciones de generador en lugar de def, pero Guido no estuvo de
6
acuerdo. Sus argumentos se encuentran en PEP 255 — Generadores simples.
Aquí está la función más simple útil para demostrar el comportamiento de un generador:7
5. Al revisar este código, Alex Martelli sugirió que el cuerpo de este método podría ser simplemente return
iter(self.words). Tiene razón, por supuesto: el resultado de llamar a __iter__ también sería un iterador, como
debería ser. Sin embargo, aquí utilicé un ciclo for con rendimiento para presentar la sintaxis de una función
generadora, que se tratará en detalle en la siguiente sección.
6. A veces agrego un prefijo o sufijo gen cuando nombro las funciones del generador, pero esto no es una práctica
común. Y no puede hacer eso si está implementando un iterable, por supuesto: el método especial necesario
debe llamarse __iter__.
>>> siguiente(g)
3 >>>
siguiente(g) #
Rastreo (llamadas recientes más última):
...
Detener iteración
Cualquier función de Python que contenga la palabra clave yield es una función generadora.
Por lo general, el cuerpo de una función generadora tiene bucle, pero no necesariamente; aquí
solo repito yield tres veces.
Los generadores son iteradores que producen los valores de las expresiones pasadas a yield.
Debido a que g es un iterador, llamar a next(g) obtiene el siguiente elemento producido por yield.
Una función generadora construye un objeto generador que envuelve el cuerpo de la función.
Cuando invocamos next(...) en el objeto generador, la ejecución avanza al siguiente rendimiento en el
cuerpo de la función, y la llamada next(...) se evalúa al valor producido cuando se suspende el cuerpo
de la función. Finalmente, cuando el cuerpo de la función regresa, el objeto generador adjunto genera
StopIteration, de acuerdo con el protocolo Iterator .
El ejemplo 14-6 hace que la interacción entre un bucle for y el cuerpo de la función
más explícito.
Ejemplo 14-6. Una función generadora que imprime mensajes cuando se ejecuta
>>> def gen_AB(): #
... imprimir('comenzar')
... rendimiento #
... 'A' imprimir ('continuar')
... rendimiento 'B' #
... imprimir ('fin.') #
...
>>> para c en gen_AB(): #
... print('-->', c) #
...
comienzo
--> Un
continuar
--> Fin B.
>>>
8. Antes de Python 3.3, era un error proporcionar un valor con la declaración de devolución en una función de generador.
Ahora eso es legal, pero la devolución aún provoca que se genere una excepción StopIteration. La persona que llama puede recuperar
el valor de retorno del objeto de excepción. Sin embargo, esto solo es relevante cuando se utiliza una función de generador.
como una corrutina, como veremos en “Devolución de un valor desde una corrutina” en la página 475.
El bloque de bucle imprime --> y el valor devuelto por next(g). Pero esta salida solo se verá
después de la salida de las llamadas de impresión dentro de la función del generador.
> A.
La iteración continúa con una segunda llamada next(g), avanzando el cuerpo de la función
del generador de yield 'A' a yield 'B'. El texto continuar se genera debido a la segunda
impresión en el cuerpo de la función del generador. yield 'B' produce el valor B consumido
por el bucle for , que se asigna a la variable de bucle c , por lo que el bucle imprime --> B.
La iteración continúa con una tercera llamada next(it), avanzando hasta el final del cuerpo
de la función. El final del texto. aparece en la salida debido a la tercera impresión en el
cuerpo de la función del generador.
Cuando el cuerpo de la función del generador se ejecuta hasta el final, el objeto del generador
genera StopIteration. La maquinaria del bucle for detecta esa excepción y el bucle termina
limpiamente.
Ahora, con suerte, está claro cómo funciona Sentence.__iter__ en el ejemplo 14-5 : __iter__ es una
función generadora que, cuando se llama, crea un objeto generador que implementa la interfaz del
iterador, por lo que la clase SentenceIterator ya no es necesaria.
Esta segunda versión de Sentence es mucho más corta que la primera, pero no es tan perezosa
como podría ser. Hoy en día, la pereza se considera un buen rasgo, al menos en lenguajes de
programación y API. Una implementación perezosa pospone la producción de valores hasta el último
momento posible. Esto ahorra memoria y también puede evitar el procesamiento inútil.
La interfaz de Iterator está diseñada para ser perezosa: next(my_iterator) produce un elemento a la
vez. Lo contrario de perezoso es ansioso: evaluación perezosa y evaluación entusiasta son términos
técnicos reales en la teoría del lenguaje de programación.
Nuestras implementaciones de Sentence hasta ahora no han sido perezosas porque __init__
construye con entusiasmo una lista de todas las palabras en el texto, vinculándolas al atributo
self.words . Esto implicará procesar todo el texto, y la lista puede usar tanta memoria como el texto mismo.
(probablemente más; depende de cuántos caracteres no verbales haya en el texto). La mayor parte de este
trabajo será en vano si el usuario solo repite las dos primeras palabras.
Cada vez que usa Python 3 y comienza a preguntarse "¿Hay una manera perezosa de hacer esto?", A menudo
la respuesta es "Sí".
La función re.finditer es una versión perezosa de re.findall que, en lugar de una lista, devuelve un generador que
produce instancias de re.MatchObject bajo demanda. Si hay muchas coincidencias, re.finditer ahorra mucha
memoria. Usándolo, nuestra tercera versión de Sentence ahora es perezosa: solo produce la siguiente palabra
cuando se necesita. El código está en el Ejemplo 14-7.
Ejemplo 14-7. sentencia_gen2.py: Sentencia implementada usando una función generadora llamando a la función
generadora re.finditer
importar
volver a importar reprlib
RE_PALABRA = re.compilar('\w+')
oración de clase :
finditer construye un iterador sobre las coincidencias de RE_WORD en self.text, produciendo instancias
de MatchObject .
Las funciones de generador son un atajo increíble, pero el código se puede acortar aún más con una expresión
de generador.
Una expresión de generador puede entenderse como una versión perezosa de una lista por comprensión: no
construye una lista con entusiasmo, sino que devuelve un generador que producirá los elementos de forma perezosa.
Bajo demanda. En otras palabras, si una lista por comprensión es una fábrica de listas, un generador
expresión es una fábrica de generadores.
El ejemplo 14-8 es una demostración rápida de una expresión generadora, comparándola con una lista comprensiva.
hensión
Ejemplo 14-8. La función generadora gen_AB es utilizada por una lista de comprensión, luego por
una expresión generadora
Seguir
final.
>>> para i en res1: #
... print('-->', i)
...
-->AAA
--> BBB
>>> res2 = (x*3 para x en gen_AB()) # >>>
res2 #
<objeto generador <genexpr> en 0x10063c240>
>>> para i en res2: #
... imprimir('-->', i)
...
comienzo
-->AAA
Seguir
--> BBB
final.
La comprensión de la lista itera ansiosamente sobre los elementos producidos por el generador
objeto producido llamando a gen_AB(): 'A' y 'B'. Tenga en cuenta la salida en el siguiente
Líneas: inicio, continuación, fin.
Este bucle for está iterando sobre la lista res1 producida por la lista de comprensión.
La expresión del generador devuelve res2. Se realiza la llamada a gen_AB() , pero eso
call devuelve un generador, que no se consume aquí.
Solo cuando el ciclo for itera sobre res2, el cuerpo de gen_AB realmente se ejecuta.
Cada iteración del ciclo for llama implícitamente a next(res2), avanzando gen_AB al
siguiente rendimiento. Tenga en cuenta la salida de gen_AB con la salida de la impresión
en el bucle for .
Entonces, una expresión generadora produce un generador y podemos usarlo para reducir aún
más el código en la clase Oración . Vea el Ejemplo 14-9.
RE_PALABRA = re.compilar('\w+')
oración de clase :
def __repr__(self):
return 'Oración(%s)' % reprlib.repr(self.text)
def __iter__(self):
return (match.group() for match in RE_WORD.finditer(self.text))
La única diferencia con el ejemplo 14-7 es el método __iter__ , que aquí no es una función
generadora (no tiene rendimiento) , pero usa una expresión generadora para construir un generador
y luego la devuelve. El resultado final es el mismo: la persona que llama a __iter__ obtiene un
objeto generador.
Las expresiones generadoras son azúcar sintáctica: siempre se pueden reemplazar por funciones
generadoras, pero a veces son más convenientes. La siguiente sección trata sobre el uso de
expresiones del generador.
En el Ejemplo 14-9, vimos que una expresión de generador es un atajo sintáctico para crear un
generador sin definir ni llamar a una función. Por otro lado, la función generadora
Las funciones son mucho más flexibles: puede codificar una lógica compleja con varias sentencias e
incluso puede usarlas como corrutinas (consulte el Capítulo 16).
Para los casos más simples, servirá una expresión generadora, y es más fácil de leer de un vistazo, como
muestra el ejemplo de Vector .
Mi regla general para elegir la sintaxis a usar es simple: si la expresión del generador abarca más de un
par de líneas, prefiero codificar una función de generador por el bien de la legibilidad. Además, debido a
que las funciones del generador tienen un nombre, se pueden reutilizar. Siempre puede nombrar una
expresión generadora y usarla más tarde asignándola a una variable, por supuesto, pero eso está
ampliando su uso previsto como generador único.
Sugerencia
de sintaxis Cuando se pasa una expresión generadora como único
argumento a una función o constructor, no necesita escribir un conjunto
de paréntesis para la llamada a la función y otro para encerrar la
expresión generadora. Un solo par funcionará, como en la llamada
Vector desde el método __mul__ en el Ejemplo 10-16, reproducido
aquí. Sin embargo, si hay más argumentos de función después de la
expresión del generador, debe encerrarlo entre paréntesis para evitar un SyntaxError:
def __mul__(self, escalar): if
isinstance(escalar, numeros.Real):
devuelve Vector (n * escalar para n en sí mismo) de lo
contrario:
volver No implementado
Los ejemplos de oraciones que hemos visto ejemplifican el uso de generadores que desempeñan el papel
de iteradores clásicos: recuperar elementos de una colección. Pero los generadores también se pueden
usar para producir valores independientes de una fuente de datos. La siguiente sección muestra un ejemplo
de eso.
Cubriremos itertools.count en la siguiente sección, pero ¿qué sucede si necesita generar un AP acotado
de números de cualquier tipo?
El ejemplo 14-10 muestra algunas pruebas de consola de una clase ArithmeticProgression que veremos
en un momento. La firma del constructor en el Ejemplo 14-10 es Progresión aritmética (comienzo, paso [,
final]). La función range() es similar a Arithme
ticProgression aquí, pero su firma completa es range(start, stop[, step]). Elegí implementar una
firma diferente porque para una progresión aritmética el paso es obligatorio pero el final es
opcional. También cambié los nombres de los argumentos de inicio/parada a inicio/fin para dejar
muy claro que opté por una firma diferente. En cada prueba del Ejemplo 14-10 , llamo a list() en
el resultado para inspeccionar los valores generados.
Tenga en cuenta que el tipo de los números en la progresión aritmética resultante sigue el tipo
de inicio o paso, de acuerdo con las reglas de coerción numérica de la aritmética de Python. En
el ejemplo 14-10, verá listas de números enteros, flotantes, fraccionarios y decimales .
rendimiento
resultado
índice += 1 resultado = self.begin + self.step * índice
Este bucle se ejecuta para siempre o hasta que el resultado coincida o supere self.end. Cuando
este bucle sale, también lo hace la función.
Se calcula el siguiente resultado potencial. Es posible que nunca se produzca, porque el ciclo
while puede terminar.
En la última línea del Ejemplo 14-11, en lugar de simplemente incrementar el resultado con self.step
iterativamente, opté por usar una variable de índice y calcular cada resultado sumando self.begin a
self.step multiplicado por index para reducir el efecto acumulativo de errores al trabajar con flotantes.
La clase ArithmeticProgression del ejemplo 14-11 funciona según lo previsto y es un claro ejemplo del
uso de una función generadora para implementar el método especial __iter__ . Sin embargo, si el
objetivo de una clase es construir un generador implementando __iter__, la clase puede reducirse a
una función generadora. Una función de generador es, después de todo, una fábrica de generadores.
El ejemplo 14-12 muestra una función generadora llamada aritprog_gen que hace el mismo trabajo que
ArithmeticProgression pero con menos código. Todas las pruebas en el Ejemplo 14-10 pasan si solo
10
llama a aritprog_gen en lugar de ArithmeticProgression.
9. En Python 2, había una función integrada coerce() pero desapareció en Python 3, se consideró innecesaria porque las
reglas de coerción numérica están implícitas en los métodos del operador aritmético. Entonces, la mejor manera que se
me ocurrió para obligar al valor inicial a ser del mismo tipo que el resto de la serie fue realizar la suma y usar su tipo para
convertir el resultado. Pregunté sobre esto en la lista de Python y obtuve una excelente respuesta de Steven D'Aprano.
10. El directorio 14-it-generator/ en el repositorio de código Fluent Python incluye doctests y un script, aritÿ
prog_runner.py, que ejecuta las pruebas contra todas las variaciones de los scripts aritprog*.py .
índice += 1
resultado = comenzar + paso * índice
El ejemplo 14-12 es genial, pero recuerda siempre: hay muchos generadores listos para usar en la
biblioteca estándar, y la siguiente sección mostrará una implementación aún más genial usando el
módulo itertools .
itertools en Python 3.4 tiene 19 funciones generadoras que se pueden combinar en una variedad de
formas interesantes.
(generación) 2.5
Sin embargo, itertools.count nunca se detiene, por lo que si llama a list(count()), Python intentará
crear una lista más grande que la memoria disponible y su máquina estará muy gruñona mucho
antes de que falle la llamada.
Por otro lado, está la función itertools.takewhile : produce un generador que consume otro generador
y se detiene cuando un predicado dado se evalúa como False.
Entonces podemos combinar los dos y escribir esto:
>>> gen = itertools.takewhile(lambda n: n < 3, itertools.count(1, .5)) >>> list(gen) [1, 1.5, 2.0,
2.5]
Ejemplo 14-13. aritprog_v3.py: esto funciona como las funciones anteriores de aritprog_gen
importar itertools
Tenga en cuenta que aritprog_gen no es una función generadora en el Ejemplo 14-13: no tiene rendimiento en su
cuerpo. Pero devuelve un generador, por lo que funciona como una fábrica de generadores, tal como lo hace una
función de generador.
El punto del Ejemplo 14-13 es: al implementar generadores, sepa lo que está disponible en la biblioteca estándar, de
lo contrario, es muy probable que reinvente la rueda. Es por eso que la siguiente sección cubre varias funciones de
generador listas para usar.
La función generadora os.walk es impresionante, pero en esta sección quiero centrarme en las funciones de propósito
general que toman iterables arbitrarios como argumentos y generan generadores de retorno que producen elementos
seleccionados, calculados o reorganizados. En las siguientes tablas, resumo dos docenas de ellos, de los módulos
integrados, itertools y functools .
Por conveniencia, los agrupé por funcionalidad de alto nivel, independientemente de dónde estén definidos.
Quizás conozca todas las funciones mencionadas en esta sección, pero algunas de
ellas están infrautilizadas, por lo que una descripción general rápida puede ser
buena para recordar lo que ya está disponible.
El primer grupo son funciones generadoras de filtrado: producen un subconjunto de elementos producidos por la
entrada iterable, sin cambiar los elementos mismos. Usamos itertools.take while anteriormente en este capítulo, en
“Progresión aritmética con itertools” en la página 423. Al igual que takewhile, la mayoría de las funciones enumeradas
en la Tabla 14-1 toman un predicado, que es una función booleana de un argumento que se aplicará a cada elemento
en la entrada para determinar si el elemento está incluido en la salida.
iterar compress(it, selector_it) Consume dos iterables en paralelo; produce elementos de él siempre que el
iterar dropwhile (predicado, Lo consume omitiendo elementos mientras el predicado calcula la verdad, luego produce todos
(incorporado) filtrar (predicado, Aplica el predicado a cada elemento de iterable, generando el elemento si el predicado (elemento)
iterar filterfalse(predicado, Igual que el filtro, con la lógica de predicado negada: produce elementos cada vez que el predicado
iterar islice(it, stop) o Produce elementos a partir de una porción , similar a s[:stop] o
instrumentos islice(it, start, stop, s[start:stop:step] excepto que puede ser iterable y la operación es perezosa
step=1)
iterar takewhile(predicado, Produce elementos mientras el predicado calcula la verdad, luego se detiene y no se realizan
más comprobaciones
instrumentos it)
El listado de la consola en el Ejemplo 14-14 muestra el uso de todas las funciones en la Tabla 14-1.
El siguiente grupo son los generadores de mapeo: producen elementos calculados a partir de cada
elemento individual en el iterable de entrada, o iterables, en el caso de map y starmap. 11 Los
generadores de la tabla 14-2 arrojan un resultado por elemento en los iterables de entrada. Si la entrada
proviene de más de un iterable, la salida se detiene tan pronto como se agota el primer iterable de
entrada.
11. Aquí, el término "mapeo" no está relacionado con los diccionarios, pero tiene que ver con el mapa incorporado.
itertools acumulan (eso, rendimientos sumas acumuladas; si se proporciona func , se obtiene el resultado de aplicarlo al
[función]) primer par de elementos, luego al primer resultado y al siguiente elemento, etc.
(incorporado) enumerar(itera Produce 2 tuplas de la forma (índice, artículo), desde donde se cuenta el índice
ble, inicio=0) inicio, y el elemento se toma del iterable
(incorporado) mapa(función, it1, Aplica func a cada elemento del mismo, dando el resultado; si se dan N iterables, func
[it2, …, itN]) debe tomar N argumentos y los iterables se consumirán en paralelo
itertools starmap(func, it) Aplica func a cada elemento del mismo, dando el resultado; la iterable de entrada debe producir
Suma corriente.
Funcionamiento mínimo.
Correr al máximo.
Producto en ejecución.
Factoriales desde 1! a las 10!.
Multiplicar números de dos iterables en paralelo: los resultados se detienen cuando finaliza el iterable
más corto.
Promedio corriente.
A continuación, tenemos el grupo de generadores de fusión: todos estos elementos de rendimiento de múltiples
iterables de entrada. chain y chain.from_iterable consumen los iterables de entrada secuencialmente (uno tras
otro), mientras que product, zip y zip_longest consumen los iterables de entrada en paralelo. Consulte la Tabla
14-3.
cadena itertools(it1, …, itN) Entrega todos los elementos de it1, luego de it2 , etc., sin problemas
itertools chain.from_iterable(it) Produce todos los elementos de cada iterable producido por él, uno tras otro,
perfectamente; debería producir elementos iterables , por ejemplo, una lista de iterables
producto itertools(it1, …, itN, repetir Producto cartesiano: produce N-tuplas creadas mediante la combinación de elementos de
turba=1) cada entrada iterable como podría producir bucles for anidados; repetir permite que los
(incorporado) zip(it1, …, itN) Produce N tuplas creadas a partir de elementos tomados de los iterables en paralelo,
itertools zip_longest(it1, …, itN, valor Produce N-tuplas construidas a partir de elementos tomados de los iterables en
de relleno=Ninguno) paralelo, deteniéndose solo cuando se agota el último iterable, llenando los espacios en
blanco con el valor de relleno
El ejemplo 14-17 muestra el uso de las funciones del generador itertools.chain y zip y sus hermanos. Recuérdese
que la función zip lleva el nombre del cierre de cremallera o cremallera (sin relación con la compresión). Tanto zip
como itertools.zip_longest se introdujeron en “The Awesome zip” en la página 293.
zip se usa comúnmente para fusionar dos iterables en una serie de dos tuplas.
itertools.zip_longest funciona como zip, excepto que consume todos los iterables de entrada
hasta el final, rellenando las tuplas de salida con None según sea necesario.
[(0, 0, 0), (0, 0, 1), (0, 1, 0), (0, 1, 1), (1, 0, 0), (1, 0, 1), ( 1, 1, 0), (1,
1, 1)] >>> filas = itertools.product('AB', rango(2), repetir=2) >>> for
fila en filas: imprimir(fila)
...
('A', 0, 'A', 0)
('A', 0, 'A', 1)
('A', 0, 'B', 0)
('A', 0, 'B', 1)
('A', 1, 'A', 0)
('A', 1, 'A', 1)
('A', 1, 'B', 0)
('A', 1, 'B', 1)
('B', 0, 'A', 0)
('B', 0, 'A', 1)
('B', 0, 'B', 0)
('B', 0, 'B', 1)
('B', 1, 'A', 0)
('B', 1, 'A', 1)
('B', 1, 'B', 0)
('B', 1, 'B', 1)
El producto cartesiano de una str con tres caracteres y un rango con dos enteros da seis tuplas
(porque 3 * 2 es 6).
El producto de dos rangos de cartas ('AK') y cuatro palos es una serie de ocho tuplas.
Dado un solo iterable, el producto produce una serie de tuplas, no muy útiles.
El argumento de la palabra clave repeat=N le dice al producto que consuma cada entrada iterable
N veces.
Algunas funciones generadoras expanden la entrada generando más de un valor por elemento de
entrada. Se enumeran en la tabla 14-4.
Tabla 14-4. Funciones de generador que expanden cada elemento de entrada en salida múltiple
elementos
combinaciones itertools(it, Combinaciones de rendimiento de elementos out_len de los elementos producidos por él
out_len)
itertools combinaciones_con_reubicación(it, Produce combinaciones de elementos out_len de los elementos producidos por él,
recuento de itertools (inicio = 0, paso = 1) Produce números a partir del inicio, incrementado por paso, indefinidamente
itertools ciclo(it) Produce elementos almacenando una copia de cada uno, luego produce la secuencia
itertools repetir (elemento, [veces]) Entregar el artículo dado repetidamente, indefinidamente a menos que varias veces
es dado
Las funciones de conteo y repetición de itertools devuelven generadores que evocan elementos
de la nada: ninguno de ellos toma un iterable como entrada. Vimos itertools.count en
“Progresión aritmética con itertools” en la página 423. El generador de ciclos hace un
copia de seguridad de la entrada iterable y produce sus elementos repetidamente. El ejemplo 14-19 ilustra
el uso de conteo, repetición y ciclo.
No puedo crear una lista a partir de ct, porque ct nunca se detiene, así que busco los siguientes tres
elementos.
Puedo crear una lista a partir de un generador de conteo si está limitado por islice o takewhile.
Una lista solo se puede construir si está limitada por islice; se recuperan los siete elementos siguientes
aquí.
Un uso común de repetir: proporcionar un argumento fijo en el mapa; aquí proporciona el multiplicador 5 .
Ejemplo 14-20. Las funciones del generador combinatorio producen múltiples valores por elemento de entrada
Todas las combinaciones de len()==2 de los elementos en 'ABC'; el orden de los elementos en las tuplas
generadas es irrelevante (podrían ser conjuntos).
Todas las combinaciones de len()==2 de los elementos en 'ABC', incluidas las combinaciones con elementos
repetidos.
Todas las permutaciones de len()==2 de los elementos en 'ABC'; el orden de los elementos en las tuplas
generadas es relevante.
El último grupo de funciones generadoras que cubriremos en esta sección está diseñado para generar todos los
elementos en los iterables de entrada, pero reorganizados de alguna manera. Aquí hay dos funciones que devuelven
múltiples generadores: itertools.groupby e itertools.tee. La otra función generadora en este grupo, la incorporada
invertida , es la única cubierta en esta sección que no acepta ningún iterable como entrada, sino solo secuencias.
Esto tiene sentido: debido a que invertido arrojará los elementos del último al primero, solo funciona con una
secuencia con una longitud conocida. Pero evita el costo de hacer una copia invertida de la secuencia al proporcionar
cada elemento según sea necesario. Puse la función itertools.product junto con los generadores combinados en la
Tabla 14-3 porque todos consumen más de un iterable, mientras que los generadores en la Tabla 14-5 aceptan como
máximo una entrada iterable.
itertools groupby(it, Produce 2 tuplas de la forma (clave, grupo), donde clave es el criterio de agrupación
clave=Ninguno) y grupo es un generador que genera los elementos del grupo
(incorporado) invertido (secuencia) Entrega elementos de secuencia en orden inverso, del último al primero; seq debe ser una secuencia
camiseta itertools(es, n=2) Produce una tupla de n generadores, cada uno de los cuales produce los elementos de la entrada iterable
independientemente
Para usar groupby, la entrada debe estar ordenada; aquí las palabras están ordenadas por longitud.
Nuevamente, recorra el par de clave y grupo para mostrar la clave y expandir el grupo en una lista.
Aquí se usa el generador inverso para iterar sobre los animales de derecha a izquierda.
La última de las funciones generadoras de este grupo es iterator.tee, que tiene un comportamiento único: produce
varios generadores iterables a partir de una única entrada, cada uno de los cuales produce todos los elementos
de la entrada. Esos generadores se pueden consumir de forma independiente, como se muestra en el ejemplo
14-22.
Ejemplo 14-22. itertools.tee produce múltiples generadores, cada uno de los cuales produce cada elemento del
generador de entrada
>>> lista(itertools.tee('ABC'))
[<itertools._tee objeto en 0x10222abc8>, <itertools._tee objeto en 0x10222ac08>] >>> g1, g2 =
itertools.tee('ABC') >> > siguiente(g1)
'A'
>>> siguiente(g2)
'A'
>>> siguiente(g2)
'B'
>>> lista(g1)
['B', 'C'] >>>
lista(g2)
['C']
>>> lista(zip(*itertools.tee('ABC')))
[('A', 'A'), ('B', 'B'), ('C', 'C')]
Tenga en cuenta que varios ejemplos en esta sección usaron combinaciones de funciones generadoras.
Esta es una gran característica de estas funciones: debido a que todas toman generadores como argumentos y
devuelven generadores, se pueden combinar de muchas maneras diferentes.
En cuanto al tema de la combinación de generadores, la declaración yield from , nueva en Python 3.3, es una
herramienta para hacer precisamente eso.
... rendimiento yo
...
>>> s = 'ABC'
>>> t = tupla(rango(3))
>>> lista(cadena(s, t))
['A', 'B', 'C', 0, 1, 2]
La función del generador de cadenas está delegando a cada iterable recibido a su vez. PEP 380
— La sintaxis para delegar a un subgenerador introdujo una nueva sintaxis para hacerlo, que se muestra
en la siguiente lista de consola:
Como puede ver, yield from i reemplaza el bucle for interno por completo. El uso del rendimiento
from en este ejemplo es correcto, y el código se lee mejor, pero parece meramente sintáctico
azúcar. Además de reemplazar un bucle, el rendimiento de crea un canal que conecta el interior
generador directamente al cliente del generador exterior. Este canal se vuelve realmente imÿ
importante cuando los generadores se usan como corrutinas y no solo producen sino que también consumen
valores del código del cliente. El capítulo 16 se sumerge en las corrutinas y tiene varias páginas.
explicando por qué el rendimiento de es mucho más que azúcar sintáctico.
Después de este primer encuentro con yield from, volveremos a nuestra revisión de iterable-savvy
funciones en la biblioteca estándar.
Tabla 14-6. Funciones integradas que leen iterables y devuelven valores únicos
(incorporado) Todo eso) Devuelve True si todos los elementos son verdaderos; de lo contrario, devuelve False; todos([])
devuelve verdadero
(incorporado) cualquiera Devuelve True si alguno de sus elementos es verdadero; de lo contrario, devuelve False; ningún([])
devuelve falso
(incorporado) max(it, [clave=,] [por defecto=]) Devuelve el valor máximo de los elementos que contiene;a la clave es un pedido
(incorporado) min(it, [clave=,] [por defecto=]) vacío. Devuelve el valor mínimo de los elementos en él. bkey es una función de
functools reduce(func, it, [inicial]) vacío Devuelve el resultado de aplicar func al primer par de elementos, luego a ese
resultado y al tercer elemento, y así sucesivamente; si se da, la inicial forma el par inicial
(incorporado) sum(it, comienzo=0) La suma de todos los elementos que contiene, con el valor de inicio opcional
a también se puede llamar como max(arg1, arg2, …, [key=?]), en cuyo caso se devuelve el máximo entre los argumentos.
b
También se puede llamar como min(arg1, arg2, …, [key=?]), en cuyo caso se devuelve el mínimo entre los argumentos.
Verdadero >>>
siguiente(g) 8
Una explicación más larga sobre functools.reduce apareció en “Vector Take #4: Hashing and a
Faster ==” en la página 288.
Se ordena otro incorporado que toma un iterable y devuelve algo más . A diferencia de reversed,
que es una función generadora, sorted crea y devuelve una lista real. Después de todo, cada uno
de los elementos de la iterable de entrada debe leerse para que puedan ordenarse, y la ordenación
ocurre en una lista, por lo tanto , sorted solo devuelve esa lista después de que se hace. Menciono
sorted aquí porque consume un iterable arbitrario.
Por supuesto, las funciones ordenadas y reductoras solo funcionan con iterables que finalmente se detienen.
De lo contrario, seguirán recopilando elementos y nunca devolverán un resultado.
Ahora volveremos al iter() incorporado: tiene una característica poco conocida que aún no hemos cubierto.
Pero iter tiene otro truco: se puede llamar con dos argumentos para crear un iterador a partir de una función
normal o cualquier objeto al que se pueda llamar. En este uso, el primer argumento debe ser invocable para
ser invocado repetidamente (sin argumentos) para generar valores, y el segundo argumento es un centinela:
un valor de marcador que, cuando lo devuelve el invocable, hace que el iterador genere StopIteration en su
lugar . de ceder el centinela.
El siguiente ejemplo muestra cómo usar iter para lanzar un dado de seis caras hasta que salga un 1 :
...
...
4
3
Tenga en cuenta que la función iter aquí devuelve un callable_iterator. El bucle for del ejemplo puede
ejecutarse durante mucho tiempo, pero nunca mostrará 1, porque ese es el valor centinela. Como es habitual
con los iteradores, el objeto d6_iter del ejemplo se vuelve inútil una vez agotado. Para empezar de nuevo,
debe reconstruir el iterador invocando iter(...) de nuevo.
Un ejemplo útil se encuentra en la documentación de la función integrada iter . Este fragmento lee líneas de
un archivo hasta que se encuentra una línea en blanco o se llega al final del archivo:
Para cerrar este capítulo, presento un ejemplo práctico del uso de generadores para manejar un gran
volumen de datos de manera eficiente.
Hace algunos años trabajé en BIREME, una biblioteca digital administrada por la OPS/OMS (Organización
Panamericana de la Salud/Organización Mundial de la Salud) en São Paulo, Brasil.
Entre los conjuntos de datos bibliográficos creados por BIREME se encuentran LILACS (Índice de Ciencias de la
Salud de América Latina y el Caribe) y SciELO (Biblioteca Científica Electrónica en Línea), dos bases de datos
integrales que indexan la literatura científica y técnica producida en la región.
Desde finales de la década de 1980, el sistema de base de datos utilizado para administrar LILACS es CDS/ISIS,
una base de datos documental no relacional creada por la UNESCO y finalmente reescrita en C por BIREME para
ejecutarse en servidores GNU/Linux. Uno de mis trabajos era investigar alternativas para una posible migración de
LILACS, y eventualmente de SciELO, mucho más grande, a una base de datos de documentos moderna y de código
abierto como CouchDB o MongoDB.
Como parte de esa investigación, escribí un script de Python, isis2json.py, que lee un archivo CDS/ISIS y escribe un
archivo JSON adecuado para importar a CouchDB o MongoDB. Inicialmente, el script leía archivos en formato
ISO-2709 exportados por CDS/ISIS. La lectura y la escritura debían realizarse de forma incremental porque los
conjuntos de datos completos eran mucho más grandes que la memoria principal. Eso fue bastante fácil: cada
iteración del bucle for principal leía un registro del archivo .iso , lo modificaba y lo escribía en la salida .json .
Sin embargo, por razones operativas, se consideró necesario que isis2json.py admitiera otro formato de datos CDS/
ISIS: los archivos binarios .mst utilizados en producción en BIREME, para evitar la costosa exportación a ISO-2709.
Ahora tenía un problema: las bibliotecas utilizadas para leer ISO-2709 y .mstfiles tenían API muy diferentes. Y el ciclo
de escritura de JSON ya era complicado porque el script aceptaba una variedad de opciones de línea de comandos
para reestructurar cada registro de salida. Leer datos utilizando dos API diferentes en el mismo bucle for donde se
produjo el JSON sería difícil de manejar.
La solución fue aislar la lógica de lectura en un par de funciones generadoras: una para cada formato de entrada
compatible. Al final, el script isis2json.py se dividió en cuatro funciones. Puede ver el script principal de Python 2 en el
Ejemplo A-5, pero el código fuente completo con las dependencias se encuentra en fluentpython/ isis2json en GitHub.
Aquí hay una descripción general de alto nivel de cómo está estructurado el script:
principal
La función principal utiliza argparse para leer las opciones de la línea de comandos que configuran la estructura
de los registros de salida. Según la extensión del nombre de archivo de entrada, se selecciona una función de
generador adecuada para leer los datos y generar los registros, uno por uno.
iter_iso_records Esta
función generadora lee archivos .iso (se supone que están en formato ISO-2709). Toma dos argumentos:
el nombre del archivo e isis_json_type, una de las opciones relacionadas con la estructura del registro.
Cada iteración de su ciclo for lee un registro, crea un dict vacío, lo llena con datos de campo y genera
el dict.
iter_mst_records Esta
otra función generadora lee archivos .mst.13 Si observa el código fuente de isis2json.py , verá que no
es tan simple como iter_iso_records, pero su interfaz y estructura general es la misma: toma un nombre
de archivo y un argumento isis_json_type e ingresa un bucle for , que crea y produce un dict por
iteración, que representa un único registro.
write_json
Esta función realiza la escritura real de los registros JSON, uno a la vez. Toma
numerosos argumentos, pero el primero , input_gen, es una referencia a una
función generadora: ya sea iter_iso_records o iter_mst_records. El bucle for
principal en write_json itera sobre los diccionarios producidos por el generador
seleccionado en put_gen , lo masajea de varias maneras según lo determinen las
opciones de la línea de comandos y agrega el registro JSON al archivo de salida.
Al aprovechar las funciones del generador, pude desacoplar la lógica de lectura de la lógica de escritura. Por
supuesto, la forma más sencilla de desacoplarlos sería leer todos los registros en la memoria y luego
escribirlos en el disco. Pero esa no era una opción viable debido al tamaño de los conjuntos de datos.
Mediante el uso de generadores, la lectura y la escritura se intercalan, por lo que el script puede procesar
archivos de cualquier tamaño.
Ahora, si isis2json.py necesita admitir un formato de entrada adicional, por ejemplo, MARCXML, un DTD
utilizado por la Biblioteca del Congreso de EE. UU. para representar datos ISO-2709, será fácil agregar una
tercera función de generador para implementar la lógica de lectura, sin cambiando cualquier cosa en la
complicada función write_json .
Esto no es ciencia espacial, pero es un ejemplo real en el que los generadores proporcionaron una solución
flexible para procesar bases de datos como un flujo de registros, manteniendo bajo el uso de la memoria
independientemente de la cantidad de datos. Cualquiera que maneje grandes conjuntos de datos encuentra
muchas oportunidades para usar generadores en la práctica.
La siguiente sección aborda un aspecto de los generadores que en realidad omitiremos por ahora. Siga
leyendo para entender por qué.
13. La biblioteca utilizada para leer el complejo binario .mst está escrita en Java, por lo que esta funcionalidad solo está disponible
cuando isis2json.py se ejecuta con el intérprete Jython, versión 2.5 o posterior. Para obtener más detalles, consulte el
archivo README.rst en el repositorio. Las dependencias se importan dentro de las funciones del generador que las
necesitan, por lo que el script puede ejecutarse incluso si solo está disponible una de las bibliotecas externas.
Al igual que .__next__(), .send() hace que el generador avance al siguiente rendimiento, pero también
permite que el cliente que usa el generador le envíe datos: cualquier argumento que se pase a .send()
se convierte en el valor del correspondiente expresión de rendimiento dentro del cuerpo de la función
del generador. En otras palabras, .send() permite el intercambio de datos bidireccional entre el código
del cliente y el generador, en contraste con .__next__(), que solo permite que el cliente reciba datos
del generador.
Esta es una "mejora" tan importante que en realidad cambia la naturaleza de los generadores: cuando
se usan de esta manera, se convierten en corrutinas. David Beazley, probablemente el escritor y
orador más prolífico sobre corrutinas en la comunidad de Python, advirtió en un famoso tutorial PyCon
US 2009:
• Nota: Hay un uso de hacer que el rendimiento produzca un valor en una corrutina, pero no está vinculado
iteración.14
—David Beazley
“Un Curioso Curso sobre Corrutinas y Concurrencia”
Seguiré el consejo de Dave y cerraré este capítulo, que en realidad trata sobre técnicas de iteración,
sin tocar el envío y las otras características que hacen que los generadores se puedan usar como
corrutinas. Las corrutinas se tratarán en el Capítulo 16.
14. Diapositiva 33, "Mantener las cosas claras", en "Un curso curioso sobre corrutinas y concurrencia".
15. De acuerdo con el archivo de la jerga, asimilar no es simplemente aprender algo, sino absorberlo para que "se convierta en parte de
tú, parte de tu identidad.”
calibres En Python, un iterador clásico implementado “a mano” como en el Ejemplo 14-4 no tiene uso
práctico, excepto como ejemplo didáctico.
En este capítulo, construimos algunas versiones de una clase para iterar sobre palabras individuales en
archivos de texto que pueden ser muy largos. Gracias al uso de generadores, las sucesivas refactorizaciones
de la clase Sentence se vuelven más cortas y fáciles de leer, cuando sabes cómo funcionan.
Después de eso, analizamos la función integrada iter : primero, para ver cómo devuelve un iterador cuando
se llama iter(o), y luego para estudiar cómo construye un iterador a partir de cualquier función cuando se
llama iter(func, sentinel ).
Para un contexto práctico, describí la implementación de una utilidad de conversión de base de datos que
utiliza funciones de generador para desacoplar la lógica de lectura de la de escritura, lo que permite el
manejo eficiente de grandes conjuntos de datos y facilita la compatibilidad con más de un formato de
entrada de datos.
También se mencionó en este capítulo el rendimiento de la sintaxis, nuevo en Python 3.3, y coroutines.
Ambos temas se acaban de presentar aquí; obtienen más cobertura más adelante en el libro.
Otras lecturas
Una explicación técnica detallada de los generadores aparece en The Python Language Reference en
6.2.9. Expresiones de rendimiento. El PEP donde se definieron las funciones del generador es PEP 255
— Generadores Simples.
La documentación del módulo itertools es excelente debido a todos los ejemplos incluidos.
Aunque las funciones de ese módulo se implementan en C, la documentación muestra cuántas de ellas se
escribirían en Python, a menudo aprovechando otras funciones del módulo. Los ejemplos de uso también
son excelentes: por ejemplo, hay un fragmento que muestra cómo usar la función de acumulación para
amortizar un préstamo con intereses, dada una lista de pagos a lo largo del tiempo. También hay una
sección de Recetas de Itertools con funciones adicionales de alto rendimiento que utilizan las funciones de
itertools como bloques de construcción.
El rendimiento de la sintaxis se explica con ejemplos en Novedades de Python 3.3 (consulte PEP 380:
Sintaxis para delegar en un subgenerador). También lo cubriremos en detalle en "Uso de yield from" en la
página 477 y "El significado de yield from" en la página 483 en el Capítulo 16.
Si está interesado en las bases de datos de documentos y desea obtener más información
sobre el contexto de “Estudio de caso: Generadores en una utilidad de conversión de bases
de datos” en la página 437, Code4Lib Journal, que cubre la intersección entre bibliotecas y
tecnología, publicó mi artículo “De ISIS a CouchDB: Bases de datos y modelos de datos para
registros bibliográficos”. Una sección del documento describe el script isis2json.py . El resto
explica por qué y cómo el modelo de datos semiestructurados implementado por bases de
datos de documentos como CouchDB y MongoDB son más adecuados para la recopilación
cooperativa de datos bibliográficos que el modelo relacional.
Plataforma improvisada
Los diseñadores deben asegurarse de que los controles y las pantallas para diferentes propósitos sean
significativamente diferentes entre sí.
—Donald normando
El código fuente desempeña el papel de "controles y pantallas" en los lenguajes de programación. Pienso
que Python está excepcionalmente bien diseñado; su código fuente suele ser tan legible como el pseudocódigo.
Pero nada es perfecto. Guido van Rossum debería haber seguido el consejo de Donald Norman (previamente
citado) e introducido otra palabra clave para definir expresiones generadoras, en lugar de reutilizar def. La
sección "Pronunciamientos BDFL" de PEP 255 - Generadores simples en realidad argumenta:
Pero Guido odia introducir nuevas palabras clave y no encontró ese argumento convincente, por lo que nos
quedamos con la definición.
Reutilizar la sintaxis de funciones para generadores tiene otras malas consecuencias. En el artículo y trabajo
experimental “Python, the Full Monty: A Tested Semantics for the Python Programming Language”, Politz16
et al. muestre este ejemplo trivial de una función generadora (sección 4.1 del artículo):
Luego, los autores señalan que no podemos abstraer el proceso de rendimiento con una llamada de función
(Ejemplo 14-24).
16. Joe Gibbs Politz, Alejandro Martínez, Matthew Milano, Sumner Warren, Daniel Patterson, Junsong Li,
Anand Chitipothu y Shriram Krishnamurthi, “Python: The Full Monty”, SIGPLAN Not. 48, 10 (octubre de
2013), 217-232.
Ejemplo 14-24. “[Esto] parece realizar una simple abstracción sobre el proceso de
ceder” (Politz et al.)
def f(): def
do_yield(n): yield nx =
0 while True:
X+=1
do_rendimiento(x)
Si llamamos a f() en el Ejemplo 14-24, obtenemos un ciclo infinito, y no un generador, porque la palabra clave
yield solo convierte a la función que lo encierra inmediatamente en una función generadora.
Aunque las funciones de generador parecen funciones, no podemos delegar otra función de generador con una
simple llamada de función. Como punto de comparación, el idioma Lua no impone esta limitación. Una corrutina
Lua puede llamar a otras funciones y cualquiera de ellas puede ceder el paso a la persona que llamó originalmente.
El nuevo rendimiento de la sintaxis se introdujo para permitir que un generador o corrutina de Python delegue el
trabajo a otro, sin requerir la solución de un bucle for interno.
El ejemplo 14-24 se puede "arreglar" anteponiendo a la llamada de función el rendimiento de, como en el ejemplo
14-25.
Ejemplo 14-25. Esto en realidad realiza una abstracción simple sobre el proceso de
producir
def f(): def
do_rendimiento(n):
rendimiento n
x=0
mientras es Verdadero:
x += 1
rendimiento de do_yield(x)
Reutilizar def para declarar generadores fue un error de usabilidad, y el problema se agravó en Python 2.5 con
rutinas, que también están codificadas como funciones con rendimiento. En el caso de las corrutinas, el
rendimiento simplemente aparece, generalmente, en el lado derecho de una asignación, porque recibe el
argumento de la llamada .send() del cliente. Como dice David Beazley:
A pesar de algunas similitudes, los generadores y las rutinas son básicamente dos conceptos
diferentes.17
Creo que las rutinas también merecían su propia palabra clave. Como veremos más adelante, las corrutinas a
menudo se usan con decoradores especiales, lo que las distingue de otras funciones. Pero las funciones del
generador no se decoran con tanta frecuencia, por lo que tenemos que escanear sus cuerpos en busca de
rendimiento para darnos cuenta de que no son funciones en absoluto, sino una bestia completamente diferente.
Se puede argumentar que, debido a que esas funciones se hicieron para funcionar con poca sintaxis
adicional, la sintaxis adicional sería simplemente "azúcar sintáctica". Me gusta el azúcar sintáctico
cuando hace que las características que son diferentes se vean diferentes. La falta de azúcar sintáctico
es la razón principal por la que el código Lisp es difícil de leer: cada construcción de lenguaje en Lisp
parece una llamada de función.
Hay al menos tres formas de pensar sobre la relación entre iteradores y generadores.
El primero es el punto de vista de la interfaz. El protocolo iterador de Python define dos métodos:
__next__ y __iter__. Los objetos generadores implementan ambos, por lo que desde esta perspectiva,
cada generador es un iterador. Según esta definición, los objetos creados por enumerate() son iteradores:
Esto sucede porque tipos.GeneratorType se define como "El tipo de objetos iteradores generadores,
producidos al llamar a una función generadora".
El tercero es el punto de vista conceptual. En el patrón de diseño clásico de Iterator, como se define en
el libro GoF, el iterador atraviesa una colección y produce elementos a partir de ella. El iterador puede
ser bastante complejo; por ejemplo, puede navegar a través de una estructura de datos en forma de árbol.
Pero, por mucha lógica que haya en un iterador clásico, siempre lee valores de una fuente de datos
existente, y cuando llamas a next(it), no se espera que el iterador cambie el elemento que obtiene de la
fuente; se supone que debe rendirse tal como está.
Por el contrario, un generador puede producir valores sin atravesar necesariamente una colección,
como lo hace el rango . E incluso si se adjuntan a una colección, los generadores no se limitan a generar
solo los elementos que contiene, sino que pueden generar algunos otros valores derivados de ellos. Un
claro ejemplo de esto es la función enumerar . Por la definición original del patrón de diseño
tern, el generador devuelto por enumerate no es un iterador porque crea las tuplas que produce.
clase Fibonacci:
def __iter__(self):
devuelve el Generador de Fibonacci()
def __siguiente__(auto):
resultado = auto.a
self.a, self.b = self.b, self.a + self.b devuelve el
resultado
def __iter__(auto):
retornar auto
El ejemplo 14-26 funciona, pero es solo un ejemplo tonto. Aquí está el generador Pythonic Fibonacci:
def fibonacci(): a, b
= 0, 1 while
True: produce
a a, b =
b, a + b
Y, por supuesto, siempre puede usar la construcción del lenguaje generador para realizar las
funciones básicas de un iterador: recorrer una colección y obtener elementos de ella.
En realidad, los programadores de Python no son estrictos con esta distinción: los generadores
también se denominan iteradores, incluso en los documentos oficiales. La definición canónica de un
iterador en el Glosario de Python es tan general que abarca tanto iteradores como generadores:
Vale la pena leer la definición completa de iterador en el Glosario de Python. Por otro lado, la
definición de generador allí trata iterador y generador como sinónimos, y usa la palabra "generador"
para referirse tanto a la función del generador como al generador.
objeto que construye. Entonces, en la jerga de la comunidad de Python, iterador y generador son sinónimos
bastante cercanos.
Sin embargo, esa misma oración tiene una nota al pie que dice:
Podemos hacer que esta interfaz sea aún más pequeña fusionando Next, IsDone y
CurrentItem en una sola operación que avance al siguiente objeto y lo devuelva. Si el
recorrido finaliza, esta operación devuelve un valor especial (0, por ejemplo) que marca
el final de la iteración.
Esto es parecido a lo que tenemos en Python: el único método __next__ hace el trabajo. Pero en lugar de
usar un centinela, que podría pasarse por alto por error, la excepción StopIteration señala el final de la
iteración. Simple y correcto: así es Python.
18. Gamma et. al., Patrones de diseño: Elementos de software orientado a objetos reutilizable, pág. 261.
CAPÍTULO 15
Los administradores de contexto pueden llegar a ser casi tan importantes como la propia subrutina. Solo
hemos arañado la superficie con ellos. […] Basic tiene una declaración with , hay declaraciones with en
muchos idiomas. Pero no hacen lo mismo, todos hacen algo muy superficial, lo salvan de búsquedas
repetidas de [atributos] punteadas, no hacen configuración y desmontaje. Solo porque es el mismo
nombre, no creas que es lo mismo. La declaración with es muy importante.1
—Raymond Hettinger
Evangelista elocuente de Python
En este capítulo, discutiremos las funciones de flujo de control que no son tan comunes en otros lenguajes
y, por esta razón, tienden a pasarse por alto o a usarse poco en Python. Están:
La declaración with establece un contexto temporal y lo elimina de manera confiable, bajo el control de un
objeto administrador de contexto. Esto evita errores y reduce el código repetitivo, lo que hace que las API
sean al mismo tiempo más seguras y fáciles de usar. Los programadores de Python están encontrando
muchos usos para los bloques más allá del cierre automático de archivos.
La cláusula else no tiene ninguna relación con with. Pero esta es la Parte V, y no pude encontrar otro lugar
para cubrir otra cosa, y no tendría un capítulo de una página al respecto, así que aquí está.
Repasemos el tema más pequeño para llegar a la esencia real de este capítulo.
1. Discurso de apertura de PyCon EE. UU. 2013: "Qué hace que Python sea impresionante"; la parte sobre con comienza a las 23:00 y termina a las
26:15.
447
Machine Translated by Google
La semántica de for/else, while/else y try/else están estrechamente relacionadas, pero son muy diferentes
de if/else. Inicialmente, la palabra else dificultó mi comprensión de estas características, pero finalmente me
acostumbré.
por
El bloque else se ejecutará sólo si el ciclo for se ejecuta hasta su finalización (es decir, no si se cancela
for con una interrupción).
tiempo
El bloque else se ejecutará sólo si el bucle while sale porque la condición se volvió falsa (es decir, no
cuando el while se cancela con una interrupción).
try
El bloque else solo se ejecutará si no se genera ninguna excepción en el bloque try . Los documentos
oficiales también establecen: "Las excepciones en la cláusula else no son manejadas por las cláusulas
excepto anteriores".
En todos los casos, la cláusula else también se omite si una excepción o una instrucción return, break o
continue hace que el control salte fuera del bloque principal del compuesto.
declaración.
Creo que else es una elección muy mala para la palabra clave en todos los
casos, excepto si. Implica una alternativa excluyente, como "Ejecutar este
ciclo, de lo contrario hacer eso", pero la semántica para else en los ciclos
es la opuesta: "Ejecutar este ciclo, luego hacer aquello". Esto sugiere
entonces como una mejor palabra clave, que también tendría sentido en el
contexto de prueba : "Prueba esto, luego haz aquello". Sin embargo,
agregar una nueva palabra clave es un cambio radical en el idioma y Guido
lo evita como la peste.
El uso de else con estas declaraciones a menudo hace que el código sea más fácil de leer y ahorra la
molestia de configurar banderas de control o agregar declaraciones if adicionales .
else:
aumentar ValueError('¡No se encontró sabor a plátano!')
En el caso de los bloques try/except , else puede parecer redundante al principio. Después de todo,
after_call() en el siguiente fragmento de código se ejecutará solo si danger_call( ) no genera una
excepción, ¿correcto?
intente:
llamada_peligrosa( )
after_call() excepto
OSError: log('OSError...')
Sin embargo, hacerlo coloca after_call() dentro del bloque de prueba sin una buena razón. Para
mayor claridad y corrección, el cuerpo de un bloque de prueba solo debe tener las declaraciones
que pueden generar las excepciones esperadas. Esto es mucho mejor:
intente: llamada_peligrosa
() excepto OSError:
log('OSError...')
else: after_call()
Ahora está claro que el bloque try está protegiendo contra posibles errores en danger_call( ) y no
en after_call(). También es más obvio que after_call() solo se ejecutará si no se generan excepciones
en el bloque de prueba .
En Python, try/except se usa comúnmente para controlar el flujo, y no solo para el manejo de
errores. Incluso hay un acrónimo/eslogan para eso documentado en el glosario oficial de Python.
sario:
EAFP
Más fácil pedir perdón que permiso. Este estilo de codificación común de Python asume la
existencia de claves o atributos válidos y detecta excepciones si la suposición resulta falsa.
Este estilo limpio y rápido se caracteriza por la presencia de muchas declaraciones de prueba
y excepción. La técnica contrasta con el estilo LBYL común a muchos otros lenguajes como C.
Dado el estilo EAFP, tiene aún más sentido conocer y usar bien los bloques else en las sentencias
try/except .
Ahora abordemos el tema principal de este capítulo: los poderosos con declaración.
La declaración with se diseñó para simplificar el patrón try/finally , que garantiza que se realice alguna operación
después de un bloque de código, incluso si el bloque se aborta debido a una excepción, un retorno o una
llamada sys.exit() . El código de la cláusulafinal generalmente libera un recurso crítico o restaura algún estado
anterior que se modificó temporalmente.
El protocolo del administrador de contexto consta de los métodos __enter__ y __exit__ . Al comienzo de with,
se invoca __enter__ en el objeto del administrador de contexto. El papel de la cláusula finalmente lo desempeña
una llamada a __exit__ en el objeto del administrador de contexto al final del bloque with .
El ejemplo más común es asegurarse de que un objeto de archivo esté cerrado. Consulte el Ejemplo 15-1 para
ver una demostración detallada del uso de with para cerrar un archivo.
fp está vinculado al archivo abierto porque el método __enter__ del archivo devuelve self.
Lea algunos datos de fp.
Pero no puede realizar E/S con fp porque al final del bloque with , se llama al método
TextIOWrapper.__exit__ y se cierra el archivo.
2. Los bloques with no definen un nuevo alcance, como lo hacen las funciones y los módulos.
La primera llamada en el Ejemplo 15-1 hace un punto sutil pero crucial: el objeto del administrador de contexto es el
resultado de evaluar la expresión después de with, pero el valor vinculado a la variable de destino (en la cláusula as )
es el resultado de llamar a __enter__ en el objeto del administrador de contexto.
Sucede que en el Ejemplo 15-1, la función open() devuelve una instancia de TextIOWrapper y su método __enter__ se
devuelve a sí mismo. Pero el método __enter__ también puede devolver algún otro objeto en lugar del administrador
de contexto.
Cuando el flujo de control sale del bloque with de alguna manera, el método __exit__ se invoca en el objeto del
administrador de contexto, no en lo que sea devuelto por __enter__.
La cláusula as de la instrucción with es opcional. En el caso de abierto, siempre lo necesitará para obtener una
referencia al archivo, pero algunos administradores de contexto devuelven Ninguno porque no tienen ningún objeto útil
para devolver al usuario.
El ejemplo 15-2 muestra el funcionamiento de un administrador de contexto perfectamente frívolo diseñado para
resaltar la distinción entre el administrador de contexto y el objeto devuelto por su método __enter__ .
Ahora el bloque con ha terminado. Podemos ver que el valor devuelto por __enter__, contenido en what, es
la cadena 'JABBERWOCKY'.
def __enter__(self):
import sys
self.original_write = sys.stdout.write
sys.stdout.write = self.reverse_write devuelve
'JABBERWOCKY'
Devuelve la cadena 'JABBERWOCKY' solo para que tengamos algo que poner en el objetivo
variable qué.
Python llama a __exit__ con Ninguno, Ninguno, Ninguno si todo salió bien; si una excepción
se genera, los tres argumentos obtienen los datos de excepción, como se describe a continuación.
Si __exit__ devuelve Ninguno o cualquier cosa menos Verdadero, cualquier excepción generada en el
el bloque se propagará.
Cuando las aplicaciones reales se hacen cargo de la salida estándar, a menudo quieren
para reemplazar sys.stdout con otro objeto similar a un archivo por un tiempo, luego
volver al original. El contextlib.redirect_stdout
el administrador de contexto hace exactamente eso: simplemente pásele el objeto similar a un archivo
eso sustituirá a sys.stdout.
El intérprete llama al método __enter__ sin argumentos, más allá del yo implícito. Los tres argumentos pasados
a __exit__ son:
exc_type
La clase de excepción (por ejemplo, ZeroDivisionError).
exc_value
La instancia de excepción. A veces, los parámetros pasados al constructor de excepción,
como el mensaje de error, se pueden encontrar en exc_value.args.
rastrear
Un objeto de rastreo.3
Para obtener una descripción detallada de cómo funciona un administrador de contexto, consulte el Ejemplo
15-4, donde Looking Glass se usa fuera de un bloque with , por lo que podemos llamar manualmente a sus
métodos __enter__ y __exit__ .
'JABBERWOCKY'
Monster es la cadena 'JABBERWOCKY'. El identificador True aparece invertido porque toda la salida
a través de stdout pasa por el método de escritura que parcheamos en __enter__.
3. Los tres argumentos recibidos por sí mismo son exactamente lo que obtiene si llama a sys.exc_info() en el bloque
finalmente de una instrucción try/finally. Esto tiene sentido, considerando que la declaración with está destinada a
reemplazar la mayoría de los usos de try/finally, y llamar a sys.exc_info() a menudo era necesario para determinar
qué acción de limpieza sería necesaria.
Los administradores de contexto son una característica bastante novedosa y, de manera lenta pero segura, la comunidad
de Python está encontrando nuevos usos creativos para ellos. Algunos ejemplos de la biblioteca estándar son:
• Configuración de ambientes para operaciones aritméticas con objetos Decimales ; consulte la documentación de
decimal.localcontext .
• Aplicar parches temporales a los objetos para realizar pruebas; ver unittest.mock.patch
función.
La biblioteca estándar también incluye las utilidades contextlib , que se describen a continuación.
contextos con declaraciones" en la biblioteca estándar de Python. Además del ya mencionado redirect_stdout, el módulo
contextlib incluye clases y otras funciones que son más ampliamente aplicables:
cierre Una
función para crear administradores de contexto a partir de objetos que proporcionan un método close() pero no
implementan el protocolo __enter__/__exit__ .
reprimir
Un administrador de contexto para ignorar temporalmente las excepciones especificadas.
@contextmanager Un
decorador que le permite crear un administrador de contexto a partir de una función de generador simple, en lugar
de crear una clase e implementar el protocolo.
Decorador de contexto
Una clase base para definir administradores de contexto basados en clases que también se pueden usar como
decoradores de funciones, ejecutando la función completa dentro de un contexto administrado.
Salir de la pila
Un administrador de contexto que le permite ingresar un número variable de administradores de contexto. Cuando
finaliza el bloque with , ExitStack llama a los métodos __exit__ de los administradores de contexto apilados en orden
LIFO (última entrada, primera salida). Use esta clase cuando no sepa de antemano cuántos administradores de
contexto necesita ingresar en su bloque with ; por ejemplo, al abrir todos los archivos de una lista arbitraria de
archivos al mismo
tiempo.
Usando @contextmanager
El decorador @contextmanager reduce el repetitivo de crear un administrador de contexto: en lugar
de escribir una clase completa con métodos __enter__/__exit__ , simplemente implementa un
generador con un rendimiento único que debería producir lo que quiera que devuelva el método
__en ter__ .
En un generador decorado con @contextmanager, yield se usa para dividir el cuerpo de la función
en dos partes: todo antes de yield se ejecutará al comienzo del bloque while cuando el intérprete
llame a __enter__; el código después de yield se ejecutará cuando se llame a __exit__ al final del
bloque.
Aquí hay un ejemplo. El ejemplo 15-5 reemplaza la clase LookingGlass del ejemplo 15-3 con una
función generadora.
@contextlib.contextmanager def
looking_glass(): import sys
original_write =
sys.stdout.write
def escritura_inversa(texto):
escritura_original( texto[::-1])
sys.stdout.write = reverse_write
rendimiento 'JABBERWOCKY'
sys.stdout.write = original_write
Cuando el control sale del bloque with de alguna forma, la ejecución continúa después del yield; aquí se
restaura el sys.stdout.write original .
La única diferencia con el Ejemplo 15-2 es el nombre del administrador de contexto: espejo en lugar de
Espejo.
Esencialmente, el decorador contextlib.contextmanager envuelve la función en una clase que implementa los
métodos __enter__ y __exit__. 4
3. Devuelve el valor producido por next(gen), por lo que puede vincularse a una variable de destino en el
formulario with/as.
1. Comprueba que se pasó una excepción como exc_type; si es así, se invoca gen.throw(exception) , lo que
provoca que la excepción se genere en la línea de rendimiento dentro del cuerpo de la función del generador.
El ejemplo 15-5 tiene un defecto grave: si se genera una excepción en el cuerpo del bloque with , el intérprete de
Python la detectará y la generará nuevamente en la expresión yield dentro de looking_glass. Pero no hay manejo
de errores allí, por lo que la función looking_glass
4. La clase real se llama _GeneratorContextManager. Si quieres ver exactamente cómo funciona, lee su
código fuente en Lib/ contextlib.py en la distribución de Python 3.4.
cancelará sin restaurar el método sys.stdout.write original , dejando el sistema en un estado no válido.
El ejemplo 15-7 agrega un manejo especial de la excepción ZeroDivisionError , lo que lo hace funcionalmente
equivalente al ejemplo 15-3 basado en clases.
importar contextlib
@contextlib.contextmanager def
looking_glass(): import sys
original_write =
sys.stdout.write
def escritura_inversa(texto):
escritura_original( texto[::-1])
sys.stdout.write = escritura_inversa
''
msg
= try:
yield 'JABBERWOCKY'
excepto ZeroDivisionError:
msg = '¡Por favor, NO divida por cero!'
finalmente: sys.stdout.write = original_write if msg:
print(msg)
Cree una variable para un posible mensaje de error; este es el primer cambio en relación con el
Ejemplo 15-5.
Recuerde que el método __exit__ le dice al intérprete que ha manejado la excepción devolviendo True; en
ese caso, el intérprete suprime la excepción. Por otro lado, si __exit__ no devuelve explícitamente un valor,
el intérprete obtiene el Ninguno habitual y propaga la excepción. Con @contextmanager, el comportamiento
predeterminado se invierte: el método __exit__ proporcionado por el decorador asume que cualquier
excepción enviada al generador se maneja y debe suprimirse.5 Debe volver a generar explícitamente un
5. La excepción se envía al generador mediante el método throw, que se trata en “Terminación de la rutina y manejo
de excepciones” en la página 471.
importar csv
La función inplace es un administrador de contexto que le brinda dos identificadores, infh y outfh en el ejemplo,
para el mismo archivo, lo que permite que su código lo lea y lo escriba al mismo tiempo. Es más fácil de usar que
la función fileinput.input de la biblioteca estándar (que, por cierto, también proporciona un administrador de contexto).
Si desea estudiar el código fuente in situ de Martijn ( enumerado en la publicación), busque la palabra clave yield :
todo antes de que se trate de configurar el contexto, lo que implica crear un archivo de respaldo, luego abrir y
generar referencias a los identificadores de archivos legibles y escribibles que será devuelto por la llamada
__enter__ . El procesamiento __exit__ después de que el rendimiento cierra los controladores de archivos y restaura
el archivo desde la copia de seguridad si algo salió mal.
Tenga en cuenta que el uso de yield en un generador utilizado con el decorador @contextmanager no tiene nada
que ver con la iteración. En los ejemplos que se muestran en esta sección, la función del generador opera más
como una corrutina: un procedimiento que se ejecuta hasta un punto, luego
6. Se adoptó esta convención porque cuando se crearon los administradores de contexto, los generadores no podían devolver
valores, solo rendimiento. Ahora pueden hacerlo, como se explica en “Devolver un valor desde una rutina” en la página
475. Como verá, devolver un valor desde un generador implica una excepción.
7. Este consejo se cita literalmente de un comentario de Leonardo Rochael, uno de los revisores técnicos de este libro.
¡Bien dicho, Leo!
pends para permitir que el código del cliente se ejecute hasta que el cliente quiera que la corrutina continúe
con su trabajo. El Capítulo 16 trata sobre corrutinas.
Finalmente, revisamos las funciones en el módulo de biblioteca estándar contextlib . Uno de ellos, el decorador
@contextmanager , hace posible implementar un administrador de contexto utilizando un generador simple
con un rendimiento: una solución más sencilla que codificar una clase con al menos dos métodos.
Reimplementamos LookingGlass como una función generadora de espejos y discutimos cómo manejar
excepciones cuando usamos @contextmanager.
El decorador @contextmanager es una herramienta elegante y práctica que reúne tres características
distintivas de Python: un decorador de funciones, un generador y el con estado.
mento
Otras lecturas
El Capítulo 8, "Sentencias compuestas", en The Python Language Reference dice prácticamente todo lo que
hay que decir sobre las cláusulas else en las declaraciones if, for, while y try .
Con respecto al uso Pythonic de try/except, con o sin else, Raymond Hettinger tiene una respuesta brillante a
la pregunta "¿Es una buena práctica usar try-except-else en Python?" en StackOverflow. Python in a Nutshell,
2E (O'Reilly), de Alex Martelli , tiene un capítulo sobre excepciones con una excelente discusión sobre el estilo
EAFP, y le da crédito a la pionera informática Grace Hopper por acuñar la frase "Es más fácil pedir perdón
que permiso".
La biblioteca estándar de Python, Capítulo 4, "Tipos incorporados", tiene una sección dedicada a los tipos de
administrador de contexto. Los métodos especiales __enter__/__exit__ también están documentados en The
Python Language Reference en “3.3.8. Con Gestores de Contexto de Sentencia”. Los administradores de
contexto se introdujeron en PEP 343: la declaración "con". este PPE
no es una lectura fácil porque pasa mucho tiempo cubriendo casos de esquina y argumentando en contra
de propuestas alternativas. Esa es la naturaleza de las PEP.
Raymond Hettinger destacó la declaración with como una "característica ganadora del idioma" en su
discurso de apertura de PyCon US 2013. También mostró algunas aplicaciones interesantes de los
administradores de contexto en su charla "Transforming Code into Beautiful, Idiomatic Python" en la misma
conferencia.
La publicación de blog de Jeff Preshing "The Python with Statement by Example" es interesante para los
ejemplos que usan administradores de contexto con la biblioteca de gráficos pycairo .
Beazley y Jones idearon administradores de contexto para propósitos muy diferentes en su Python
Cookbook, 3E (O'Reilly). “Receta 8.3. Hacer que los objetos admitan el protocolo de administración de
contexto” implementa una clase LazyConnection cuyas instancias son administradores de contexto que
abren y cierran conexiones de red automáticamente con bloques.
“Receta 9.22. Definición de administradores de contexto de manera fácil” presenta un administrador de
contexto para el código de tiempo y otro para realizar cambios transaccionales en un objeto de lista :
dentro del bloque with , se realiza una copia de trabajo de la instancia de la lista y todos los cambios se
aplican a esa copia de trabajo. . Solo cuando el bloque with se completa sin excepción, la copia de trabajo
reemplaza la lista original. Sencillo e ingenioso.
Plataforma improvisada
Factorizando el pan
En su discurso de apertura de PyCon US 2013, "What Makes Python Awesome", Raymond Hettinger
dice que cuando vio por primera vez la propuesta de declaración, pensó que era "un poco arcana".
Al principio, tuve una reacción similar. Los PEP suelen ser difíciles de leer, y el PEP 343 es típico
en ese sentido.
Entonces, nos dijo Hettinger, tuvo una idea: las subrutinas son el invento más importante en la
historia de los lenguajes informáticos. Si tiene secuencias de operaciones como A;B;C y P;B;Q,
puede factorizar B en una subrutina. Es como descontar el relleno de un sándwich: usar atún con
diferentes panes. Pero, ¿y si quieres eliminar el pan, hacer sándwiches con pan de trigo, usando un
relleno diferente cada vez? Eso es lo que ofrece la sentencia with . Es el complemento de la
subrutina. Hettinger continuó diciendo:
La declaración with es muy importante. Los animo a salir y tomar esta punta del iceberg y profundizar más.
Probablemente puedas hacer cosas profundas con la declaración with .
Los mejores usos de la misma no se han descubierto todavía. Espero que si haces un buen uso de él, se copiará a
otros idiomas y todos los idiomas futuros lo tendrán. Puedes ser parte del descubrimiento de algo casi tan profundo
como la invención de la propia subrutina.
Hettinger admite que está exagerando con la declaración. Sin embargo, es una característica muy
útil. Cuando usó la analogía del sándwich para explicar cómo con es el complemento de la
subrutina, se abrieron muchas posibilidades en mi mente.
Si necesita convencer a alguien de que Python es increíble, debería ver el discurso de apertura
de Hettinger. El bit sobre los administradores de contexto es de 23:00 a 26:15. Pero todo el
keynote es excelente.
CAPÍTULO 16
corrutinas
Si los libros de Python sirven de guía, [las corrutinas son] la característica menos documentada,
oscura y aparentemente inútil de Python.
—David Beazley
autor de Python
Encontramos dos sentidos principales para el verbo “ceder” en los diccionarios: producir o ceder. Ambos
sentidos se aplican en Python cuando usamos la palabra clave yield en un generador. Una línea como
yield item produce un valor que recibe la persona que llama a next(...), y también cede, suspendiendo la
ejecución del generador para que la persona que llama pueda continuar hasta que esté listo para
consumir otro valor invocando next( ) de nuevo. La persona que llama extrae valores del generador.
Una rutina es sintácticamente como un generador: solo una función con la palabra clave yield en su
cuerpo. Sin embargo, en una corrutina, yield generalmente aparece en el lado derecho de una expresión
(por ejemplo, datum = yield), y puede o no producir un valor; si no hay una expresión después de la
palabra clave yield , el generador produce Ninguno . . La corrutina puede recibir datos de la persona
que llama, que utiliza .send(datum) en lugar de next(…) para alimentar la corrutina. Por lo general, la
persona que llama inserta valores en la rutina.
Incluso es posible que no entren ni salgan datos a través de la palabra clave yield . Independientemente
del flujo de datos, el rendimiento es un dispositivo de flujo de control que se puede utilizar para
implementar multitareas cooperativas: cada corrutina cede el control a un programador central para que
se puedan activar otras corrutinas.
Las corrutinas de Python son el producto de una serie de mejoras a las humildes funciones del generador
que hemos visto hasta ahora en el libro. Seguir la evolución de las corrutinas en Python ayuda a
comprender sus funciones en etapas de mayor funcionalidad y complejidad.
463
Machine Translated by Google
Después de una breve descripción de cómo se permitió que los generadores actuaran como una rutina, saltamos
al núcleo del capítulo. Entonces veremos:
La infraestructura para coroutines apareció en PEP 342 — Coroutines via Enhanced Generators, implementado en
Python 2.5 (2006): desde entonces, la palabra clave yield se puede usar en una expresión y se agregó el
método .send(value) a la API del generador.
Usando .send(…), la persona que llama al generador puede publicar datos que luego se convierten en el valor de
la expresión de rendimiento dentro de la función del generador. Esto permite que un generador se use como una
corrutina: un procedimiento que colabora con la persona que llama, entregando y recibiendo valores de la persona
que llama.
Además de .send(...), PEP 342 también agregó los métodos .throw(...) y .close() que, respectivamente, permiten a
la persona que llama lanzar una excepción para que se maneje dentro del generador y terminarla. Estas
características se tratan en la siguiente sección y en “Terminación de corrutina y manejo de excepciones” en la
página 471.
El último paso evolutivo para coroutines vino con PEP 380 - Sintaxis para delegar a un subgenerador, implementado
en Python 3.3 (2012). PEP 380 realizó dos cambios de sintaxis en las funciones del generador, para hacerlas más
útiles como rutinas:
• El rendimiento de la sintaxis permite que los generadores complejos se refactoricen en generadores anidados
más pequeños, al mismo tiempo que se evita una gran cantidad de código repetitivo que antes se requería
para que un generador delegara a los subgeneradores.
Estos cambios más recientes se abordarán en "Devolución de un valor de una rutina" en la página 475 y "Uso de
yield from" en la página 477.
Sigamos la tradición establecida de Fluent Python y comencemos con algunos hechos y ejemplos muy básicos,
luego pasemos a características cada vez más alucinantes.
Una rutina se define como una función generadora: con rendimiento en su cuerpo.
el rendimiento se usa en una expresión; cuando la rutina está diseñada solo para recibir
datos del cliente que produce Ninguno: esto está implícito porque no hay expresión
a la derecha de la palabra clave yield .
Como es habitual con los generadores, llamas a la función para recuperar un objeto generador.
La primera llamada es la siguiente (...) porque el generador no se ha iniciado, por lo que no está esperando
en un rendimiento y no podemos enviarle ningún dato inicialmente.
Esta llamada hace que el rendimiento en el cuerpo de la rutina se evalúe en 42; ahora la rutina
se reanuda y corre hasta el próximo rendimiento o terminación.
En este caso, el control sale del final del cuerpo de la rutina, lo que provoca que el
Maquinaria generadora para plantear StopIteration, como de costumbre.
Una rutina puede estar en uno de cuatro estados. Puede determinar el estado actual usando el
inspect.getgeneratorstate(…) , que devuelve una de estas cadenas:
'GEN_CREATED'
'GEN_EN EJECUCIÓN'
1. Solo verá este estado en una aplicación de subprocesos múltiples, o si el objeto generador llama a getgenerator
Estado en sí mismo, que no es útil.
'GEN_SUSPENDIDO'
Actualmente suspendido en una expresión de rendimiento .
'GEN_CERRADO'
La ejecución se ha completado.
Debido a que el argumento del método de envío se convertirá en el valor de la expresión de rendimiento pendiente , se
deduce que solo puede realizar una llamada como my_coro.send(42) si la rutina está actualmente suspendida. Pero ese no
es el caso si la rutina nunca se ha activado, cuando su estado es 'GEN_CREATED'. Es por eso que la primera activación de
una rutina siempre se realiza con next(my_coro); también puede llamar a my_coro.send(None), y el efecto es el mismo.
Si crea un objeto de rutina e inmediatamente intenta enviarle un valor que no es Ninguno, esto es lo que sucede:
La llamada inicial next(my_coro) a menudo se describe como "preparar" la corrutina (es decir, avanzarla al primer rendimiento
para que esté lista para usar como una corrutina en vivo).
Para tener una mejor idea del comportamiento de una rutina, es útil un ejemplo que produzca más de una vez. Vea el Ejemplo
16-2.
>>> mi_coro2.send(99)
-> Recibido: c = 99
Rastreo (última llamada más reciente):
Archivo "<stdin>", línea 1, en <módulo>
StopIteration
>>> getgeneratorstate(my_coro2)
'GEN_CERRADO'
Avanzar la rutina hasta el primer rendimiento, imprimir -> Iniciado: a = 14 mensaje, luego
generar el valor de a y suspender para esperar a que se asigne el valor a b.
getgeneratorstate informa GEN_SUSPENDED (es decir, la corrutina se detiene en una
expresión de rendimiento ).
1. next(my_coro2) imprime el primer mensaje y se ejecuta para generar a, lo que genera el número
Figura 16-1. Tres fases en la ejecución de la rutina simple_coro2 (tenga en cuenta que cada
fase termina en una expresión de rendimiento, y la siguiente fase comienza en la misma línea,
cuando el valor de la expresión de rendimiento se asigna a una variable)
promedio = Ninguno
mientras es Verdadero:
término = rendimiento
promedio total += término
cuenta += 1
promedio = total/cuenta
Este ciclo infinito significa que esta corrutina seguirá aceptando valores y produciendo
resultados mientras la persona que llama los envíe. Esta corrutina solo terminará cuando
la persona que llama llame a .close() , o cuando se recolecte basura porque no hay más
referencias a ella.
2. Este ejemplo está inspirado en un fragmento de Jacob Holm en la lista de ideas de Python, mensaje titulado "Rendimiento de:
garantías de finalización". Algunas variaciones aparecen más adelante en el hilo, y Holm explica con más detalle su pensamiento
en el mensaje 003912.
La declaración de rendimiento aquí se usa para suspender la corrutina, producir un resultado para la persona
que llama y, más tarde, obtener un valor enviado por la persona que llama a la corrutina, que reanuda su
ciclo infinito.
La ventaja de usar una rutina es que total y count pueden ser variables locales simples: no se necesitan atributos de
instancia ni cierres para mantener el contexto entre llamadas.
El ejemplo 16-4 son pruebas documentales para mostrar la rutina del promedio en funcionamiento.
Ejemplo 16-4. coroaverager0.py: doctest para la rutina promedio móvil en el Ejemplo 16-3
>>> coro_promedio.enviar(5)
15.0
Ahora estamos en el negocio: cada llamada a .send (...) produce el promedio actual.
En el doctest (Ejemplo 16-4), la llamada next(coro_avg) hace que la corrutina avance al rendimiento, arrojando el
valor inicial de promedio, que es Ninguno, por lo que no aparece en la consola. En este punto, la rutina se suspende
en el rendimiento, esperando que se envíe un valor. La línea coro_avg.send(10) proporciona ese valor, lo que hace
que se active la corrutina, lo asigna a un término, actualiza las variables total, conteo y promedio , y luego inicia otra
iteración en el ciclo while , que arroja el promedio y espera otro término.
El lector atento puede estar ansioso por saber cómo se puede terminar la ejecución de una instancia de promediador
(por ejemplo, coro_avg) , porque su cuerpo es un bucle infinito. Cubriremos eso en “Terminación de la rutina y manejo
de excepciones” en la página 471.
Pero antes de discutir la terminación de rutinas, hablemos de cómo comenzar. Cebar una rutina antes de usarla es
una tarea necesaria pero fácil de olvidar. Para evitarlo, se puede aplicar un decorador especial a la rutina. Uno de
esos decoradores se presenta a continuación.
def coroutine(func):
"""Decorator: prima `func` avanzando al primer `yield`"""
@wraps(función)
imprimación def (* argumentos, ** kwargs):
gen = func(*args,**kwargs) next(gen)
return gen return primer
Cebe el generador.
Devolverlo.
El ejemplo 16-6 muestra el decorador @coroutine en uso. Contraste con el ejemplo 16-3.
Ejemplo 16-6. coroaverager1.py: doctest y código para una corrutina promedio móvil usÿ
usando el decorador @coroutine del Ejemplo 16-5
"""
"""
@corutina
3. Hay varios decoradores similares publicados en la Web. Este está adaptado de la receta ActiveState.
Pipeline hecho de rutinas por Chaobin Tang, quien a su vez acredita a David Beazley.
def promediador():
total = 0.0
cuenta = 0
promedio = Ninguno
mientras que es Verdadero:
término = rendimiento
promedio total += término
contar += 1
promedio = total/recuento
Llame a averager(), creando un objeto generador que está preparado dentro de la función de
imprimación del decorador coroutine .
getgeneratorstate informa GEN_SUSPENDED, lo que significa que la rutina está lista para recibir un
valor.
Inmediatamente puede comenzar a enviar valores a coro_avg: ese es el objetivo del decorador.
Varios marcos proporcionan decoradores especiales diseñados para trabajar con rutinas. No todos preparan
realmente la corrutina; algunos brindan otros servicios, como conectarlo a un bucle de eventos. Un ejemplo
de la biblioteca de redes asíncronas Tornado es el decorador tornado.gen .
La sintaxis yield from que veremos en “Uso de yield from” en la página 477 prepara automáticamente la
corrutina que llama, haciéndola incompatible con decoradores como @coroutine del Ejemplo 16-5. El
decorador asyncio.coroutine de la biblioteca estándar de Python 3.4 está diseñado para funcionar con
rendimiento, por lo que no prepara la rutina. Lo cubriremos en el Capítulo 18.
Ahora nos centraremos en las características esenciales de las corrutinas: los métodos que se usan para
40,0
>>> coro_promedio.send(50)
45,0
>>> coro_avg.send('spam') # Rastreo
(última llamada más reciente):
...
TypeError: tipo(s) de operando no admitidos para +=: 'float' y 'str' >>>
coro_avg.send(60) # Traceback (última llamada más reciente):
La causa del error fue el envío de un valor 'spam' que no se pudo sumar a la variable total en la
rutina.
El ejemplo 16-7 sugiere una forma de terminar las corrutinas: puede usar send con algún valor de
centinela que le diga a la corrutina que salga. Los singletons incorporados constantes como Ninguno
y Ellipsis son valores centinela convenientes. Los puntos suspensivos tienen la ventaja de ser
bastante inusuales en los flujos de datos. Otro valor centinela que he visto que se usa es StopIteration:
la clase en sí, no una instancia de ella (y no generarla). En otras palabras, usándolo como: my_co
ro.send(StopIteration).
Desde Python 2.5, los objetos generadores tienen dos métodos que permiten al cliente enviar
explícitamente excepciones a la rutina: lanzar y cerrar:
generador.cerrar()
Hace que la expresión de rendimiento donde se pausó el generador genere una excepción de
salida del generador. No se informa ningún error a la persona que llama si el generador no
maneja esa excepción o genera StopIteration, generalmente ejecutándose hasta completarse.
Al recibir un GeneratorExit, el generador no debe generar un valor, de lo contrario, se genera
un Run timeError . Si el generador lanza cualquier otra excepción, se propaga a la persona que
llama.
Veamos qué tan cerca y cómo controlar una rutina. El ejemplo 16-8 enumera la función
demo_exc_handling utilizada en los siguientes ejemplos.
Ejemplo 16-8. coro_exc_demo.py: código de prueba para estudiar el manejo de excepciones en una
rutina
class DemoException(Exception):
"""Un tipo de excepción para la demostración."""
def demo_exc_handling():
print('-> rutina iniciada') while True:
pruebe: x =
rendimiento excepto
DemoException: print('*** DemoException manejada.
Continuando...') else: print('-> rutina recibida: {!r}'.format(x))
La última línea del Ejemplo 16-8 es inalcanzable porque el ciclo infinito solo se puede cancelar con
una excepción no controlada, y eso finaliza la corrutina de inmediato.
'GEN_CERRADO'
>>> exc_coro.send(11)
-> rutina recibida: 11 >>>
exc_coro.throw(DemoException)
*** DemoException manejada. Continuando...
>>> getgeneratorstate(exc_coro)
'GEN_SUSPENDIDO'
Por otro lado, si se lanza una excepción no controlada a la rutina, se detiene; su estado se
convierte en 'GEN_CLOSED'. El ejemplo 16-11 lo demuestra.
Si es necesario que se ejecute algún código de limpieza sin importar cómo termine la corrutina,
debe envolver la parte relevante del cuerpo de la corrutina en un bloque try/finally , como en
el Ejemplo 16-12.
Ejemplo 16-12. coro_finally_demo.py: uso de try/ finally para realizar acciones al terminar la
rutina
class DemoException(Exception):
"""Un tipo de excepción para la demostración."""
def demo_finally():
print('-> corrutina iniciada') try:
while True:
pruebe: x =
rendimiento excepto
DemoException: print('*** DemoException manejada. Continuando...')
más:
Una de las razones principales por las que se agregó el rendimiento de la construcción a Python 3.3
tiene que ver con lanzar excepciones en corrutinas anidadas. La otra razón era permitir que las
corrutinas devolvieran valores de manera más conveniente. Siga leyendo para ver cómo.
El resultado devuelto por el promediador en el ejemplo 16-13 es una tupla con nombre con el número
de términos promediados (recuento) y el promedio. Podría haber devuelto solo el valor promedio , pero
devolver una tupla expone otro dato interesante que se acumuló: el recuento de términos.
Ejemplo 16-13. coroaverager2.py: código para una rutina de promedio que devuelve un resultado
def promediador():
total = 0.0
conteo = 0
promedio = Ninguno
mientras es Verdadero:
término =
rendimiento si el término es Ninguno:
ruptura
total += término
contar += 1
promedio = total/recuento
resultado de retorno (recuento, promedio)
Para devolver un valor, una rutina debe terminar normalmente; esta es la razón por la que esta
versión de promediador tiene una condición para salir de su ciclo de acumulación.
Devuelve una tupla con nombre con el recuento y el promedio. Antes de Python 3.3, era un
error de sintaxis devolver un valor en una función de generador.
Para ver cómo funciona este nuevo promediador , podemos controlarlo desde la consola, como en el
Ejemplo 16-14.
Enviar Ninguno finaliza el ciclo, lo que hace que la corrutina finalice al devolver el resultado. Como
de costumbre, el objeto generador genera StopIteration. El atributo de valor de la excepción lleva el
valor devuelto.
Tenga en cuenta que el valor de la expresión de retorno se pasa de contrabando a la persona que llama
como un atributo de la excepción StopIteration . Esto es un truco, pero conserva el comportamiento existente
de los objetos generadores: elevar StopIteration cuando se agota.
Ejemplo 16-15. La captura de StopIteration nos permite obtener el valor devuelto por el promediador
Esta forma indirecta de obtener el valor de retorno de una rutina tiene más sentido cuando nos damos cuenta
de que se definió como parte de PEP 380, y el rendimiento de la construcción lo maneja automáticamente al
capturar StopIteration internamente. Esto es análogo al uso de StopIteration en bucles for : la excepción es
manejada por la maquinaria del bucle de una manera que es transparente para el usuario. En el caso de
yield from, el intérprete no solo consume la StopIteration, sino que su atributo de valor se convierte en el
valor de la propia expresión yield from . Desafortunadamente, no podemos probar esto de forma interactiva
en la consola, ya que
porque es un error de sintaxis usar yield from (o yield, para el caso) fuera de una función.4
La siguiente sección tiene un ejemplo en el que se usa la rutina del promedio con yield from para producir un
resultado, como se pretende en PEP 380. Así que abordemos yield from.
Usando el rendimiento de
Lo primero que debe saber sobre yield from es que es una construcción de lenguaje completamente nueva. Hace
mucho más que producir que la reutilización de esa palabra clave sea posiblemente engañosa. Construcciones
similares en otros lenguajes se llaman await, y ese es un nombre mucho mejor porque transmite un punto crucial:
cuando un generador gen llama a yield desde subgen(), el subgen toma el control y entregará valores a la persona
que llama a gen; la persona que llama, en efecto, impulsará subgen directamente. Mientras tanto , gen se bloqueará,
esperando hasta que finalice subgen.5
Hemos visto en el Capítulo 14 que yield from puede usarse como un atajo para yield en un bucle for . Por ejemplo,
esto:
4. Hay una extensión de iPython llamada ipython-yf que permite evaluar el rendimiento directamente desde la consola de
iPython. Se utiliza para probar código asíncrono y funciona con asyncio. Se envió como un parche para Python 3.5 pero no
fue aceptado. Consulte el Problema n.º 22412: Hacia una línea de comando habilitada para asyncio en el rastreador de
errores de Python.
5. Mientras escribo esto, hay un PEP abierto que propone agregar las palabras clave await y async: PEP 492 —
Corrutinas con sintaxis async y await.
Cuando mencionamos por primera vez yield from en “Nueva sintaxis en Python 3.3: yield from” en la página
433, el código del Ejemplo 16-16 demuestra un uso práctico para él.6
Un ejemplo de rendimiento un poco más complicado, pero más útil, se encuentra en la “Receta 4.14. Flattening
a Nested Sequence” en Python Cookbook de Beazley y Jones, 3E (código fuente disponible en GitHub).
Lo primero que hace la expresión yield from x con el objeto x es llamar a iter(x) para obtener un iterador de él.
Esto significa que x puede ser iterable.
Sin embargo, si reemplazar los bucles for anidados que arrojan valores fuera la única contribución de yield, esta
adición de lenguaje no habría tenido buenas posibilidades de ser aceptada.
La naturaleza real de yield from no se puede demostrar con simples iterables; requiere el uso alucinante de
generadores anidados. Es por eso que PEP 380, que introdujo el rendimiento de, se titula "Sintaxis para delegar
a un subgenerador".
La característica principal de yield from es abrir un canal bidireccional desde la persona que llama más externa
hasta el subgenerador más interno, de modo que los valores se puedan enviar y generar de un lado a otro
directamente desde ellos, y las excepciones se pueden lanzar hasta el final sin agregar una gran cantidad de
código repetitivo de manejo de excepciones en las rutinas intermedias. Esto es lo que permite la delegación de
rutinas de una manera que antes no era posible.
El uso de yield from requiere una disposición de código no trivial. Para hablar sobre las partes móviles
requeridas, PEP 380 usa algunos términos de una manera muy específica: generador delegado La función
subgenerator El
generador obtenido de la parte <iterable> del rendimiento de la expresión.
Este es el “subgenerador” mencionado en el título de PEP 380: “Sintaxis para Delegar a un Subgenerador”.
6. El ejemplo 16-16 es solo un ejemplo didáctico. El módulo itertools ya proporciona una función de cadena optimizada
escrito en c
llamador PEP 380 utiliza el término "llamador" para referirse al código de cliente que llama al
generador delegador. Dependiendo del contexto, uso "cliente" en lugar de "llamador", para
distinguirlo del generador delegado, que también es un "llamador" (llama al subgenerador).
El ejemplo 16-17 proporciona más contexto para ver el rendimiento en el trabajo, y la figura 16-2
identifica las partes relevantes del ejemplo.7
Figura 16-2. Mientras que el generador delegado está suspendido en el rendimiento de, la persona
que llama envía datos directamente al subgenerador, que devuelve los datos a la persona que
llama. El generador de delegación se reanuda cuando el subgenerador regresa y el intérprete
genera StopIteration con el valor devuelto adjunto.
El script coroaverager3.py lee un dictado con pesos y alturas de niñas y niños en una clase
imaginaria de séptimo grado. Por ejemplo, la clave 'boys;m' corresponde a las alturas de 9 boys,
en metros; 'niñas;kg' son los pesos de 10 niñas en kilogramos. El script introduce los datos de
cada grupo en la rutina promedio que hemos visto antes, y produce un informe como este:
$ python3 coroaverager3.py 9
niños con un promedio de 40,42 kg
# el subgenerador
def promediador():
total = 0.0
cuenta = 0
promedio = Ninguno
mientras que es cierto:
término =
rendimiento si el término
es Ninguno: romper
total += término
contar += 1
promedio = total/recuento
resultado devuelto (recuento, promedio)
# el generador delegador
def grouper(resultados, clave): while
True:
resultados[clave] = rendimiento de promediador()
def principal(datos):
resultados = {}
para clave, valores en data.items():
grupo = agrupador (resultados, clave)
siguiente (grupo) para valor en valores:
group.send(valor)
group.send(Ninguno) # ¡importante!
# informe de salida
datos =
{ 'chicas;kg':
[40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5], 'chicas;m': [1.6,
1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43] 'chicos;kg': [39.0, 40.8, 43.2,
40.8, 43.1, 38.6, 41.4, 40.6, 36.3], 'chicos;m': [1.38, 1.5, 1.32, 1.25, 1.37,
1.48, 1.25, 1.49, 1.46 ],
if __nombre__ == '__principal__':
principal(datos)
Cada valor enviado por el código de cliente en main estará vinculado al término aquí.
La condición final crucial. Sin él, el rendimiento de llamar a esta rutina se bloqueará para siempre.
el generador de delegación.
Cada iteración en este ciclo crea una nueva instancia de promedio; cada uno es un objeto
generador que funciona como una rutina.
Cada vez que se envía un valor al mero , se canaliza a la instancia del promedio por el
rendimiento de. grouper se suspenderá aquí siempre que la instancia de promedio esté
consumiendo valores enviados por el cliente. Cuando una instancia de promediador se ejecuta
hasta el final, el valor que devuelve está vinculado a los resultados [clave]. El ciclo while luego
procede a crear otra instancia de promedio para consumir más valores. main es el código de
cliente, o "persona que llama" en el lenguaje PEP 380. Esta es la función que impulsa todo.
group es un objeto generador que resulta de llamar a grouper con el dictado de resultados para
recopilar los resultados y una clave particular. Funcionará como una rutina.
Prepara la rutina.
Envía cada valor al agrupador. Ese valor termina en el término = línea de rendimiento del
promediador; el mero nunca tiene la oportunidad de verlo.
Enviar Ninguno al agrupador hace que la instancia del promediador actual finalice y permite que el
agrupador se ejecute nuevamente, lo que crea otro promediador para el siguiente grupo de valores.
La última llamada en el Ejemplo 16-17 con el comentario "¡importante!" destaca una línea de código crucial:
group.send(None), que finaliza un promediador y comienza el siguiente. Si comenta esa línea, el script no
produce ningún resultado. Quitar el comentario de la línea de impresión (resultados) cerca del final de main
revela que el dictado de resultados termina vacío.
Aquí hay una descripción general de cómo funciona el Ejemplo 16-17 , que explica lo que sucedería si
omitiéramos la llamada group.send (Ninguno) marcada como "importante". en principal:
• Cada iteración del bucle for externo crea una nueva instancia de agrupador denominada grupo ;
este es el generador de delegación.
• La llamada next(group) prepara el generador de delegación del agrupador, que entra en su ciclo while
True y se suspende en el yield from, después de llamar al subgenerador .
promediador
• El bucle for interno llama a group.send(value); esto alimenta directamente al promediador del
subgenerador . Mientras tanto, la instancia de grupo actual de mero se suspende en el rendimiento de.
• Cuando finaliza el ciclo for interno , la instancia del grupo aún está suspendida en el rendimiento de,
por lo que la asignación a resultados[clave] en el cuerpo del agrupador aún no se ha realizado.
• Sin el último group.send(None) en el bucle for externo , el subgenerador de promediador nunca finaliza,
el grupo generador de delegación nunca se reactiva y la asignación a results[key] nunca ocurre.
• Cuando la ejecución vuelve a la parte superior del bucle for externo, se crea una nueva instancia de
agrupador y se vincula al grupo . La instancia de agrupador anterior se recolecta como basura (junto
con su propia instancia de subgenerador de promediador sin terminar).
El ejemplo 16-17 demuestra el arreglo más simple de yield from, con solo un generador delegado y
un subgenerador. Debido a que el generador de delegación funciona como una canalización, puede
conectar cualquier número de ellos en una canalización: un generador de delegación usa yield from
para llamar a un subgenerador, que a su vez es un generador de delegación que llama a otro
subgenerador con yield from, y así sucesivamente. Eventualmente, esta cadena debe terminar en un
generador simple que use solo yield, pero también puede terminar en cualquier objeto iterable, como
en el Ejemplo 16-16.
Cada rendimiento de la cadena debe ser impulsado por un cliente que llame a next(...) o .send(...) en
el generador de delegación más externo. Esta llamada puede ser implícita, como un bucle for .
Ahora revisemos la descripción formal del rendimiento de la construcción, tal como se presenta en
PEP 380.
El significado de rendimiento de
Mientras desarrollaba PEP 380, Greg Ewing, el autor, fue cuestionado sobre la complejidad de la
semántica propuesta. Una de sus respuestas fue "Para los humanos, casi toda la información
importante está contenida en un párrafo cerca de la parte superior". Luego citó parte del borrador del
PEP 380 que en su momento decía así:
La versión aprobada del PEP 380 explica el comportamiento del rendimiento a partir de seis puntos
en la sección Propuesta. Los reproduzco casi exactamente aquí, excepto que reemplacé cada
aparición de la palabra ambigua "iterador" con "subgenerador" y agregué algunas aclaraciones. El
ejemplo 16-17 ilustra estos cuatro puntos:
8. Mensaje a Python-Dev: “PEP 380 (rendimiento de un subgenerador) comentarios” (21 de marzo de 2009).
• Cualquier valor que produzca el subgenerador se pasa directamente a la persona que llama
del generador delegador (es decir, el código de cliente). • Todos los valores enviados al
Las otras dos características de yield from tienen que ver con las excepciones y la terminación:
close() del generador de delegación, se llama al método close() del subgenerador, si lo tiene. Si
esta llamada da como resultado una excepción, se propaga al generador de delegación. De
lo contrario, GeneratorExit se genera en el generador de delegación.
La semántica detallada de yield from es sutil, especialmente los puntos relacionados con las
excepciones. Greg Ewing hizo un gran trabajo poniéndolos en palabras en inglés en PEP 380.
Ewing también documentó el comportamiento del rendimiento al usar pseudocódigo (con sintaxis
de Python). Personalmente, encontré útil dedicar algún tiempo a estudiar el pseudocódigo en PEP
380. Sin embargo, el pseudocódigo tiene 40 líneas y no es tan fácil de entender al principio.
Una buena manera de abordar ese pseudocódigo es simplificarlo para manejar solo el caso de uso
más básico y común de yield from.
El ejemplo 16-17 es un guión donde se cumplen esos supuestos simplificadores. De hecho, en mucho
código de la vida real, se espera que el generador de delegación se ejecute hasta su finalización. Así que veamos cómo
rendimiento de las obras en este mundo más feliz y más simple.
Eche un vistazo al ejemplo 16-18, que es una expansión de esta declaración única, en el
cuerpo del generador delegante:
_i = iter(EXPR) prueba:
_y = siguiente(_i)
excepto StopIteration como _e:
_r = _e.valor más:
mientras que 1:
_s = rendimiento _y
prueba:
_y = _i.send(_s) excepto
StopIteration como _e: _r = _e.value
descanso
RESULTADO = _r
El EXPR puede ser iterable, porque iter() se aplica para obtener un iterador _i (este
es el subgenerador).
El subgenerador está cebado; el resultado se almacena para ser el primer valor producido _y.
Mientras se ejecuta este ciclo, el generador de delegación está bloqueado, operando solo
como un canal entre la persona que llama y el subgenerador.
Rendimiento el elemento actual producido por el subgenerador; esperar un valor _s enviado por
llamador. Tenga en cuenta que este es el único rendimiento en este listado.
Intente avanzar el subgenerador, reenviando los _s enviados por la persona que llama.
En este pseudocódigo simplificado, conservé los nombres de las variables utilizadas en el pseudocódigo
publicado en PEP 380. Las variables son: _i (iterador)
El subgenerador
_y (producido)
Un valor producido por el subgenerador
_r (resultado)
El resultado final (es decir, el valor del rendimiento de la expresión cuando finaliza el subgenerador)
_s (enviado)
Un valor enviado por la persona que llama al generador delegado, que se reenvía al subgenerador
_e (excepción)
Una excepción (siempre una instancia de StopIteration en este pseudocódigo simplificado)
Además de no manejar .throw(...) y .close(), el pseudocódigo simplificado siempre usa .send(...) para reenviar las
llamadas next() o .send(...) del cliente al subgenerador.
No se preocupe por estas finas distinciones en una primera lectura. Como se mencionó, el ejemplo 16-17
funcionaría perfectamente bien si el rendimiento de hiciera solo lo que se muestra en el pseudocódigo simplificado
del ejemplo 16-18.
Pero la realidad es más complicada, debido a la necesidad de manejar las llamadas .throw(...) y .close() del
cliente, que deben pasarse al subgenerador. Además, el subgenerador puede ser un iterador simple que no
admita .throw(...) o .close(), por lo que esto debe ser manejado por el rendimiento de la lógica. Si el subgenerador
implementa esos métodos, dentro del subgenerador ambos métodos provocan que se generen excepciones, que
también deben ser manejadas por el rendimiento de la maquinaria. El subgenerador también puede lanzar sus
propias excepciones, no provocadas por la persona que llama, y esto también debe tratarse en el rendimiento de
la implementación. Finalmente, como una optimización, si la persona que llama llama a next(...) o .send(Ninguno),
ambos se reenvían como una llamada next(...) en el subgenerador; solo si la persona que llama envía un valor
que no es Ninguno , se utiliza el método .send(...) del subgenerador.
Nuevamente, el código que se muestra en el Ejemplo 16-19 es una expansión de esta declaración única, en el
cuerpo del generador de delegación:
mientras que 1:
probar:
_s = rendimiento
_y excepto GeneratorExit como _e:
intente:
_m = _i.cerrar
excepto AttributeError:
pasar
más:
_metro()
subir _e
excepto BaseException como _e: _x
= sys.exc_info()
probar:
_m = _i.tirar
excepto AttributeError:
subir _e
más:
probar:
_y = _m(*_x)
excepto StopIteration como _e:
_r = _e.valor
descanso
más:
intente: si _s es ninguno:
_y = siguiente (_i)
más:
_y = _i.send(_s)
excepto StopIteration como _e: _r =
_e.value
descanso
RESULTADO = _r
El EXPR puede ser iterable, porque iter() se aplica para obtener un iterador _i (este
es el subgenerador).
El subgenerador está cebado; el resultado se almacena para ser el primer valor producido _y.
Mientras se ejecuta este ciclo, el generador delegador está bloqueado, operando solo como un canal
entre la persona que llama y el subgenerador.
Rendimiento el elemento actual producido por el subgenerador; espere un valor _s enviado por la
persona que llama. Este es el único rendimiento en este listado.
Esto se ocupa de las excepciones lanzadas por la persona que llama usando .throw (...). Una vez
más, el subgenerador puede ser un iterador sin ningún método de lanzamiento para llamar, en cuyo
caso la excepción se genera en el generador delegado.
Si el subgenerador tiene un método throw , llámelo con la excepción pasada por la persona que llama.
El subgenerador puede manejar la excepción (y el ciclo continúa); puede generar StopIteration (el
resultado _r se extrae de él y el bucle finaliza); o puede generar la misma u otra excepción, que no
se maneja aquí y se propaga al generador delegador.
Llame a continuación en el subgenerador si el último valor recibido de la persona que llama fue Ninguno; de
lo contrario, llame a enviar.
Si el subgenerador generó StopIteration, obtenga el valor, asígnelo a _r y salga del ciclo, reanudando
el generador delegado. _r es el RESULTADO: el valor del rendimiento total de la expresión.
La mayor parte de la lógica del rendimiento del pseudocódigo se implementa en seis bloques try/except
anidados hasta cuatro niveles de profundidad, por lo que es un poco difícil de leer. Las únicas otras palabras
clave de flujo de control utilizadas son one while, one if y one yield. Encuentra las llamadas while, yield,
next(...) y .send (...) : te ayudarán a tener una idea de cómo funciona toda la estructura.
Justo en la parte superior del ejemplo 16-19, un detalle importante revelado por el pseudocódigo es que el
subgenerador está cebado (segunda llamada en el ejemplo 16-19).9 ” en la página 469 son incompatibles con
yield from.
En el mismo mensaje que cité al comienzo de esta sección, Greg Ewing dice lo siguiente sobre la expansión
del pseudocódigo de yield from:
9. En un mensaje a Python-ideas el 5 de abril de 2009, Nick Coghlan cuestionó si el cebado implícito realizado
por yield from era una buena idea.
No está destinado a conocerlo leyendo la expansión; solo está ahí para precisar todos los
detalles para los abogados de idiomas.
Centrarse en los detalles de la expansión del pseudocódigo puede no ser útil, dependiendo de su estilo
de aprendizaje. Estudiar código real que utiliza yield from es sin duda más rentable que estudiar
minuciosamente el pseudocódigo de su implementación. Sin embargo, casi todo el rendimiento de los
ejemplos que he visto está relacionado con la programación asíncrona con el módulo asíncrono , por lo
que dependen de un ciclo de eventos activo para funcionar. Veremos yield de numerosas veces en el
Capítulo 18. Hay algunos enlaces en “Lectura adicional” en la página 500 a código interesante que usa
yield from sin un ciclo de eventos.
En esta sección, describiré una simulación muy simple implementada utilizando solo rutinas y objetos
de biblioteca estándar. La simulación es una aplicación clásica de las corrutinas en la literatura
informática. Simula, el primer lenguaje OO, introdujo el concepto de corrutinas precisamente para
soportar simulaciones.
Una simulación de eventos discretos (DES) es un tipo de simulación donde un sistema se modela como
una secuencia de eventos. En un DES, el “reloj” de simulación no avanza en incrementos fijos, sino
que avanza directamente al tiempo simulado del siguiente evento modelado. Por ejemplo, si estamos
simulando la operación de un taxi desde una perspectiva de alto nivel, uno
evento es recoger a un pasajero, el siguiente es dejar al pasajero. No importa si un viaje dura 5 o 50 minutos:
cuando ocurre el evento de entrega, el reloj se actualiza a la hora de finalización del viaje en una sola operación.
En un DES, podemos simular un año de viajes en taxi en menos de un segundo. Esto contrasta con una
simulación continua en la que el reloj avanza continuamente en un incremento fijo y, por lo general, pequeño.
Intuitivamente, los juegos por turnos son ejemplos de simulaciones de eventos discretos: el estado del juego
solo cambia cuando un jugador se mueve, y mientras un jugador decide el siguiente movimiento, el reloj de la
simulación se congela. Los juegos en tiempo real, por otro lado, son simulaciones continuas en las que el reloj
de simulación está funcionando todo el tiempo, el estado del juego se actualiza muchas veces por segundo y los
jugadores lentos están en verdadera desventaja.
Ambos tipos de simulaciones se pueden escribir con varios subprocesos o con un solo subproceso utilizando
técnicas de programación orientadas a eventos, como devoluciones de llamadas o corrutinas impulsadas por un
bucle de eventos. Podría decirse que es más natural implementar una simulación continua utilizando subprocesos
para dar cuenta de las acciones que suceden en paralelo en tiempo real. Por otro lado, las rutinas ofrecen
exactamente la abstracción correcta para escribir un DES. SimPy11 es un paquete DES para Python que usa
una rutina para representar cada proceso en la simulación.
Si está interesado en simulaciones, vale la pena estudiar SimPy. Sin embargo, en esta sección, describiré un
DES muy simple implementado utilizando solo funciones de biblioteca estándar. Mi objetivo es ayudarte a
desarrollar una intuición sobre la programación de acciones concurrentes con rutinas. Comprender la siguiente
sección requerirá un estudio cuidadoso, pero la recompensa vendrá como información sobre cómo las bibliotecas
como asyncio, Twisted y Tornado pueden administrar muchas actividades simultáneas utilizando un solo hilo de
ejecución.
En nuestro programa de simulación, taxi_sim.py, se crean varios taxis. Cada uno hará un número fijo de viajes y
luego se irá a casa. Un taxi sale del garaje y comienza a "merodear", en busca de un pasajero. Esto dura hasta
que se recoge a un pasajero y comienza un viaje. Cuando se deja al pasajero, el taxi vuelve a merodear.
El tiempo transcurrido durante las rondas y los viajes se genera mediante una distribución exponencial.
Para una visualización más limpia, los tiempos están en minutos enteros, pero la simulación también funcionaría.
11. Consulte la documentación oficial de Simpy, que no debe confundirse con la conocida pero no relacionada SymPy, una
biblioteca de matemáticas simbólicas.
utilizando intervalos flotantes.12 Cada cambio de estado en cada cabina se informa como un evento.
La figura 16-3 muestra un ejemplo de ejecución del programa.
Figura 16-3. Ejemplo de ejecución de taxi_sim.py con tres taxis. El argumento -s 3 establece la semilla del
generador aleatorio para que las ejecuciones del programa puedan reproducirse para depuración y
demostración. Las flechas de colores resaltan los viajes en taxi.
Lo más importante a tener en cuenta en la figura 16-3 es el intercalado de los viajes de los tres taxis. Agregué
manualmente las flechas para que sea más fácil ver los viajes en taxi: cada flecha
12. No soy un experto en operaciones de flotas de taxis, así que no tome en serio mis números. Las distribuciones
exponenciales se usan comúnmente en DES. Verás algunos viajes muy cortos. Solo imagine que es un día lluvioso y que
algunos pasajeros toman taxis solo para dar la vuelta a la cuadra, en una ciudad ideal donde hay taxis cuando llueve.
comienza cuando se recoge a un pasajero y termina cuando se deja al pasajero. Intuitivamente, esto demuestra
cómo se pueden usar corrutinas para administrar actividades concurrentes.
• Cada taxi sale del garaje 5 minutos después que el otro. • El taxi 0
• El taxista del taxi 0 sólo realiza dos viajes (flechas moradas): el primero comienza en el tiempo=2 y finaliza
en el tiempo=18; el segundo comienza en el tiempo = 28 y termina en el tiempo = 65, el viaje más largo en
esta ejecución de simulación.
• El taxi 1 hace cuatro viajes (flechas verdes) y luego se va a casa a la hora=110. • El taxi 2
hace seis viajes (flechas rojas) y luego se va a casa a la hora=109. Su último viaje dura solo un minuto,
13
comenzando en el tiempo = 97. • Mientras el taxi 1 hace su primer viaje, comenzando en el tiempo=8, el
taxi 2 sale del garaje en el tiempo=10 y completa dos viajes (flechas rojas cortas). • En esta ejecución de
muestra, todos los eventos programados se completaron en el tiempo de simulación predeterminado
La simulación también puede terminar con eventos pendientes. Cuando eso sucede, el mensaje final dice así:
La lista completa de taxi_sim.py se encuentra en el Ejemplo A-6. En este capítulo, mostraremos solo las partes
que son relevantes para nuestro estudio de rutinas. Las funciones realmente importantes son solo dos:
taxi_process (una corrutina) y el método Simulator.run donde se ejecuta el bucle principal de la simulación.
El ejemplo 16-20 muestra el código para taxi_process. Esta corrutina utiliza dos objetos definidos en otro lugar:
la función compute_delay , que devuelve un intervalo de tiempo en minutos, y la clase Event , una tupla con
nombre definida así:
En una instancia de Evento , time es el tiempo de simulación en el que ocurrirá el evento, proc es el identificador
de la instancia de proceso de rodaje y action es una cadena que describe la actividad.
taxi_process se llamará una vez por taxi, creando un objeto generador para
representar sus operaciones. ident es el número del taxi (por ejemplo, 0, 1, 2 en la muestra
correr); viajes es el número de viajes que hará este taxi antes de volver a casa;
start_time es cuando el taxi sale del garaje.
El ciclo for finaliza después de la cantidad dada de viajes y un último 'regreso a casa'
se produce el evento. La rutina se suspenderá por última vez. Cuando se reactiva,
se envia la hora desde el loop principal de la simulacion, pero aqui no la asigno
a cualquier variable porque no se utilizará.
14. El verbo "impulsar" se usa comúnmente para describir el funcionamiento de una corrutina: el código del cliente controla la corrutina
enviándole valores. En el Ejemplo 16-21, el código de cliente es lo que escribe en la consola.
Cree un objeto generador para representar un taxi con ident=13 que hará dos
viajes y comienza a trabajar en t=0.
Ahora podemos enviarle la hora actual. En la consola, el último resultado; _ variable está ligada a
aquí le sumo 7 al tiempo, lo que significa que el taxi gastará 7
minutos buscando al primer pasajero.
Enviar _.time + 23 significa que el viaje con el primer pasajero durará 23 minutos.
Después de dos viajes completos, el bucle finaliza y se produce el evento de 'ir a casa' .
El próximo intento de enviar a la rutina hace que se caiga hasta el final. Cuando
regresa, el intérprete genera StopIteration.
Tenga en cuenta que en el Ejemplo 16-21 estoy usando la consola para emular el bucle principal de simulación.
Obtengo el atributo .time de un evento producido por la rutina de taxi , agrego un valor arbitrario
número, y utilizar la suma en el próximo taxi . Enviar llamada para reactivarlo. En la simulación,
las corrutinas de taxi son impulsadas por el bucle principal en el método Simulator.run . los
El "reloj" de la simulación se mantiene en la variable sim_time y se actualiza con la hora de cada
evento producido.
Por lo tanto, los valores del diccionario de taxis serán tres objetos generadores distintos con diferentes
parámetros. Por ejemplo, el taxi 1 hará 4 viajes y comenzará a buscar pasajeros en start_time=5. Este
dict es el único argumento requerido para construir una instancia de Simulator .
auto.eventos
PriorityQueue para contener instancias de eventos . PriorityQueue le permite colocar elementos y
luego ordenarlos por elemento [0]; es decir, el atributo de tiempo en el caso de nuestros objetos de
tupla con nombre de evento.
self.procs
Un dictado que asigna cada número de proceso a un proceso activo en la simulación: un objeto
generador que representa un taxi. Esto estará vinculado a una copia del dictamen de taxis que se
muestra anteriormente.
PriorityQueue para contener los eventos programados, ordenados por tiempo creciente.
Obtenemos el argumento procs_map como un dict (o cualquier mapeo), pero construimos un dict
a partir de él, para tener una copia local porque cuando se ejecuta la simulación, cada taxi que va
a casa se elimina de self.procs, y no queremos para cambiar el objeto pasado por el usuario.
Las colas de prioridad son un componente fundamental de las simulaciones de eventos discretos: los
eventos se crean en cualquier orden, se colocan en la cola y luego se recuperan en orden de acuerdo con
el tiempo programado de cada uno. Por ejemplo, los primeros dos eventos colocados en la cola pueden
ser:
Esto significa que el taxi 0 tardará 14 minutos en recoger al primer pasajero, mientras que el taxi 1,
comenzando en el tiempo = 10, tardará 1 minuto y recogerá a un pasajero en el tiempo = 11. Si esos
dos eventos están en la cola, el primer evento que obtiene el bucle principal de la cola de prioridad
será Evento (tiempo = 11, proceso = 1, acción = 'recoger pasajero').
sim = Simulador(taxis)
sim.run(end_time)
La lista con llamadas para la clase Simulator está en el Ejemplo 16-23, pero aquí hay una vista de
alto nivel del algoritmo implementado en Simulator.run:
una. Prepare la rutina para cada taxi llamando a next() en él. Esto producirá la primera
Evento para cada taxi.
Obtenga el evento actual de self.events . Este será el objeto Evento con el tiempo más bajo en
PriorityQueue.
C. Mostrar el evento. d.
Ejemplo 16-23. taxi_sim.py: Simulator, una clase básica de simulación de eventos discretos;
centrarse en el método de ejecución
Simulador de clase :
self.events.put(primer_evento)
evento_actual = self.events.get()
tiempo_sim, id_proc , acción_previa = evento_actual
print('taxi:', id_proc, id_proc * ' ', evento_actual )=active_proc
self.procs[id_proc]
siguiente_tiempo = tiempo_sim + cálculo_duración(acción_anterior)
intente:
next_event = active_proc.send(next_time)
excepto StopIteration:
del self.procs[proc_id] else:
self.events.put(next_event)
más:
msg '*** fin del tiempo de simulación: {} eventos pendientes ***'
= print(msg.format(self.events.qsize()))
Use sorted para recuperar los elementos self.procs ordenados por clave; no nos importa
sobre la clave, así que asígnela a _.
next(proc) prepara cada corrutina avanzándola al primer rendimiento, para que esté lista
para enviar datos. Se produce un evento .
Agregue cada evento a Self.events PriorityQueue. El primer evento para cada taxi.
es 'salir del garaje', como se ve en la ejecución de la muestra (Ejemplo 16-20).
Obtener Evento con el menor tiempo en la cola de prioridad; este es el evento actual.
Descomprima los datos del evento . Esta línea actualiza el reloj de simulación, sim_time, para
reflejan el momento en que ocurrió el evento.15
15. Esto es típico de una simulación de eventos discretos: el reloj de simulación no se incrementa en una cantidad fija en
cada ciclo, pero avanza de acuerdo con la duración de cada evento completado.
Volviendo al Capítulo 15, tenga en cuenta que el método Simulator.run en el Ejemplo 16-23 usa
bloques else en dos lugares que no son sentencias if :
• El ciclo while principal tiene una instrucción else para informar que la simulación finalizó
porque se alcanzó la hora_finalización , y no porque no hubiera más eventos para procesar.
• La declaración de prueba en la parte inferior del ciclo while intenta obtener un próximo_evento
enviando el siguiente_tiempo al proceso taxi actual y, si tiene éxito, el bloque else coloca el
próximo_evento en la cola de autoeventos .
Creo que el código en Simulator.run sería un poco más difícil de leer sin esos bloques .
El objetivo de este ejemplo era mostrar un bucle principal que procesa eventos y conduce coÿ
rutinas enviándoles datos. Esta es la idea básica detrás de asyncio, que estudiaremos en el
Capítulo 18.
Está el estilo tradicional de "jalar" (iteradores), el estilo "empujar" (como el ejemplo del
promedio), y luego están las "tareas" (¿Ya leyó el tutorial de rutinas de Dave
Beazley?...).16 El Capítulo 14 se dedicó a los iteradores; este capítulo introdujo corrutinas
utilizadas en "estilo push" y también como "tareas" muy simples: los procesos de taxi en el ejemplo de simulación.
El Capítulo 18 los pondrá en uso como tareas asincrónicas en la programación concurrente.
16. Mensaje para hilo "Rendimiento de: Garantías de finalización" en la lista de correo de Python-ideas. El tutorial de
David Beazley al que se refiere Guido es "Un curso curioso sobre rutinas y concurrencia".
El ejemplo del promedio móvil demostró un uso común para una rutina: como un acumulador que procesa los
elementos que se le envían. Vimos cómo se puede aplicar un decorador para preparar una rutina, lo que hace
que su uso sea más conveniente en algunos casos. Pero tenga en cuenta que los decoradores de imprimación
no son compatibles con algunos usos de rutinas. En particular, yield from subgenerator() asume que el
subgenerador no está cebado y lo ceba automáticamente.
Las corrutinas del acumulador pueden generar resultados parciales con cada llamada al método de envío , pero
se vuelven más útiles cuando pueden devolver valores, una característica que se agregó en Python 3.3 con PEP
380. Vimos cómo la instrucción devuelve el_resultado en un generador ahora genera StopIteration( the_result),
lo que permite a la persona que llama recuperar the_result del atributo de valor de la excepción. Esta es una
forma bastante engorrosa de recuperar los resultados de la rutina, pero es manejada automáticamente por el
rendimiento de la sintaxis introducida en PEP 380.
La cobertura de yield from comenzó con ejemplos triviales usando iterables simples, luego pasó a un ejemplo
que destaca los tres componentes principales de cualquier uso significativo de yield from: el generador delegado
(definido por el uso de yield from en su cuerpo), el subgenerador activado por yield from, y el código de cliente
que realmente impulsa toda la configuración mediante el envío de valores al subgenerador a través del canal de
transferencia establecido por yield from en el generador delegado. Esta sección concluyó con una mirada a la
definición formal de rendimiento del comportamiento como se describe en PEP 380 usando pseudocódigo en
inglés y similar a Python.
Cerramos el capítulo con el ejemplo de simulación de eventos discretos, que muestra cómo los generadores se
pueden usar como una alternativa a los subprocesos y las devoluciones de llamada para admitir la concurrencia.
Aunque es simple, la simulación de taxi ofrece un primer vistazo de cómo los marcos de trabajo controlados por
eventos, como Tornado y asyncio, usan un bucle principal para controlar las corrutinas que ejecutan actividades
simultáneas con un solo hilo de ejecución. En la programación orientada a eventos con corrutinas, cada actividad
concurrente la lleva a cabo una corrutina que repetidamente devuelve el control al ciclo principal, lo que permite
que otras corrutinas se activen y avancen. Esta es una forma de multitarea cooperativa: las corrutinas ceden el
control voluntaria y explícitamente al planificador central. Por el contrario, los subprocesos implementan la
multitarea preventiva. El programador puede suspender subprocesos en cualquier momento, incluso a la mitad
de una declaración, para dar paso a otros subprocesos.
Una nota final: este capítulo adoptó una definición amplia e informal de una rutina: una función generadora
impulsada por un cliente que le envía datos a través de llamadas .send (...) o rendimiento desde. Esta definición
amplia es la que se usa en PEP 342 — Corrutinas a través de generadores mejorados y en la mayoría de los
libros de Python existentes mientras escribo esto. La biblioteca asyncio que veremos en el Capítulo 18 se basa
en coroutines, pero allí se adopta una definición más estricta de coroutine: las coroutines asyncio están
(generalmente) decoradas con un decorador @asyncio.coroutine , y siempre están impulsadas por el rendimiento
de , no llamando a .send(…) directamente en
a ellos. Por supuesto, las corrutinas asyncio están impulsadas por next(...) y .send(...) bajo las
cubiertas, pero en el código de usuario solo usamos yield from para que se ejecuten.
Otras lecturas
David Beazley es la máxima autoridad en generadores y corrutinas de Python. El Python Cookbook,
3E (O'Reilly), del que es coautor con Brian Jones, tiene numerosas recetas con corrutinas. Los
tutoriales PyCon de Beazley sobre el tema son legendarios por su profundidad y amplitud. El primero
fue en PyCon US 2008: “Generator Tricks for Systems Programmers”. PyCon US 2009 vio el
legendario "Un curso curioso sobre rutinas y concurrencia" (enlaces de video difíciles de encontrar
para las tres partes: parte 1, parte 2, parte 3). Su tutorial más reciente de PyCon 2014 en Montreal
fue "Generators: The Final Frontier", en el que aborda más ejemplos de simultaneidad, por lo que
en realidad trata más sobre los temas del Capítulo 18 de Fluent Python. Dave no puede resistirse a
hacer explotar los cerebros en sus clases, por lo que en la última parte de “The Final Frontier”, las
corrutinas reemplazan el clásico patrón Visitor en un evaluador de expresiones aritméticas.
Las corrutinas permiten nuevas formas de organizar el código y, al igual que la recursividad o el
polimorfismo (despacho dinámico), lleva algún tiempo acostumbrarse a sus posibilidades. Un
ejemplo interesante de algoritmo clásico reescrito con rutinas se encuentra en la publicación
"Algoritmo codicioso con rutinas", de James Powell. También puede buscar "Recetas populares
etiquetadas como corrutina" en la base de datos de recetas de ActiveState Code .
Paul Sokolovsky implementó yield from en el intérprete MicroPython súper delgado de Damien
George diseñado para ejecutarse en microcontroladores. Mientras estudiaba la función, creó un
gran diagrama detallado para explicar cómo funciona el rendimiento de y lo compartió en la lista de
correo de python-tulip. Sokolovsky tuvo la amabilidad de permitirme copiar el PDF en el sitio de este
libro, donde tiene una URL más permanente.
Mientras escribo esto, la gran mayoría de los usos de yield from to be found están en asyncio mismo
o en el código que lo usa. Pasé mucho tiempo buscando ejemplos de rendimiento que no dependieran
de asyncio. Greg Ewing, quien escribió PEP 380 e implementó el rendimiento en CPython, publicó
algunos ejemplos de su uso: una clase BinaryTree , un analizador XML simple y un programador de
tareas.
El Python efectivo de Brett Slatkin (Addison-Wesley) tiene un excelente capítulo breve titulado
"Considere las corrutinas para ejecutar muchas funciones al mismo tiempo" (disponible en línea
como capítulo de muestra). Ese capítulo incluye el mejor ejemplo que he visto de impulsar
generadores con rendimiento : una implementación del Juego de la vida de John Conway en el que
se usan corrutinas para administrar el estado de cada celda a medida que se ejecuta el juego. El
código de ejemplo de Python eficaz se puede encontrar en un repositorio de GitHub. Refactoricé el
código para el ejemplo de Game of Life, separando las funciones y clases que implementan el juego
de los fragmentos de prueba usados en el libro de Slatkin (código original). También reescribí las pruebas.
como doctests, para que pueda ver el resultado de las distintas corrutinas y clases sin ejecutar el script.
El ejemplo refactorizado se publica como una esencia de GitHub.
Otros ejemplos interesantes de rendimiento sin asyncio aparecen en un mensaje a la lista de tutores de
Python, "Comparación de dos archivos CSV usando Python" de Peter Otten, y un juego de piedra, papel
o tijera en el pub tutorial "Iterables, iteradores y generadores" de Ian Ward. - publicado como un
cuaderno de iPython.
Guido van Rossum envió un largo mensaje al grupo de Google python-tulip titulado "La diferencia entre
el rendimiento y el rendimiento de" que vale la pena leer. Nick Coghlan publicó una versión muy
comentada del rendimiento de la expansión a Python-Dev el 21 de marzo. 2009; en el mismo mensaje,
escribió:
PEP 492 — Coroutines con sintaxis async y await de Yury Selivanov propone la adición de dos palabras
clave a Python: async y await. El primero se utilizará con otras palabras clave existentes para definir
nuevas construcciones de lenguaje. Por ejemplo, async def se usará para definir una rutina y async
para recorrer iterables asíncronos con iteradores asíncronos (implementando __aiter__ y __anext__,
versiones de rutina de __iter__ y __next__). Para evitar conflictos con la próxima palabra clave async ,
la función esencial asyncio.async() pasará a llamarse asyncio.ensure_future() en Python 3.4.4. La
palabra clave await hará algo similar a yield from, pero solo se permitirá dentro de corrutinas definidas
con async def, donde el uso de yield y yield from estará prohibido. Con la nueva sintaxis, el PEP
establece una clara separación entre los generadores heredados que evolucionaron hasta convertirse
en objetos coroutine-like y una nueva generación de objetos coroutine nativos con un mejor soporte de
lenguaje gracias a una infraestructura como las palabras clave async y await y varios métodos especiales
nuevos. Las corrutinas están a punto de volverse realmente importantes en el futuro de Python y el
lenguaje debe adaptarse para integrarlas mejor.
Experimentar con simulaciones de eventos discretos es una excelente manera de sentirse cómodo con
la multitarea cooperativa. El artículo de Wikipedia “Simulación de eventos discretos” es un buen lugar
para comenzar.17 Un breve tutorial sobre cómo escribir simulaciones de eventos discretos a mano (sin
bibliotecas especiales) es “Escribir una simulación de eventos discretos: diez lecciones fáciles” de
Ashish Gupta. El código está en Java, por lo que está basado en clases y no utiliza corrutinas, pero
puede trasladarse fácilmente a Python. Independientemente del código, el tutorial es una buena introducción breve a
17. Hoy en día, incluso los profesores titulares están de acuerdo en que Wikipedia es un buen lugar para comenzar a estudiar
prácticamente cualquier tema de informática. No es cierto sobre otros temas, pero para la informática, las rocas de Wikipedia.
la terminología y los componentes de una simulación de eventos discretos. Convertir los ejemplos
de Gupta en clases de Python y luego en clases que aprovechen corrutinas es un buen ejercicio.
Para una biblioteca lista para usar en Python, usando corrutinas, está SimPy. Su documentación en
línea explica:
SimPy es un marco de simulación de eventos discretos basado en procesos basado en Python estándar.
Su despachador de eventos se basa en los generadores de Python y también se puede utilizar para
redes asíncronas o para implementar sistemas multiagente (tanto con comunicación simulada como real).
Las corrutinas no son tan nuevas en Python, pero estaban bastante vinculadas a dominios de
aplicaciones de nicho antes de que los marcos de programación asíncrona comenzaran a admitirlos,
comenzando con Tornado. La adición de yield en Python 3.3 y asyncio en Python 3.4 probablemente
impulsará la adopción de corrutinas y del mismo Python 3.4. Sin embargo, Python 3.4 tiene menos
de un año cuando escribo esto, por lo que una vez que vea los tutoriales de David Beazley y los
ejemplos de libros de cocina sobre el tema, no hay mucho contenido que profundice en la
programación de rutinas de Python. Por ahora.
Plataforma improvisada
Subir de lambda
En los lenguajes de programación, las palabras clave establecen las reglas básicas de flujo de control y
evaluación de expresiones.
Una palabra clave en un idioma es como una pieza en un juego de mesa. En el lenguaje del ajedrez, las
palabras clave son ÿ, ÿ, ÿ, ÿ, ÿ y ÿ. En el juego de Go, es •.
Los jugadores de ajedrez tienen seis tipos diferentes de piezas para implementar sus planes, mientras que
los jugadores de Go parecen tener solo un tipo de pieza. Sin embargo, en la semántica de Go, las piezas
adyacentes forman piezas sólidas más grandes de muchas formas diferentes, con propiedades emergentes.
Algunos arreglos de piezas de Go son indestructibles. El Go es más expresivo que el Ajedrez. En Go hay 361
posibles movimientos de apertura y un estimado de 1e+170 posiciones legales; para el Ajedrez, los números
son 20 movimientos de apertura 1e+50 posiciones.
Agregar una nueva pieza al Ajedrez sería un cambio radical. Agregar una nueva palabra clave en un lenguaje
de programación también es un cambio radical. Por lo tanto, tiene sentido que los diseñadores de idiomas
desconfíen de la introducción de palabras clave.
25 Vamos
El lenguaje, no el juego.
33 Pitón Python 2.7 tiene 31 palabras clave; Python 1.5 tenía 28.
41 Rubí Las palabras clave se pueden utilizar como identificadores (p. ej., la clase también es un nombre de método).
49 Java
Como en C, los nombres de los tipos primitivos (char, float, etc.) están reservados.
60 JavaScript Incluye todas las palabras clave de Java 1.0, muchas de las cuales no se utilizan.
555 COBOL Yo no inventé esto. Consulte este manual de IBM ILE COBOL.
Python 3 agregó no local, promovió Ninguno, Verdadero y Falso al estado de palabra clave, y
dejó de imprimir y exec. Es muy poco común que un idioma suelte palabras clave cuando
evoluciona La tabla 16-1 enumera algunos idiomas, ordenados por número de palabras clave.
Scheme heredó de Lisp una instalación de macros que permite a cualquier persona crear formularios especiales
añadiendo nuevas estructuras de control y reglas de evaluación al lenguaje. definido por el usuario
los identificadores de esas formas se denominan "palabras clave sintácticas". El estándar Esquema R5RS
establece "No hay identificadores reservados" (página 45 del estándar), pero un implemento típico
mentación como MIT/GNU Scheme viene con 34 palabras clave sintácticas predefinidas,
como if, lambda y define-syntax, la palabra clave que le permite conjurar una nueva clave.
palabras.18
Ahora, volvamos a la sintaxis de Python. Creo que Guido es demasiado conservador con las palabras clave. Es agradable
tener un pequeño conjunto de ellos, y agregar nuevas palabras clave potencialmente rompe una gran cantidad de código.
Pero el uso de else en los bucles revela un problema recurrente: la sobrecarga de los
palabras clave cuando una nueva sería una mejor opción. En el contexto de for, while y
pruebe, una nueva palabra clave entonces sería preferible a abusar de otra cosa.
La introducción de yield from es particularmente preocupante. Una vez más, creo que Python
los usuarios estarían mejor atendidos por una nueva palabra clave. Peor aún, esto inicia una nueva tendencia:
encadenar palabras clave existentes para crear una nueva sintaxis, en lugar de agregar sensata, descriptiva
palabras clave Temo que algún día estemos estudiando detenidamente el significado de aumentar de lambda.
18. "¿El valor de la sintaxis?" es una discusión interesante sobre la sintaxis extensible y el lenguaje de programación
usabilidad El foro, Lambda the Ultimate, es un abrevadero para los fanáticos de los lenguajes de programación.
19. Una publicación muy recomendada relacionada con este problema en el contexto de JavaScript, Python y otros lenguajes es
“¿De qué color es tu función?” de Bob Nyström.
Mientras concluyo el proceso de revisión técnica de este libro, parece que PEP 492 de Yury
Selivanov — Coroutines con sintaxis async y await ya está en camino de ser aceptado para su
implementación en Python 3.5. El PEP cuenta con el apoyo de Guido van Rossum y Victor
Stinner, respectivamente el autor y uno de los principales mantenedores de la biblioteca asyncio
que sería el principal caso de uso para la nueva sintaxis. En respuesta al mensaje de Selivanov
a Python-ideas, Guido incluso sugiere retrasar el lanzamiento de Python 3.5 para que se pueda
implementar el PEP.
Por supuesto, esto pondría fin a la mayoría de las quejas que expresé en las secciones
anteriores.
CAPÍTULO 17
Las personas que critican los hilos son típicamente programadores de sistemas que tienen en mente
casos de uso que el típico programador de aplicaciones nunca encontrará en su vida. […] En el 99%
de los casos de uso es probable que un programador de aplicaciones se encuentre, el patrón simple
de generar un montón de subprocesos independientes y recopilar los resultados en una cola es todo
lo que uno necesita saber.1
— Michele Simionato
Pensador profundo
de Python Este capítulo se centra en la biblioteca concurrent.futures presentada en Python 3.2, pero
también disponible para Python 2.5 y posteriores como el paquete de futuros en PyPI. Esta biblioteca
encapsula el patrón descrito por Michele Simionato en la cita anterior, por lo que su uso es casi trivial.
Aquí también presento el concepto de "futuros": objetos que representan la ejecución asincrónica de
una operación. Esta poderosa idea es la base no solo de concur rent.futures sino también del
paquete asyncio , que trataremos en el Capítulo 18.
Para hacer este último punto con código, escribí tres programas simples para descargar imágenes
de 20 banderas de países de la Web. El primero, flags.py, se ejecuta secuencialmente: solo responde
1. Del post de Michele Simionato Hilos, procesos y concurrencia en Python: algunos pensamientos, subtitulado
"Eliminar la exageración en torno a la (no) revolución multinúcleo y algunos (con suerte) comentarios sensatos sobre
subprocesos y otras formas de concurrencia".
505
Machine Translated by Google
busca la siguiente imagen cuando la anterior se descarga y se guarda en el disco. Los otros dos scripts
realizan descargas simultáneas: solicitan todas las imágenes prácticamente al mismo tiempo y guardan los
archivos a medida que llegan. El script flags_threadpool.py usa el paquete concur rent.futures , mientras que
flags_asyncio.py usa asyncio.
El ejemplo 17-1 muestra el resultado de ejecutar los tres scripts, tres veces cada uno. También publiqué un
video de 73s en YouTube para que pueda verlos funcionar mientras una ventana de OS X Finder muestra las
banderas a medida que se guardan. Los scripts descargan imágenes de flupy.org, que está detrás de un
CDN, por lo que es posible que vea resultados más lentos en las primeras ejecuciones. Los resultados del
Ejemplo 17-1 se obtuvieron después de varias ejecuciones, por lo que la memoria caché de la CDN estaba templada.
Ejemplo 17-1. Tres ejecuciones típicas de los scripts flags.py, flags_threadpool.py y flags_asyncio.py
$ python3 flags.py BD
BR CD CN DE EG ET FR ID IN IR JP MX NG PH PK RU TR US VN
20 banderas descargadas en 7.26s
$ python3 flags.py BD BR CD CN
DE EG ET FR ID IN IR JP MX NG PH PK RU TR US VN 20 banderas
descargadas en 7.20s $ python3 flags.py BD BR CD CN DE EG ET FR ID IN
IR JP MX NG PH PK RU TR US VN
EG BR FR IN BD JP DE RU PK PH CD MX ID US NG TR CN VN ET IR
20 banderas descargadas en 1.60s
$ python3 flags_threadpool.py BD DE
EG CN ID RU IN VN ET MX FR CD NG US JP TR PK BR IR PH
20 banderas descargadas en 1.22s
$ python3 flags_asyncio.py BD BR
IN ID TR DE CN US IR PK PH FR RU NG VN ET MX EG JP CD 20 banderas
descargadas en 1.36s $ python3 flags_asyncio.py RU CN BR IN FR BD TR
EG VN IR PH CD ET ID NG DE JP PK MX US
El resultado de cada ejecución comienza con los códigos de país de las banderas a medida que se
descargan y finaliza con un mensaje que indica el tiempo transcurrido.
Tenga en cuenta el orden de los códigos de país: las descargas ocurrieron en un orden
diferente cada vez con los scripts simultáneos.
La diferencia de rendimiento entre los scripts simultáneos no es significativa, pero ambos son más
de cinco veces más rápidos que el script secuencial, y esto es solo para una tarea bastante
pequeña. Si escala la tarea a cientos de descargas, los scripts simultáneos pueden superar al
secuencial por un factor de 20 o más.
Ahora estudiemos las implementaciones de dos de los scripts probados en el Ejemplo 17-1:
flags.py y flags_threadpool.py. Dejaré la tercera secuencia de comandos, flags_asyncio.py, para
el Capítulo 18, pero quería demostrar las tres juntas para aclarar un punto: independientemente
de la estrategia de concurrencia que utilice (subprocesos o asyncio), verá un rendimiento mucho
mejor que código secuencial en aplicaciones vinculadas a E/S, si lo codifica correctamente.
En el código.
descarga secuencial 17-2 no es muy interesante, pero reutilizaremos la mayor parte de su código y configuraciones
para implementar las secuencias de comandos concurrentes, por lo que merece atención.
Ejemplo 17-2. flags.py: script de descarga secuencial; algunas funciones serán reutilizadas por los
otros scripts
importar os
importar hora
importar sys
solicitudes de importación
'
POP20_CC = ('CN IN ID DE EE. UU. BR PK NG BD RU
JP 'MX PH VN ET EG DE IR TR CD FR'). dividir ()
BASE_URL = 'http://flupy.org/data/flags'
DEST_DIR = 'descargas/'
def mostrar(texto):
imprimir(texto, fin=' ')
sys.stdout.flush()
def descargar_muchos(cc_list):
para cc en ordenados (cc_list):
imagen = get_flag (cc)
mostrar (cc)
save_flag(imagen, cc.inferior() + '.gif')
volver len(cc_list)
def main(download_many): t0
= tiempo.tiempo()
cuenta = descargar_muchos(POP20_CC)
transcurrido = tiempo.tiempo() - t0
msg = '\n{} banderas descargadas en {:.2f}s'
imprimir(mensaje.formato(recuento, transcurrido))
si __nombre__ == '__principal__':
principal (descargar_muchos)
Dado un código de país, cree la URL y descargue la imagen, devolviendo el contenido binario de la respuesta.
Muestre una cadena y vacíe sys.stdout para que podamos ver el progreso en una pantalla de una línea; esto
es necesario porque Python normalmente espera que se vacíe un salto de línea
el búfer de salida estándar.
Recorra la lista de códigos de países en orden alfabético, para dejar en claro que el orden se conserva en la
salida; devolver el número de códigos de países descargados.
Realmente no hay nada nuevo en flags.py. Sirve como línea de base para comparar los otros scripts y lo usé como
una biblioteca para evitar código redundante al implementarlos.
Ahora veamos una reimplementación usando concurrent.futures.
principales del paquete concurrent.futures son las clases ThreadPoolExecutor y ProcessPoolExecutor , que
implementan una interfaz que le permite enviar llamadas para su ejecución en diferentes subprocesos o procesos,
respectivamente. Las clases administran un grupo interno de subprocesos o procesos de trabajo, y una cola de tareas
para ser
2. Las imágenes son originalmente del World Factbook de la CIA, una publicación del gobierno de EE. UU. de dominio
público. Los copié en mi sitio para evitar el riesgo de lanzar un ataque DOS en CIA.gov.
El ejemplo 17-3 muestra la forma más fácil de implementar las descargas al mismo tiempo, utilizando
el método ThreadPoolExecutor.map .
MAX_TRABAJADORES = 20
def descargar_uno(cc):
imagen = obtener_bandera(cc)
mostrar (cc)
save_flag(imagen, cc.inferior() + '.gif')
volver cc
def descargar_muchos(cc_list):
trabajadores = min(MAX_WORKERS, len(cc_list))
con futures.ThreadPoolExecutor(workers) como ejecutor: res =
executor.map(download_one, sorted(cc_list))
return len(lista(res))
si __nombre__ == '__principal__':
principal (descargar_muchos)
Función para descargar una sola imagen; esto es lo que ejecutará cada subproceso.
Establezca el número de subprocesos de trabajo: use el número más pequeño entre el
máximo que queremos permitir (MAX_WORKERS) y los elementos reales que se procesarán,
por lo que no se crean subprocesos innecesarios.
El método del mapa es similar al mapa incorporado, excepto que el método download_one
la función se llamará simultáneamente desde varios subprocesos; devuelve un generador
que se puede iterar para recuperar el valor devuelto por cada función.
Tenga en cuenta que la función download_one del ejemplo 17-3 es esencialmente el cuerpo del ciclo
for en la función download_many del ejemplo 17-2. Esta es una refactorización común cuando se
escribe código concurrente: convertir el cuerpo de un bucle for secuencial en una función para ser
llamada concurrentemente.
La biblioteca se llama concurrency.futures , pero no se ven futuros en el Ejemplo 17-3, por lo que
quizás se pregunte dónde están. La siguiente sección explica.
Los futuros son componentes esenciales en el interior de concurrent.futures y de async cio, pero
como usuarios de estas bibliotecas a veces no los vemos. El ejemplo 17-3 aprovecha los futuros
entre bastidores, pero el código que escribí no los toca directamente. Esta sección es una descripción
general de los futuros, con un ejemplo que los muestra en acción.
A partir de Python 3.4, hay dos clases denominadas Future en la biblioteca estándar: concur
rent.futures.Future y asyncio.Future. Tienen el mismo propósito: una instancia de cualquiera de las
clases Future representa un cálculo diferido que puede haberse completado o no. Esto es similar a
la clase Deferred en Twisted, la clase Future en Tornado y los objetos Promise en varias bibliotecas
de JavaScript.
Los futuros encapsulan las operaciones pendientes para que se puedan poner en cola, se pueda
consultar su estado de finalización y se puedan recuperar sus resultados (o excepciones) cuando
estén disponibles.
Una cosa importante que debe saber sobre los futuros en general es que usted y yo no debemos
crearlos: están destinados a ser instanciados exclusivamente por el marco de concurrencia, ya sea
concurrent.futures o asyncio. Es fácil entender por qué: un futuro representa algo que eventualmente
sucederá, y la única forma de estar seguro de que algo sucederá es programar su ejecución. Por lo
tanto, las instancias concurrentes.futures.Future se crean solo como resultado de programar algo
para su ejecución con una subclase concurrent.futures.Executor . Por ejemplo, el método
Executor.submit() toma un invocable, lo programa para que se ejecute y devuelve un futuro.
No se supone que el código del cliente cambie el estado de un futuro: el marco de concurrencia
cambia el estado de un futuro cuando se realiza el cálculo que representa, y no podemos controlar
cuándo sucede eso.
Ambos tipos de Future tienen un método .done() que no bloquea y devuelve un booleano que le indica
si el invocable vinculado a ese futuro se ha ejecutado o no. En lugar de preguntar si se realiza un
futuro, el código del cliente generalmente solicita que se le notifique. Es por eso que ambas clases
Future tienen un método .add_done_callback() : le das un callable, y el callable se invocará con el
futuro como único argumento cuando el futuro esté listo.
También hay un método .result() , que funciona igual en ambas clases cuando se realiza el futuro:
devuelve el resultado de la llamada o vuelve a generar cualquier excepción que se haya producido
cuando se ejecutó la llamada. Sin embargo, cuando no se hace el futuro, el comportamiento del
método de resultado es muy diferente entre los dos tipos de Futuro. En una instancia de
concurrency.futures.Future , invocar f.result() bloqueará el hilo de la persona que llama hasta que el
resultado esté listo. Se puede pasar un argumento de tiempo de espera opcional y, si el futuro no se
realiza en el tiempo especificado, se genera una excepción TimeoutError . En “asyncio.Future:
Nonblocking by Design” en la página 545, veremos que el método asyncio.Future.result no admite el
tiempo de espera, y la forma preferida de obtener el resultado de futuros en esa biblioteca es usar
yield from, que no funciona con instancias de concurrency.futures.Future .
Varias funciones en ambas bibliotecas devuelven futuros; otros los utilizan en su implementación de
forma transparente para el usuario. Un ejemplo de esto último es el Execu tor.map que vimos en el
Ejemplo 17-3: devuelve un iterador en el que __next__ llama al método de resultado de cada futuro,
por lo que lo que obtenemos son los resultados de los futuros, y no los futuros mismos. .
Para obtener una visión práctica de los futuros, podemos reescribir el Ejemplo 17-3 para usar la
función concur rent.futures.as_completed , que toma una iteración de futuros y devuelve un iterador
que produce futuros a medida que se realizan.
resultados = []
para el futuro en futures.as_completed(to_do):
res = futuro.resultado()
mensaje = ' {} resultado: {!r}'
print(msg.format(futuro, res))
resultados.append(res)
volver len(resultados)
Para esta demostración, use solo los cinco países más poblados.
Iterar los códigos de países alfabéticamente, para dejar claro que los resultados llegan
de orden.
Almacene cada futuro para que luego podamos recuperarlos con as_completed.
Tenga en cuenta que la llamada future.result() nunca se bloqueará en este ejemplo porque el fu
ture está saliendo de as_completed. El ejemplo 17-5 muestra el resultado de una corrida de
Ejemplo 17-4.
$ python3 flags_threadpool_ac.py
Programado para BR: <Futuro en 0x100791518 state=running>
Programado para CN: <Futuro en 0x100791710 state=running>
Programado para ID: <Futuro en 0x100791a90 state=running>
Programado para IN: <Futuro en 0x101807080 estado=pendiente>
Programado para EE. UU.: <Futuro en 0x101807128 estado=pendiente>
CN <Futuro en 0x100791710 state=finished devolvió str> resultado: 'CN'
BR ID <Futuro en 0x100791518 state=finished devolvió str> resultado: 'BR'
<Futuro en 0x100791a90 state=finished devolvió str> result: 'ID'
IN <Futuro en 0x101807080 state=finished devolvió str> resultado: 'IN'
US <Futuro en 0x101807128 state=finished devolvió str> resultado: 'US'
Los futuros están programados en orden alfabético; el repr() de un futuro muestra su estado: los tres primeros
se están ejecutando, porque hay tres subprocesos de trabajo.
Aquí, dos hilos emiten códigos antes de que download_many en el hilo principal pueda mostrar el resultado
del primer hilo.
Vimos dos variantes del script de descarga usando concurrent.futures: Ejemplo 17-3 con ThreadPoolExecutor.map y
Ejemplo 17-4 con futures.as_completed. Si tiene curiosidad sobre el código de flags_asyncio.py, puede echar un
vistazo al Ejemplo 18-5 en el Capítulo 18.
Estrictamente hablando, ninguno de los scripts concurrentes que probamos hasta ahora puede realizar descargas en
paralelo. Los ejemplos de concurrent.futures están limitados por el GIL, y flags_asyncio.py es de subproceso único.
En este punto, es posible que tenga preguntas sobre los puntos de referencia informales que acabamos de hacer:
• ¿Cómo puede flags_threadpool.py funcionar 5 veces más rápido que flags.py si los subprocesos de Python están
limitados por un bloqueo de intérprete global (GIL) que solo permite que se ejecute un subproceso en cualquier
momento? • ¿Cómo puede flags_asyncio.py funcionar 5 veces más rápido que flags.py cuando ambos son únicos ?
roscado?
Continúe leyendo para comprender por qué GIL es casi inofensivo con el procesamiento vinculado a E/S.
escribimos código de Python, no tenemos control sobre el GIL, pero una función integrada o una extensión escrita
en C puede liberar el GIL mientras se ejecuta. tareas que requieren mucho tiempo.
De hecho, una biblioteca de Python codificada en C puede administrar la GIL, iniciar sus propios subprocesos del
sistema operativo y aprovechar todos los núcleos de CPU disponibles. Esto complica considerablemente el código
de la biblioteca, y la mayoría de los autores de bibliotecas no lo hacen.
Sin embargo, todas las funciones de biblioteca estándar que realizan bloqueos de E/S liberan la GIL cuando esperan
un resultado del sistema operativo. Esto significa que los programas de Python que están vinculados a E/S pueden
beneficiarse del uso de subprocesos en el nivel de Python: mientras un subproceso de Python espera una respuesta
de la red, la función de E/S bloqueada libera el GIL para que se pueda ejecutar otro subproceso.
Es por eso que David Beazley dice: "Los subprocesos de Python son geniales para no hacer nada".4
Ahora echemos un breve vistazo a una forma sencilla de solucionar el GIL para trabajos vinculados a la CPU
utilizando concurrent.futures.
Tanto ProcessPoolExecutor como ThreadPoolExecutor implementan la interfaz genérica Execu tor , por lo que es
muy fácil cambiar de una solución basada en subprocesos a una basada en procesos utilizando concurrent.futures.
3. Esta es una limitación del intérprete CPython, no del lenguaje Python en sí. Jython y IronPython son
no limitado de esta manera; pero Pypy, el intérprete de Python más rápido disponible, también tiene un GIL.
No hay ninguna ventaja en usar un ProcessPoolExecutor para el ejemplo de descarga de indicadores o cualquier
trabajo vinculado a E/S. Es fácil verificar esto; simplemente cambie estas líneas en el Ejemplo 17-3:
def download_many(cc_list):
trabajadores = min(MAX_WORKERS, len(cc_list))
with futures.ThreadPoolExecutor(workers) como ejecutor:
A esto:
def download_many(cc_list):
con futures.ProcessPoolExecutor() como ejecutor:
Para usos simples, la única diferencia notable entre las dos clases ejecutoras concretas es que
ThreadPoolExecutor.__init__ requiere un argumento max_workers que establece la cantidad de subprocesos
en el grupo. Ese es un argumento opcional en ProcessPoolExecu tor, y la mayoría de las veces no lo usamos;
el valor predeterminado es el número de CPU devuelto por os.cpu_count(). Esto tiene sentido: para el
procesamiento vinculado a la CPU, no tiene sentido solicitar más trabajadores que CPU. Por otro lado, para el
procesamiento vinculado a E/S, puede usar 10, 100 o 1000 subprocesos en un ThreadPoolExecutor; el mejor
número depende de lo que esté haciendo y de la memoria disponible, y encontrar el número óptimo requerirá
pruebas cuidadosas.
Algunas pruebas revelaron que el tiempo promedio para descargar las 20 banderas aumentó a 1,8 s con un
ProcessPoolExecutor, en comparación con 1,4 s en la versión original de ThreadPoolExecutor . Es probable
que la razón principal de esto sea el límite de cuatro descargas simultáneas en mi máquina de cuatro núcleos,
frente a 20 trabajadores en la versión del grupo de subprocesos.
El valor de ProcessPoolExecutor está en los trabajos de uso intensivo de la CPU. Hice algunas pruebas de
rendimiento con un par de scripts vinculados a la CPU: arcfour_futures.py Cifre y descifre una docena de
matrices de bytes con tamaños de 149 KB a 384 KB usando una implementación de Python puro del algoritmo
RC4 (listado: Ejemplo A-7) .
sha_futures.py
Calcule el hash SHA-256 de una docena de arreglos de 1 MB byte con el paquete hashlib de biblioteca
estándar , que usa la biblioteca OpenSSL (listado: Ejemplo A-9).
Ninguno de estos scripts realiza operaciones de E/S, excepto para mostrar resultados resumidos. Construyen y
procesan todos sus datos en la memoria, por lo que la E/S no interfiere con su tiempo de ejecución.
La tabla 17-1 muestra los tiempos promedio que obtuve después de 64 ejecuciones del ejemplo RC4 y 48
ejecuciones del ejemplo SHA. Los tiempos incluyen el tiempo para generar realmente el trabajador proÿ
cesos
Tabla 17-1. Factor de tiempo y aceleración para los ejemplos RC4 y SHA con uno a cuatro
trabajadores en una máquina Intel Core i7 de 2,7 GHz de cuatro núcleos, utilizando Python 3.4
Para el ejemplo RC4 de Python puro, puede obtener resultados 3,8 veces más rápido si usa PyPy
y cuatro trabajadores, en comparación con CPython y cuatro trabajadores. Eso es una aceleración de 7.8
veces en relación con la línea de base de un trabajador con CPython en la Tabla 17-1.
def mostrar(*argumentos):
imprimir(strftime('[%H:%M:%S]'), end=' ')
imprimir(*argumentos)
def merodear(n):
msg = '{}merodear({}): no hacer nada por {}s...'
display(msg.format('\t'*n, n, n)) sleep(n) msg = '{}
merodear({}): hecho.' mostrar(mensaje.formato('\t'*n, n))
devolver n
* 10
def main():
display('Script iniciando.') executor
= futures.ThreadPoolExecutor(max_workers=3) resultados =
executor.map(merodear, rango(5)) display('resultados:', resultados)
.
# display('Esperando para resultados individuales:') for i, result in
enumerate(results): display('result {}: {}'.format(i, result))
principal()
Esta función simplemente imprime cualquier argumento que obtenga, precedido por una marca de tiempo
en el formato [HH:MM:SS]. loiter no hace nada más que mostrar un mensaje cuando comienza, dormir
durante n segundos y luego mostrar un mensaje cuando termina; Las tabulaciones se utilizan para
sangrar los mensajes según el valor de n.
Envíe cinco tareas al ejecutor (debido a que solo hay tres subprocesos, solo tres de esas tareas
comenzarán de inmediato: las llamadas holgazanear (0), holgazanear (1) y holgazanear (2)); esta es una
llamada sin bloqueo.
La llamada de enumeración en el bucle for invocará implícitamente next(results), que a su vez invocará
_f.result() en el futuro _f (interno) que representa la primera llamada, loiter(0). El método de resultado se
bloqueará hasta que finalice el futuro, por lo tanto, cada iteración en este ciclo tendrá que esperar a que
el siguiente resultado esté listo.
Le animo a que ejecute el ejemplo 17-6 y vea que la pantalla se actualiza de forma incremental.
Mientras lo hace, juegue con el argumento max_workers para ThreadPoolExecutor y con la función de rango que
produce los argumentos para la llamada executor.map , o reemplácelo con listas de valores cuidadosamente
seleccionados para crear diferentes retrasos.
$ python3 demo_executor_map.py
[15:56:50] Inicio del guión.
[15:56:50] holgazanear (0): no hacer nada por 0s...
[15:56:50] holgazanear (0): listo.
[15:56:50] loiter(1): no hacer nada durante 1s...
[15:56:50] loiter(2): no hacer nada durante 2s...
[15:56:50] resultados: <generator object result_iterator at 0x106517168> [15:56:50]
merodeador (3): no hacer nada durante 3 s...
[15:56:50] Esperando resultados individuales:
[15:56:50] resultado 0: 0
[15:56:51] holgazanear (1): [15:56:51]
hecho.
[15:56:51] resultado 1: 10 [15:56:52] loiter(4): sin hacer nada durante 4s...
holgazanear (2): hecho. [15:56:52]
resultado 2: 20
El primer subproceso ejecuta loiter(0), por lo que dormirá durante 0 y regresará incluso antes
el segundo hilo tiene la oportunidad de comenzar, pero YMMV.5
Esto muestra que los resultados devueltos por executor.map son un generador; nada
hasta ahora se bloquearía, independientemente del número de tareas y del max_workers
ajuste.
Debido a que loiter(0) ha terminado, el primer trabajador ahora está disponible para iniciar el cuarto
hilo para holgazanear (3).
loiter(1) se realiza un segundo después, a las 15:56:51. El hilo está libre para comenzar.
merodear (4).
5. Su millaje puede variar: con los hilos, nunca se sabe la secuencia exacta de eventos que deberían suceder
prácticamente al mismo tiempo; es posible que, en otra máquina, vea loiter(1) comenzando antes
loiter(0) termina, particularmente porque el sueño siempre libera el GIL para que Python pueda cambiar a otro
hilo incluso si duermes por 0s.
El patrón se repite: loiter(2) está hecho, se muestra su resultado; lo mismo con loiter (3).
Hay un retraso de 2 s hasta que finaliza loiter (4) , porque comenzó a las 15:56:51 y no hizo
nada durante 4 s.
La función Executor.map es fácil de usar, pero tiene una función que puede o no ser útil, según sus
necesidades: devuelve los resultados exactamente en el mismo orden en que se inician las llamadas:
si la primera llamada tarda 10 segundos en producirse un resultado, y los otros toman 1s cada uno,
su código se bloqueará durante 10s mientras intenta recuperar el primer resultado del generador
devuelto por el mapa. Después de eso, obtendrás los resultados restantes sin bloquear porque
estarán listos. Eso está bien cuando debe tener todos los resultados antes de continuar, pero a
menudo es preferible obtener los resultados cuando están listos, independientemente del orden en que se enviaron.
Para hacerlo, necesita una combinación del método Executor.submit y la función futures.as_completed ,
como vimos en el Ejemplo 17-4. Volveremos a esta técnica en “Uso de futures.as_completed” en la
página 527.
En la siguiente sección, retomaremos los ejemplos de descarga de banderas con nuevos requisitos
que nos obligarán a iterar sobre los resultados de futures.as_completed en lugar de usar executor.map.
Para probar el manejo de una variedad de condiciones de error, creé los ejemplos de flags2 :
flags2_common.py Este módulo contiene funciones y configuraciones comunes utilizadas por todos
los ejemplos de flags2 , incluida una función principal , que se encarga del análisis de la línea de
comandos, el tiempo , y
reportando resultados. Este es realmente un código de soporte, no directamente relacionado con el tema de
este capítulo, por lo que el código fuente se encuentra en el Apéndice A, Ejemplo A-10.
flags2_secuencial.py Un
cliente HTTP secuencial con manejo de errores adecuado y visualización de barra de progreso. Su función
download_one también es utilizada por flags2_threadpool.py.
flags2_threadpool.py Cliente
HTTP simultáneo basado en futures.ThreadPoolExecutor para demostrar el manejo de errores y la integración
de la barra de progreso.
flags2_asyncio.py La
misma funcionalidad que el ejemplo anterior pero implementada con asyncio y aiohttp. Esto se tratará en
"Mejora de la secuencia de comandos del descargador asyncio" en la página 554, en el Capítulo 18.
La característica más visible de los ejemplos flags2 es que tienen una barra de progreso animada en modo texto
implementada con el paquete TQDM. Publiqué un video de 108 segundos en YouTube para mostrar la barra de
progreso y contrastar la velocidad de los tres scripts de flags2 .
En el video, empiezo con la descarga secuencial, pero la interrumpo después de los 32 segundos porque iba a
tardar más de 5 minutos en llegar a 676 URL y obtener 194 banderas; Luego ejecuto los scripts de subprocesos y
asyncio tres veces cada uno, y cada vez que completan el trabajo en 6 segundos o menos (es decir, más de 60
veces más rápido). La Figura 17-1 muestra dos capturas de pantalla: durante y después de ejecutar
flags2_threadpool.py.
Figura 17-1. Arriba a la izquierda: flags2_threadpool.py ejecutándose con la barra de progreso en vivo generada
por tqdm; abajo a la derecha: la misma ventana de terminal después de que finalice el script.
TQDM es muy fácil de usar, el ejemplo más simple aparece en un .gif animado en el README.md del proyecto.
Si escribe el siguiente código en la consola de Python después de instalar el paquete tqdm , verá una barra de
progreso animada donde el comentario es:
>>> tiempo de
importación >>> desde tqdm
importar tqdm >>> para i en
... tqdm(rango(1000)): tiempo.dormir(.01)
...
>>> # -> la barra de progreso aparecerá aquí <-
Además del efecto limpio, la función tqdm también es interesante conceptualmente: consume cualquier iterable y
produce un iterador que, mientras se consume, muestra la barra de progreso y estima el tiempo restante para
completar todas las iteraciones. Para calcular esa estimación, tqdm necesita obtener un iterable que tenga un len,
o recibir como segundo argumento el número esperado de elementos. La integración de TQDM con nuestros
ejemplos flags2 brinda la oportunidad de profundizar en cómo funcionan realmente los scripts simultáneos,
obligándonos a usar las funciones futures.as_completed y asyncio.as_completed para que tqdm pueda mostrar el
progreso a medida que se completa cada futuro.
La otra característica del ejemplo flags2 es una interfaz de línea de comandos. Los tres scripts aceptan las mismas
opciones y puede verlos ejecutando cualquiera de los scripts con la opción -h . El ejemplo 17-8 muestra el texto
de ayuda.
Descargar banderas para códigos de países. Predeterminado: 20 países principales por población.
argumentos posicionales:
CC código de país o primera letra (por ejemplo, B para BA...BZ)
Todos los argumentos son opcionales. Los argumentos más importantes se discuten a continuación.
Una opción que no puede ignorar es -s/--server: le permite elegir qué servidor HTTP y URL base se usarán
en la prueba. Puede pasar una de las cuatro cadenas para determinar dónde buscará la secuencia de
comandos las banderas (las cadenas no distinguen entre mayúsculas y minúsculas):
LOCAL
Utilice http://localhost:8001/flags; este es el valor predeterminado. Debe configurar un servidor HTTP
local para responder en el puerto 8001. Usé Nginx para mis pruebas. El archivo README.rst del código
de ejemplo de este capítulo explica cómo instalarlo y configurarlo.
REMOTO
Utilice http://flupy.org/data/flags; ese es un sitio web público de mi propiedad, alojado en un servidor
compartido. Por favor, no lo aplaste con demasiadas solicitudes simultáneas. El dominio flupy.org está
gestionado por una cuenta gratuita en la CDN de Cloudflare , por lo que puede notar que las primeras
descargas son más lentas, pero se vuelven más rápidas cuando la memoria caché de la CDN se
calienta.6
RETRASO Utilice http://localhost:8002/flags; un proxy que retrasa las respuestas HTTP debería estar
escuchando en el puerto 8002. Utilicé un Mozilla Vaurien frente a mi Nginx local para introducir retrasos.
El archivo README.rst mencionado anteriormente tiene instrucciones para ejecutar un proxy de Vaurien.
6. Antes de configurar Cloudflare, recibí errores HTTP 503 (Servicio temporalmente no disponible) al probar los scripts con
unas pocas docenas de solicitudes simultáneas en mi económica cuenta de host compartida. Ahora esos errores se
han ido.
De manera predeterminada, cada secuencia de comandos flags2 obtendrá las banderas de los 20 países más
poblados del servidor LOCAL (http://localhost:8001/flags) utilizando un número predeterminado de conexiones
simultáneas, que varía de secuencia de comandos. El ejemplo 17-9 muestra una ejecución de muestra del script
flags2_secuencial.py utilizando todos los valores predeterminados.
Ejemplo 17-9. Ejecutando flags2_secuencial.py con todos los valores predeterminados: sitio LOCAL, 20
indicadores principales, 1 conexión simultánea
--------------------
20 banderas descargadas.
Tiempo transcurrido: 0,10 s
Puede seleccionar qué banderas se descargarán de varias maneras. El ejemplo 17-10 muestra cómo descargar
todas las banderas con códigos de países que comienzan con las letras A, B o C.
Ejemplo 17-10. Ejecute flags2_threadpool.py para obtener todas las banderas con prefijos de código de país A,
B o C del servidor DELAY
--------------------
43 banderas descargadas.
35 no encontrado.
Tiempo transcurrido: 1,72 s
Independientemente de cómo se seleccionen los códigos de país, el número de banderas a buscar se puede
limitar con la opción -l/--limit . El ejemplo 17-11 demuestra cómo ejecutar exactamente 100 solicitudes,
combinando la opción -a para obtener todas las banderas con -l 100.
Ejemplo 17-11. Ejecute flags2_asyncio.py para obtener 100 indicadores (-al 100) del servidor ERROR,
utilizando 100 solicitudes simultáneas (-m 100)
--------------------
73 banderas descargadas.
27 errores.
Tiempo transcurrido: 0,64 s
común adoptada en los tres ejemplos para tratar los errores HTTP es que los errores 404 (Not Found)
son manejados por la función a cargo de descargar un solo archivo (download_one). Cualquier otra
excepción se propaga para ser manejada por la función down load_many .
Una vez más, comenzaremos estudiando el código secuencial, que es más fácil de seguir y, en su
mayoría, lo reutiliza la secuencia de comandos del grupo de subprocesos. El ejemplo 17-12 muestra las
funciones que realizan las descargas reales en los scripts flags2_secuencial.py y flags2_threadpool.py .
si detallado:
imprimir (cc, msg)
Cualquier otra excepción de HTTPError se vuelve a generar; otras excepciones simplemente se propagarán
a la persona que llama
si error_msg:
estado = HTTPStatus.error
counter[status] += 1 si es
detallado y error_msg:
contador de retorno
Si no se ejecuta en modo detallado, cc_iter se pasa a la función tqdm , que devolverá un iterador que
arroja los elementos en cc_iter mientras también muestra la barra de progreso animada.
Las excepciones relacionadas con HTTP generadas por get_flag y no manejadas por down load_one
se manejan aquí.
Aquí se manejan otras excepciones relacionadas con la red. Cualquier otra excepción abortará el
script, porque la función flags2_common.main que llama a load_many no tiene intento/excepto.
Si se ejecuta en modo detallado, muestra el mensaje de error para el código de país actual, si
corresponde.
Devuelve el contador para que la función principal pueda mostrar los números en su informe final.
la barra de progreso de TQDM y manejar errores en cada solicitud, el script flags2_threadpool.py usa
futures.ThreadPoolExecutor con la función futures.as_completed que ya hemos visto. El ejemplo 17-14 es la
lista completa de flags2_threadpool.py. Solo se implementa la función download_many ; las otras funciones
se reutilizan desde los módulos flags2_common y flags2_secuencial .
importar colecciones
de futuros de importación concurrentes
solicitudes de importación
importar tqdm
DEFAULT_CONCUR_REQ = 30
MAX_CONCUR_REQ = 1000
res = futuro.resultado()
excepto solicitudes.excepciones.HTTPError como exc:
error_msg = 'HTTP {res.status_code} - {res.razón}'
error_msg = error_msg.format(res=exc.response)
excepto solicitudes.excepciones.ConnectionError como exc:
error_msg = 'Error de conexión'
más:
''
error_msg =
estado = res.status
si error_msg:
estado = HTTPStatus.error
contador[estado] += 1
si detallado y error_msg:
cc = to_do_map[futuro]
print('*** Error para {}: {}'.format(cc, error_msg))
contador de retorno
si __nombre__ == '__principal__':
principal (descargar_muchos, DEFAULT_CONCUR_REQ, MAX_CONCUR_REQ)
Cree el ejecutor con max_workers establecido en concur_req, calculado por la función principal
como el menor de: MAX_CONCUR_REQ, la longitud de cc_list y el valor de la opción de línea de
comandos -m/--max_req . Esto evita crear más subprocesos de los necesarios.
Este dictado mapeará cada instancia de Future , que representa una descarga, con el código de
país respectivo para el informe de errores.
Iterar sobre la lista de códigos de países en orden alfabético. El orden de los resultados dependerá
más que nada del momento de las respuestas HTTP, pero si el tamaño del grupo de subprocesos
(dado por concur_req) es mucho más pequeño que len(cc_list), es posible que observe que las
descargas se distribuyen por lotes alfabéticamente.
Cada llamada a executor.submit programa la ejecución de una llamada y
devuelve una instancia de Future . El primer argumento es el invocable, el resto son los
argumentos que recibirá.
Si no está en modo detallado, ajuste el resultado de as_completed con la función tqdm para
mostrar la barra de progreso; dado que done_iter no tiene len, debemos decirle a tqdm cuál es el
número esperado de elementos como el argumento total= , para que tqdm pueda estimar el
trabajo restante.
Llamar al método de resultado en un futuro devuelve el valor devuelto por el invocable o genera
cualquier excepción que se detectó cuando se ejecutó el invocable.
Este método puede bloquear la espera de una resolución, pero no en este ejemplo porque
as_completed solo devuelve futuros que están listos.
Manejar las posibles excepciones; el resto de esta función es idéntica a la versión secuencial de
download_many (Ejemplo 17-13), excepto por la siguiente llamada.
Para proporcionar contexto para el mensaje de error, recupere el código de país de to_do_map
utilizando el futuro actual como clave. Esto no fue necesario en la versión secuencial porque
estábamos iterando sobre la lista de códigos de países, por lo que teníamos el cc actual; aquí
estamos iterando sobre los futuros.
El ejemplo 17-14 usa un modismo que es muy útil con futures.as_completed: construir un dict para
asignar cada futuro a otros datos que pueden ser útiles cuando se complete el futuro.
Aquí to_do_map asigna cada futuro al código de país asignado. Esto facilita el procesamiento de
seguimiento con el resultado de los futuros, a pesar de que se producen fuera de orden.
Los subprocesos de Python son adecuados para aplicaciones de E/S intensivas, y el paquete
concurrent.futures los hace trivialmente simples de usar para ciertos casos de uso. Esto concluye
nuestra introducción básica a los futuros concurrentes. Analicemos ahora las alternativas para cuando
ThreadPoolExecutor o ProcessPoolExecutor no sean adecuados.
ha admitido subprocesos desde su lanzamiento 0.9.8 (1993); concurrent.futures es solo la última forma
de usarlos. En Python 3, el módulo de subprocesos original quedó obsoleto en favor del módulo de
subprocesos de nivel superior . 7 Si futures.ThreadPoolExecutor no es lo suficientemente flexible para
un determinado trabajo, es posible que deba crear su propia solución a partir de componentes básicos
de subprocesamiento , como Thread, Lock, Semaphore, etc., posiblemente utilizando las colas seguras
para subprocesos del módulo de cola para pasar datos entre hilos. Esas partes móviles están
encapsuladas por futures.ThreadPoolExecutor.
Para el trabajo vinculado a la CPU, debe eludir el GIL iniciando múltiples procesos.
El futures.ProcessPoolExecutor es la forma más fácil de hacerlo. Pero nuevamente, si su caso de uso
es complejo, necesitará herramientas más avanzadas. El paquete de multiprocesamiento emula la API
de subprocesamiento pero delega trabajos a múltiples procesos. Para programas simples, el
multiprocesamiento puede reemplazar el subprocesamiento con pocos cambios. Pero el
multiprocesamiento también ofrece facilidades para resolver el mayor desafío al que se enfrentan los
procesos de colaboración: cómo transmitir datos.
Después de estudiar el primer ejemplo basado en concurrent.futures, echamos un vistazo más de cerca
a los objetos futuros, ya sea instancias de concurrent.futures.Future o asyncio.Future, enfatizando lo
que estas clases tienen en común (se enfatizarán sus diferencias). en el Capítulo 18). Vimos cómo
crear futuros llamando a Executor.sub
7. El módulo de subprocesos ha estado disponible desde Python 1.5.1 (1998), pero algunos insisten en usar el antiguo módulo de
subprocesos. En Python 3, se cambió el nombre a _thread para resaltar el hecho de que es solo un detalle de implementación
de bajo nivel y no debe usarse en el código de la aplicación.
A continuación volvimos a los ejemplos de descarga de banderas. Mejorarlos con una barra de
progreso y el manejo adecuado de errores impulsó una mayor exploración de la función de
generador de future.as_completed que muestra un patrón común: almacenar futuros en un dict
para vincular más información a ellos al enviarlos, de modo que podamos usar esa información
cuando el futuro sale del iterador as_completed .
Concluimos la cobertura de la concurrencia con hilos y procesos con un breve recordatorio de los
módulos de subprocesos y multiprocesamiento de nivel inferior, pero más flexibles, que representan
la forma tradicional de aprovechar hilos y procesos en Python.
Otras lecturas
El paquete concurrent.futures fue aportado por Brian Quinlan, quien lo presentó en una gran charla
titulada “¡El futuro es pronto!” en PyCon Australia 2010. La charla de Quinlan no tiene diapositivas;
muestra lo que hace la biblioteca escribiendo código directamente en la consola de Python.
Como ejemplo motivador, la presentación presenta un video corto con el caricaturista/programador
de XKCD, Randall Munroe, que realiza un ataque DOS involuntario en Google Maps para crear un
mapa en color de los tiempos de conducción en su ciudad. La introducción formal a la biblioteca es
PEP 3148 - futuros - ejecutar cálculos de forma asíncrona. En el PEP, Quinlan escribió que la
biblioteca concurrent.futures estaba "fuertemente influenciada por el paquete Java java.util.concurrent
".
Parallel Programming with Python (Packt), de Jan Palach, cubre varias herramientas para la
programación concurrente, incluidos los módulos concurrent.futures, threading y multiprocessing .
Va más allá de la biblioteca estándar para analizar Celery, una cola de tareas que se usa para
distribuir el trabajo entre subprocesos y procesos, incluso en diferentes máquinas. En la comunidad
de Django, Celery es probablemente el sistema más utilizado para descargar tareas pesadas como
la generación de PDF a otros procesos, evitando así retrasos en la producción de una respuesta
HTTP.
En el Libro de cocina Python de Beazley y Jones , 3E (O'Reilly) hay recetas que usan con
current.futures que comienzan con “Recipe 11.12. Comprender la E/S impulsada por eventos”.
“Receta 12.7. Creación de un grupo de subprocesos” muestra un servidor de eco TCP simple y
“Receta 12.8. Realización de programación paralela simple” ofrece un ejemplo muy práctico: analizar
un directorio completo de archivos de registro de Apache comprimidos con gzip con la ayuda de un
Proces sPoolExecutor. Para obtener más información sobre los hilos, todo el Capítulo 12 de Beazley
y Jones es excelente, con una mención especial a la “Receta 12.10. Definición de una tarea de actor”,
que demuestra el modelo Actor: una forma comprobada de coordinar hilos a través del paso de mensajes.
El Python efectivo de Brett Slatkin (Addison-Wesley) tiene un capítulo de varios temas sobre la
concurrencia, incluida la cobertura de rutinas, futuros concurrentes con subprocesos y procesos, y el
uso de bloqueos y colas para la programación de subprocesos sin Thread PoolExecutor.
High Performance Python (O'Reilly) de Micha Gorelick e Ian Ozsvald y The Python Standard Library
by Example (Addison-Wesley), de Doug Hellmann, también cubren subprocesos y procesos.
Para una versión moderna de la concurrencia sin subprocesos ni devoluciones de llamada, Seven
Concurrency Models in Seven Weeks, de Paul Butcher (Pragmatic Bookshelf) es una lectura
excelente. Me encanta su subtítulo: “When Threads Unravel”. En ese libro, los hilos y los bloqueos se
tratan en el Capítulo 1, y los seis capítulos restantes están dedicados a las alternativas modernas a
la programación concurrente, con el apoyo de diferentes lenguajes. Python, Ruby y JavaÿScript no
se encuentran entre ellos.
Si está intrigado acerca de GIL, comience con las Preguntas frecuentes sobre la biblioteca y la
extensión de Python ("¿No podemos deshacernos del bloqueo global del intérprete?"). También vale
la pena leer las publicaciones de Guido van Rossum y Jesse Noller (colaborador del paquete de
multiprocesamiento ): "No es fácil eliminar el GIL" y "Python Threads and the Global Interpreter Lock".
Finalmente , David Beazley tiene una exploración detallada sobre el funcionamiento interno de la
GIL: "Comprender la GIL de Python " . con el nuevo algoritmo GIL introducido en Python 3.2. Sin
embargo, Beazley aparentemente usó un while True: pase vacío para simular el trabajo vinculado a
la CPU, y eso no es realista.
El problema no es significativo con las cargas de trabajo reales, según un comentario de Antoine
Pitrou, quien implementó el nuevo algoritmo GIL, en el informe de error presentado por Beazley.
Si bien el GIL es un problema real y no es probable que desaparezca pronto, Jesse Noller y Richard
Oudkerk contribuyó con una biblioteca para facilitar el trabajo en aplicaciones vinculadas a la CPU:
el paquete de multiprocesamiento , que emula la API de subprocesamiento en todos los procesos,
junto con la infraestructura de soporte de bloqueos, colas, conductos, memoria compartida,
etc. El paquete se introdujo en PEP 371: adición del paquete de multiprocesamiento a la biblioteca
estándar. La documentación oficial del paquete es un archivo .rst de 93 KB (aproximadamente 63
páginas), lo que lo convierte en uno de los capítulos más extensos de la biblioteca estándar de Python.
El multiprocesamiento es la base de concurrent.futures.ProcessPoolExecu
colina.
Para el procesamiento paralelo intensivo de CPU y datos, una nueva opción con mucho impulso en la
comunidad de big data es el motor de computación distribuida Apache Spark , que ofrece una API de
Python amigable y soporte para objetos de Python como datos, como se muestra en su página de
ejemplos. .
Dos bibliotecas elegantes y súper fáciles para paralelizar tareas sobre procesos son lelo de João SO
Bueno y python-parallelize de Nat Pryce. El paquete lelo define un decorador @parallel que puedes
aplicar a cualquier función para desbloquearla mágicamente: cuando llamas a la función decorada, su
ejecución se inicia en otro proceso. El paquete python-parallelize de Nat Pryce proporciona un
generador de paralelismo que puede usar para distribuir la ejecución de un bucle for en varias CPU.
Ambos paquetes usan el módulo de multiprocesamiento debajo de las cubiertas.
Plataforma improvisada
Evitación de subprocesos
Concurrencia: uno de los temas más difíciles en informática (normalmente es mejor evitarlo).9
—David Beazley
Entrenador de Python y científico loco
Estoy de acuerdo con las citas aparentemente contradictorias de David Beazley, arriba, y Michele Simionato al comienzo de este
capítulo. Después de asistir a un curso de concurrencia en la universidad, en el que la "programación concurrente" se equiparaba
con la gestión de subprocesos y bloqueos, llegué a la conclusión de que no quiero gestionar subprocesos y bloqueos yo mismo,
como tampoco quiero gestionarlos. Asignación y desasignación de memoria. Esos trabajos los realizan mejor los programadores de
sistemas que tienen el conocimiento, la inclinación y el tiempo para hacerlos bien, con suerte.
Es por eso que creo que el paquete concurrent.futures es emocionante: trata los subprocesos, los procesos y las colas como una
infraestructura a su servicio, no como algo con lo que tenga que lidiar directamente. Por supuesto, está diseñado con trabajos
simples en mente, los llamados problemas "vergonzosamente paralelos" . Pero esa es una gran parte de los problemas de
concurrencia que enfrentamos al escribir aplicaciones, a diferencia de los sistemas operativos o los servidores de bases de datos,
9. Diapositiva n.º 9 de "Un curso curioso sobre corrutinas y concurrencia", tutorial presentado en PyCon
2009.
Para los problemas de concurrencia "no vergonzosos", los hilos y los bloqueos tampoco son la respuesta.
Los subprocesos nunca desaparecerán en el nivel del sistema operativo, pero todos los lenguajes de
programación que he encontrado emocionantes en los últimos años proporcionan mejores abstracciones
de concurrencia de mayor nivel, como lo demuestra el libro Seven Concurrency Models . Go, Elixir y
Clojure están entre ellos. Erlang, el lenguaje de implementación de Elixir, es un excelente ejemplo de un
lenguaje diseñado desde cero con la concurrencia en mente. No me emociona por una simple razón: me
parece fea su sintaxis. Python me mimó de esa manera.
José Valim, conocido como colaborador principal de Ruby on Rails, diseñó Elixir con una sintaxis
agradable y moderna. Al igual que Lisp y Clojure, Elixir implementa macros sintácticas.
Esa es una espada de doble filo. Las macros sintácticas permiten DSL poderosos, pero la proliferación
de sublenguajes puede conducir a bases de código incompatibles y fragmentación de la comunidad. Lisp
se ahogó en una avalancha de macros, con cada tienda de Lisp usando su propio dialecto arcano.
La estandarización en torno a Common Lisp resultó en un lenguaje inflado. Espero que José Valim pueda
inspirar a la comunidad de Elixir para evitar un resultado similar.
Al igual que Elixir, Go es un lenguaje moderno con ideas frescas. Pero, en algunos aspectos, es un
lenguaje conservador, en comparación con Elixir. Go no tiene macros y su sintaxis es más simple que la
de Python. Go no admite herencia ni sobrecarga de operadores, y ofrece menos oportunidades para la
metaprogramación que Python. Estas limitaciones se consideran características. Conducen a un
comportamiento y un rendimiento más predecibles. Esa es una gran ventaja en las configuraciones de
misión crítica altamente concurrentes donde Go pretende reemplazar a C++, Java y Python.
Si bien Elixir y Go son competidores directos en el espacio de alta concurrencia, sus filosofías de diseño
atraen a diferentes multitudes. Es probable que ambos prosperen. Pero en la historia de los lenguajes de
programación, los conservadores tienden a atraer a más programadores. Me gustaría adquirir fluidez en
Go y Elixir.
Sobre el GIL
GIL simplifica la implementación del intérprete CPython y de las extensiones escritas en C, por lo que
podemos agradecer a GIL por la gran cantidad de extensiones en C disponibles para Python, y esa es sin
duda una de las razones clave por las que Python es tan popular hoy en día.
Durante muchos años, tuve la impresión de que GIL hacía que los subprocesos de Python fueran casi
inútiles más allá de las aplicaciones de juguetes. No fue hasta que descubrí que cada llamada de E/S de
bloqueo en la biblioteca estándar libera el GIL que me di cuenta de que los subprocesos de Python son
excelentes para los sistemas vinculados a E/S, el tipo de aplicaciones que los clientes suelen pagarme
por desarrollar, dada mi experiencia profesional. .
implementación de referencia de Ruby, también tiene un GIL, por lo que sus subprocesos tienen las
mismas limitaciones que los de Python. Mientras tanto, los intérpretes de JavaScript no admiten hilos de
nivel de usuario en absoluto; la programación asíncrona con devoluciones de llamada es su único camino
hacia la concurrencia. Menciono esto porque Ruby y JavaScript son los competidores directos más
cercanos a Python como lenguajes de programación dinámicos de propósito general.
CAPÍTULO 18
La concurrencia proporciona una forma de estructurar una solución para resolver un problema que puede (pero
no necesariamente) ser paralelizable.1
— Rob Pike
Al profesor Imre Simon2 le gustaba decir que hay dos grandes pecados en la ciencia: usar diferentes
palabras para significar lo mismo y usar una palabra para significar cosas diferentes. Si realiza alguna
investigación sobre programación concurrente o paralela, encontrará diferentes definiciones de "concurrencia"
y "paralelismo". Adoptaré las definiciones informales de Rob Pike, citadas anteriormente.
Para un paralelismo real, debe tener varios núcleos. Una computadora portátil moderna tiene cuatro núcleos
de CPU, pero rutinariamente ejecuta más de 100 procesos en un momento dado bajo un uso normal e
informal. Entonces, en la práctica, la mayor parte del procesamiento ocurre simultáneamente y no en
paralelo. La computadora está constantemente lidiando con más de 100 procesos, asegurándose de que
cada uno tenga la oportunidad de progresar, incluso si la CPU en sí misma no puede hacer más de cuatro cosas a la vez.
Hace diez años, usábamos máquinas que también podían manejar 100 procesos al mismo tiempo, pero en
un solo núcleo. Es por eso que Rob Pike tituló esa charla "La concurrencia no es paralelismo (es mejor)".
2. Imre Simon (1943–2009) fue un pionero de las ciencias de la computación en Brasil que hizo contribuciones fundamentales
a la teoría de los autómatas y abrió el campo de las matemáticas tropicales. También fue un defensor del software libre y
la cultura libre. Tuve la suerte de estudiar, trabajar y pasar el rato con él.
537
Machine Translated by Google
Este capítulo presenta asyncio, un paquete que implementa la concurrencia con rutinas impulsadas por un bucle de
eventos. Es una de las bibliotecas más grandes y ambiciosas jamás agregadas a Python. Guido van Rossum
desarrolló asyncio fuera del repositorio de Python y le dio al proyecto el nombre en código de “Tulipán”, por lo que
verá referencias a esa flor cuando investigue este tema en línea. Por ejemplo, el grupo de discusión principal todavía
se llama python-tulip.
Tulip pasó a llamarse asyncio cuando se agregó a la biblioteca estándar en Python 3.4.
También es compatible con Python 3.3; puede encontrarlo en PyPI con el nuevo nombre oficial. Debido a que utiliza
ampliamente el rendimiento de las expresiones, asyncio es incompatible con versiones anteriores de Python.
programación asíncrona gestiona la alta concurrencia en aplicaciones de red, sin usar subprocesos o procesos •
Cómo las corrutinas son una mejora importante sobre las devoluciones de llamada para programas asíncronos
gramática
• Cómo evitar el bloqueo del bucle de eventos mediante la descarga de operaciones de bloqueo a un subproceso
piscina
• Escribir servidores asyncio y cómo repensar las aplicaciones web para una alta concurrencia • Por qué asyncio
Adapté el ejemplo de Simionato para usar un subproceso con el módulo Threading y luego una
rutina con asyncio, para que pueda ver los dos ejemplos uno al lado del otro y comprender cómo
codificar el comportamiento simultáneo sin subprocesos.
El resultado que se muestra en los ejemplos 18-1 y 18-2 está animado, por lo que debería
ejecutar los scripts para ver qué sucede. Si está en el metro (o en algún otro lugar sin conexión
WiFi), mire la Figura 18-1 e imagine la barra \ antes de que la palabra "pensar" esté girando.
Señal de clase :
ir = Verdadero
escribir (estado)
enjuagar()
write('\x08' * len(estado))
time.sleep(.1)
si no es señal.ir:
descanso
write(' ' * len(estado) + '\x08' * len(estado))
def slow_function(): #
finge esperar mucho tiempo para E/ S
tiempo.dormir(3)
volver 42
def principal():
resultado = supervisor()
print('Respuesta:', resultado)
si __nombre__ == '__principal__':
principal()
Esta clase define un objeto mutable simple con un atributo go que usaremos para controlar
el hilo desde afuera.
Llamar a sleep bloqueará el subproceso principal, pero de manera crucial, se liberará el GIL
para que continúe el subproceso secundario.
Esta función configura el subproceso secundario, muestra el objeto del subproceso, ejecuta
el cálculo lento y elimina el subproceso.
Ejecutar función_lenta; esto bloquea el hilo principal. Mientras tanto, la ruleta está animada
por el hilo secundario.
Cambiar el estado de la señal; esto terminará el bucle for dentro de la función de giro .
Tenga en cuenta que, por diseño, no hay una API para terminar un hilo en Python. Debes enviarle un
mensaje para que se apague. Aquí usé el atributo signal.go : cuando el subproceso principal lo
establece en falso, el subproceso giratorio eventualmente lo notará y saldrá limpiamente.
Ahora veamos cómo se puede lograr el mismo comportamiento con un @asyncio.coroutine en lugar
de un hilo.
@asyncio.coroutine def
spin(msg): escribir,
vaciar = sys.stdout.write, sys.stdout.flush
para char en itertools.cycle('|/-\\'):
' '
estado = char + + mensaje
escribir(estado)
enjuagar()
escribir('\x08' * len(estado))
probar:
rendimiento de asyncio.sleep(.1)
excepto asyncio.CancelledError: romper
@asincio.coroutine
def slow_function(): #
finge esperar mucho tiempo para E/ S
rendimiento de asyncio.sleep(3)
return 42
@asincio.coroutine
def supervisor():
spinner = asyncio.async(spin('pensando!')) print('objeto
spinner:', spinner) resultado = rendimiento de
slow_function() spinner.cancel() devolver resultado
def principal():
loop = asyncio.get_event_loop() resultado
= loop.run_until_complete(supervisor()) loop.close()
imprimir('Respuesta:', resultado)
si __nombre__ == '__principal__':
principal()
Las corrutinas destinadas a usarse con asyncio deben estar decoradas con @asyn
cio.coroutine. Esto no es obligatorio, pero es muy recomendable. Ver explicación
siguiendo este listado.
Aquí no necesitamos el argumento de la señal que se usó para cerrar el hilo
en la función de espín del ejemplo 18-1.
Use yield de asyncio.sleep(.1) en lugar de solo time.sleep(.1), para dormir sin bloquear el bucle de
eventos.
El rendimiento de la expresión asyncio.sleep(3) maneja el flujo de control al bucle principal, que reanudará
esta rutina después del retraso de suspensión. supervisor ahora también es una corrutina, por lo que
El objeto ATask se puede cancelar; esto genera asyncio.CancelledError en la línea de rendimiento donde
la rutina está actualmente suspendida. La rutina puede detectar la excepción y retrasar o incluso negarse
a cancelar.
Lleve a cabo la rutina del supervisor hasta que finalice; el valor de retorno de la rutina es el valor de
retorno de esta llamada.
El uso del decorador @asyncio.coroutine no es obligatorio, pero sí muy recomendable: hace que las corrutinas se
destaquen entre las funciones normales y ayuda con la depuración emitiendo una advertencia cuando una
corrutina se recolecta como basura sin que se produzca, lo que significa alguna operación quedó sin terminar y
es probable que sea un error. Este no es un decorador de imprimación.
def supervisor():
señal = Signal()
spinner = hilo.Thread(target=spin,
args=('pensando!', señal))
print('objeto spinner:', spinner) spinner.start() result = slow_function()
signal.go = False spinner.join() devolver resultado
@asyncio.coroutine
def supervisor():
spinner = asyncio.async(spin('pensando!'))
print('objeto spinner:', spinner) result = rendimiento
de slow_function() spinner.cancel() return result
Aquí hay un resumen de las principales diferencias a tener en cuenta entre las dos implementaciones de
supervisor :
• Usted no crea una instancia de los objetos Task , los obtiene pasando una rutina a
asyncio.async(…) o loop.create_task(…).
• Cuando obtiene un objeto Task , ya está programado para ejecutarse (por ejemplo, por async
cio.async); a una instancia de Thread se le debe indicar explícitamente que se ejecute llamando a su
método de inicio .
Esta comparación debería ayudarlo a comprender cómo se orquestan los trabajos simultáneos
con asyncio, en contraste con cómo se hace con el módulo Threading más familiar.
Un último punto relacionado con hilos versus corrutinas: si ha realizado alguna programación
no trivial con hilos, sabe lo difícil que es razonar sobre el programa porque el programador
puede interrumpir un hilo en cualquier momento. Debe recordar mantener bloqueos para
proteger las secciones críticas de su programa, para evitar ser interrumpido en medio de una
operación de varios pasos, lo que podría dejar los datos en un estado no válido.
Con coroutines, todo está protegido contra interrupciones por defecto. Debes ceder
explícitamente para dejar que se ejecute el resto del programa. En lugar de mantener bloqueos
para sincronizar las operaciones de varios subprocesos, tiene rutinas que están "sincronizadas"
por definición: solo una de ellas se está ejecutando en cualquier momento. Y cuando quiera
ceder el control, use yield o yield from para devolver el control al programador. Es por eso que
es posible cancelar una corrutina de manera segura: por definición, una corrutina solo se puede
cancelar cuando se suspende en un punto de rendimiento , por lo que puede realizar una
limpieza manejando la excepción CancelledError .
Como se mencionó en "¿Dónde están los futuros?" en la página 511, los futuros se crean solo
como resultado de programar algo para su ejecución. En asyncio, BaseEventLoop.create_task(…)
toma una rutina, la programa para que se ejecute y devuelve una instancia asyncio.Task , que
también es una instancia de asyncio.Future porque Task es una subclase de Future diseñada
para envolver una rutina. . Esto es análogo a cómo creamos instancias concurrentes de
rent.futures.Future invocando a Executor.submit(…).
En asyncio.Future, el método .result() no acepta argumentos, por lo que no puede especificar un tiempo
de espera. Además, si llama a .result() y el futuro no está listo, no bloquea la espera del resultado. En
su lugar, se genera un asyncio.InvalidStateError .
Sin embargo, la forma habitual de obtener el resultado de un asyncio.Future es ceder a partir de él,
como veremos en el Ejemplo 18-8.
El uso de yield from con un futuro automáticamente se encarga de esperar a que finalice, sin bloquear
el bucle de eventos, porque en asyncio, yield from se usa para devolver el control al bucle de eventos.
Tenga en cuenta que usar yield from con un futuro es el equivalente de rutina de la funcionalidad
ofrecida por add_done_callback: en lugar de activar una devolución de llamada, cuando se realiza la
operación retrasada, el bucle de eventos establece el resultado del futuro y el rendimiento de la
expresión . sion produce un valor de retorno dentro de nuestra rutina suspendida, lo que le permite reanudarse.
En resumen, debido a que asyncio.Future está diseñado para funcionar con rendimiento, estos métodos
a menudo no son necesarios:
Por supuesto, hay situaciones en las que .done(), .add_done_callback(…) y .results() son útiles. Pero
en el uso normal, los futuros de asyncio están impulsados por el rendimiento, no por llamar a esos
métodos.
Ahora consideraremos cómo el rendimiento de y la API asyncio reúne futuros, tareas y corrutinas.
Esta es una de las razones por las que las corrutinas y los futuros son intercambiables en muchas partes
de la API de asyncio .
Para ejecutarse, se debe programar una corrutina y luego se envuelve en una tarea asíncrona. Dada una
rutina, hay dos formas principales de obtener una tarea:
asyncio.async(coro_o_futuro, *, bucle=Ninguno)
Esta función unifica las corrutinas y los futuros: el primer argumento puede ser cualquiera de los dos.
Si es un futuro o una tarea, se devuelve sin cambios. Si es una corrutina, async llama a
loop.create_task(…) para crear una tarea. Se puede pasar un bucle de eventos opcional como
argumento de palabra clave loop= ; si se omite, async obtiene el objeto de bucle llamando a
asyncio.get_event_loop().
BaseEventLoop.create_task(coro)
Este método programa la ejecución de la rutina y devuelve un objeto asyncio.Task . Si se llama a
una subclase personalizada de BaseEventLoop, el objeto devuelto puede ser una instancia de alguna
otra clase compatible con tareas proporcionada por una biblioteca externa (por ejemplo, Tornado).
Varias funciones asyncio aceptan rutinas y las envuelven en objetos asyncio.Task automáticamente,
usando asyncio.async internamente. Un ejemplo es BaseEventLoop.run_un til_complete(…).
Si desea experimentar con futuros y corrutinas en la consola de Python o en pequeñas pruebas, puede
usar el siguiente fragmento:3
3. Sugerido por Petr Viktorin en un mensaje del 11 de septiembre de 2014 a la lista de ideas de Python.
Habiendo cubierto estos fundamentos, ahora estudiaremos el código para el script de descarga de
bandera asincrónica flags_asyncio.py demostrado junto con los scripts secuenciales y de grupo de
subprocesos en el Ejemplo 17-1 (Capítulo 17).
El ejemplo 18-5 es la lista completa del script de descarga de banderas flags_asyncio.py. Aquí hay una
vista de alto nivel de cómo funciona:
3. Cuando una corrutina de cliente, como get_flag , usa yield from para delegar a una corrutina de
biblioteca, como aiohttp.request, el control vuelve al bucle de eventos, que puede ejecutar otra
corrutina programada previamente.
4. El bucle de eventos utiliza API de bajo nivel basadas en devoluciones de llamadas para recibir notificaciones cuando se produce un bloqueo.
Esto es similar al ejemplo que vimos en “La simulación de la flota de taxis” en la página 490, donde un
bucle principal iniciaba varios procesos de taxi a la vez. A medida que rendía cada proceso de taxi, el
bucle principal programaba el siguiente evento para ese taxi (que sucedería en el futuro) y procedió a
activar el siguiente taxi en la cola. La simulación de taxi es mucho más simple y puede comprender
fácilmente su ciclo principal. Pero el flujo general es el mismo que en asyncio: un programa de un solo
subproceso donde un bucle principal activa las corrutinas en cola una por una. Cada corrutina avanza
unos pocos pasos, luego devuelve el control al ciclo principal, que luego activa la siguiente corrutina en la
cola.
importar asyncio
importar aiohttp
@asyncio.coroutine def
get_flag(cc):
url = '{}/{cc}/{cc}.gif'.format(URL_BASE, cc=cc.inferior())
resp = rendimiento de aiohttp.request('GET', url) imagen
= rendimiento de resp.read() devolver imagen
@asincio.coroutine
def descargar_uno(cc):
imagen = rendimiento de get_flag(cc)
mostrar(cc)
save_flag(imagen, cc.inferior() + '.gif')
volver cc
def descargar_muchos(cc_list):
loop = asyncio.get_event_loop() to_do =
[download_one(cc) for cc in sorted(cc_list)] wait_coro =
asyncio.wait(to_do) = loop.run_until_complete(wait_coro)
res, _
bucle.cerrar()
si __nombre__ == '__principal__':
principal (descargar_muchos)
download_one también debe ser una corrutina, porque usa yield from.
La única diferencia con la implementación secuencial de download_one son
las palabras ceden en esta línea; el resto del cuerpo de la función es exactamente como
antes de.
Cree una lista de objetos generadores llamando a la función download_one una vez por
cada bandera a recuperar.
A pesar de su nombre, la espera no es una función de bloqueo. Es una rutina que se completa cuando se
completan todas las rutinas pasadas (ese es el comportamiento predeterminado de espera; vea la
explicación después de este ejemplo).
Ejecute el bucle de eventos hasta que termine wait_coro ; aquí es donde la secuencia de comandos se
bloqueará mientras se ejecuta el bucle de eventos. Ignoramos el segundo elemento devuelto por run_un
til_complete. El motivo se explica a continuación.
La corrutina asyncio.wait(…) acepta un iterable de futuros o corrutinas; waitenvuelve cada rutina en una tarea. El
resultado final es que todos los objetos gestionados por wait se convierten en instancias de Future, de una forma u
otra. Debido a que es una función coroutine, llamar a wait(...) devuelve un objeto coroutine/generator; esto es lo que
contiene la variable wait_coro .
Para controlar la rutina, la pasamos a loop.run_until_complete(…).
La función loop.run_until_complete acepta un futuro o una rutina. Si obtiene una rutina, run_until_complete la
envuelve en una tarea, similar a lo que hace wait . Las rutinas, los futuros y las tareas pueden ser impulsados por el
rendimiento de, y esto es lo que hace run_un til_complete con el objeto wait_coro devuelto por la llamada de
espera . Cuando wait_co ro se ejecuta hasta el final, devuelve una tupla de 2 donde el primer elemento es el
conjunto de futuros completados y el segundo es el conjunto de los no completados. En el ejemplo 18-5, el segundo
conjunto siempre estará vacío; es por eso que lo ignoramos explícitamente asignando a acepta dos argumentos de
_. pero
solo palabras clave que pueden hacer que regrese incluso si algunos de los futuros no están completos: espera
timeout y
return_when. Consulte la documentación de asyncio.wait para obtener más información.
Tenga en cuenta que en el Ejemplo 18-5 no pude reutilizar la función get_flag de flags.py (Ejemplo 17-2) porque usa
la biblioteca de solicitudes , que realiza E/S de bloqueo.
Para aprovechar asyncio, debemos reemplazar cada función que llega a la red con una versión asíncrona que se
invoca con yield from, de modo que el control se devuelva al bucle de eventos. Usar yield from en get_flag significa
que debe controlarse como corolario.
rutina
Es por eso que tampoco pude reutilizar la función download_one de flags_threadpool.py (Ejemplo
17-3) . El código del Ejemplo 18-5 impulsa get_flag con yield_from, por lo que download_one es
también una corrutina. Para cada solicitud, se crea un objeto de corrutina download_one en
download_many, y todos son controlados por la función loop.run_until_com plete , después de ser
envueltos por la corrutina asyncio.wait.
Hay muchos conceptos nuevos que comprender en asyncio, pero la lógica general del Ejemplo 18-5
es fácil de seguir si emplea un truco sugerido por el mismo Guido van Rossum: entrecerrar los ojos y
fingir que el rendimiento de las palabras clave no está allí. Si lo hace, notará que el código es tan
fácil de leer como el código secuencial antiguo.
@asyncio.coroutine
def get_flag(cc): url =
'{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower()) resp = rendimiento
de aiohttp.request('GET' , url) imagen = rendimiento de resp.read()
devolver imagen
El uso de la sintaxis yield from foo evita el bloqueo porque la corrutina actual está suspendida (es
decir, el generador de delegación donde está el código yield from ), pero el flujo de control vuelve al
bucle de eventos, que puede impulsar otras corrutinas. Cuando finaliza foo future o coroutine,
devuelve un resultado a la corrutina suspendida, reanudándola.
Al final de la sección “Uso de yield from” en la página 477, expuse dos hechos sobre cada uso de
yield from. Aquí están, resumidas:
• Cada disposición de corrutinas encadenadas con yield from debe ser impulsada en última
instancia por una persona que llama que no es una corrutina, que invoca next(...) o .send(...) en
el generador de delegación más externo, explícita o implícitamente (por ejemplo, en un bucle
for ). • El subgenerador más interno de la cadena debe ser un generador simple que use solo
rendimiento, o un objeto iterable.
Cuando se usa yield from con la API asyncio , ambos hechos siguen siendo ciertos, con los siguientes
detalles:
• Las cadenas de rutinas que escribimos siempre se controlan al pasar nuestro generador de
delegación más externo a una llamada a la API asíncrona, como loop.run_until_complete (…).
En otras palabras, cuando usamos asyncio , nuestro código no impulsa una cadena de corrutina por
llamar a next(...) o .send(...) en él: el bucle de eventos asyncio hace eso.
• Las cadenas de rutinas que escribimos siempre terminan delegando con rendimiento de a algún
función coroutine asyncio o método coroutine (p. ej., rendimiento de asyn
cio.sleep(…) en el ejemplo 18-2) o rutinas de bibliotecas que implementan
protocolos de nivel superior (p. ej., resp = rendimiento de aiohttp.request('GET', url)
en la corrutina get_flag del ejemplo 18-5).
En otras palabras, el subgenerador más interno será una función de biblioteca que hace el
E/S real, no algo que escribimos.
Para resumir: como usamos asyncio, nuestro código asíncrono consiste en rutinas que
son generadores de delegación impulsados por asyncio y que finalmente delegan a async
corrutinas de la biblioteca cio , posiblemente a través de alguna biblioteca de terceros, como aiohttp.
Este arreglo crea canalizaciones donde el ciclo de eventos asincrónico impulsa, a través de nuestro
coroutines: las funciones de la biblioteca que realizan la E/S asíncrona de bajo nivel.
Ahora estamos listos para responder una pregunta planteada en el Capítulo 17:
• ¿Cómo puede flags_asyncio.py funcionar 5 veces más rápido que flags.py cuando ambos son únicos ?
roscado?
Tabla 18-1. Latencia informática moderna para leer datos de diferentes dispositivos; tercera columna
umn muestra tiempos proporcionales en una escala más fácil de entender para nosotros, humanos lentos
caché L1 3 3 segundos
caché L2 14 14 segundos
Para dar sentido a la Tabla 18-1, tenga en cuenta que las CPU modernas con relojes de GHz ejecutan miles de
millones de ciclos por segundo. Digamos que una CPU ejecuta exactamente mil millones de ciclos por segundo.
Esa CPU puede hacer 333,333,333 lecturas de caché L1 en un segundo, o 4 (¡cuatro!) lecturas de red al mismo
tiempo. La tercera columna de la tabla 18-1 pone esos números en perspectiva al multiplicar la segunda columna
por un factor constante. Entonces, en un universo alternativo, si una lectura del caché L1 tomó 3 segundos,
¡entonces una lectura de la red tomaría 7.6 años!
Hay dos formas de evitar el bloqueo de llamadas para detener el progreso de toda la aplicación:
Los subprocesos funcionan bien, pero la sobrecarga de memoria para cada subproceso del sistema operativo
(del tipo que usa Python) es del orden de megabytes, según el sistema operativo. No podemos permitirnos un
hilo por conexión si manejamos miles de conexiones.
Las devoluciones de llamada son la forma tradicional de implementar llamadas asincrónicas con poca sobrecarga
de memoria. Son un concepto de bajo nivel, similar al mecanismo de concurrencia más antiguo y primitivo de
todos: las interrupciones de hardware. En lugar de esperar una respuesta, registramos una función para llamarla
cuando suceda algo. De esta manera, cada llamada que hagamos puede ser sin bloqueo. Ryan Dahl aboga por
las devoluciones de llamada por su simplicidad y bajo costo operativo.
Por supuesto, solo podemos hacer que las devoluciones de llamada funcionen porque el bucle de eventos
subyacente a nuestras aplicaciones asincrónicas puede depender de una infraestructura que utiliza
interrupciones, subprocesos, sondeos, procesos en segundo plano, etc. done.5 Cuando el bucle de eventos
obtiene una respuesta, vuelve a llamar a nuestro código. Pero el único hilo principal compartido por el bucle de
eventos y el código de nuestra aplicación nunca se bloquea, si no cometemos errores.
Cuando se usan como corrutinas, los generadores brindan una forma alternativa de realizar programación
asíncrona. Desde la perspectiva del bucle de eventos, invocar una devolución de llamada o llamar a .send() en
una rutina suspendida es prácticamente lo mismo. Hay una sobrecarga de memoria para cada rutina suspendida,
pero es mucho menor que la sobrecarga de cada subproceso. Y evitan el temido "infierno de devolución de
llamada", del que hablaremos en "De las devoluciones de llamada a futuros y rutinas" en la página 562.
Ahora, la ventaja de cinco veces en rendimiento de flags_asyncio.py sobre flags.py debería tener sentido:
flags.py gasta miles de millones de ciclos de CPU esperando cada descarga, uno tras otro. Mientras tanto, la
CPU está haciendo mucho, simplemente no está ejecutando su programa. Por el contrario, cuando se llama a
loop_until_complete en la función download_many de
5. De hecho, aunque Node.js no admite subprocesos a nivel de usuario escritos en JavaScript, detrás de escena implementa un grupo de
subprocesos en C con la biblioteca libeio, para proporcionar sus API de archivo basadas en devolución de llamada, porque a partir de 2014
no existen API de manejo de archivos asincrónicos estables y portátiles para la mayoría de los sistemas operativos.
A medida que la infraestructura asyncio obtiene la primera respuesta, el bucle de eventos la envía a
la corrutina get_flag en espera . Cuando get_flag obtiene una respuesta, avanza al siguiente
rendimiento, que llama a resp.read() y devuelve el control al bucle principal. Otras respuestas llegan
en estrecha sucesión (porque se dieron casi al mismo tiempo). A medida que regresa get_flag , el
generador de delegación download_flag se reanuda y guarda el archivo de imagen.
Debido a que las operaciones asincrónicas están intercaladas, el tiempo total necesario para
descargar muchas imágenes al mismo tiempo es mucho menor que hacerlo secuencialmente. Al
realizar 600 solicitudes HTTP con asyncio , obtuve todos los resultados más de 70 veces más rápido
que con un script secuencial.
Ahora volvamos al ejemplo del cliente HTTP para ver cómo podemos mostrar una barra de progreso
animada y realizar el manejo de errores adecuado.
Recuerde de “Descargas con pantalla de progreso y manejo de errores” en la página 520 que el
conjunto de ejemplos flags2 comparte la misma interfaz de línea de comandos. Esto incluye
flags2_asyncio.py que analizaremos en esta sección. Por ejemplo, el Ejemplo 18-6 muestra cómo
obtener 100 indicadores (-al 100) del servidor ERROR , utilizando 100 solicitudes simultáneas (-m 100).
--------------------
73 banderas descargadas.
27 errores.
Tiempo transcurrido: 0,64 s
Uso de asyncio.as_completed En el
Ejemplo 18-5, pasé una lista de corrutinas a asyncio.wait, las cuales, cuando son impulsadas por
loop.run_until.complete, devolverían los resultados de las descargas cuando todas estuvieran listas.
Pero para actualizar una barra de progreso, necesitamos obtener resultados a medida que se realizan.
Afortunadamente, existe un equivalente asyncio de la función generadora as_completed que usamos
en el ejemplo del grupo de subprocesos con la barra de progreso (Ejemplo 17-14).
Escribir un ejemplo de flags2 para aprovechar asyncio implica reescribir varias funciones que la
versión concurrent.future podría reutilizar. Esto se debe a que solo hay un subproceso principal en un
programa asyncio y no podemos darnos el lujo de tener llamadas de bloqueo en ese subproceso, ya
que es el mismo subproceso que ejecuta el ciclo de eventos. Así que tuve que reescribir get_flag para
usar yield from para todos los accesos a la red. Ahora get_flag es una corrutina, por lo que
download_one debe manejarlo con rendimiento desde, por lo tanto , download_one se convierte en
una corrutina. Previamente, en el Ejemplo 18-5, download_one fue impulsado por download_many:
las llamadas para descargar load_one se envolvieron en una llamada asyncio.wait y se pasaron a
loop.run_until_com plete. Ahora necesitamos un control más preciso para los informes de progreso y
el manejo de errores, así que moví la mayor parte de la lógica de download_many a una nueva
corrutina downloader_coro , y usé download_many solo para configurar el ciclo de eventos y programar downloader_coro.
El ejemplo 18-7 muestra la parte superior del script flags2_asyncio.py donde se definen las corrutinas
get_flag y download_one . El ejemplo 18-8 enumera el resto de la fuente, con downloader_coro y
download_many.
Ejemplo 18-7. flags2_asincio.py: parte superior del script; el código restante está en el ejemplo 18-8
importar
colecciones de importación asyncio
importar aiohttp
desde aiohttp importar web
importar tqdm
# valor predeterminado bajo para evitar errores del sitio remoto, como
# el Servicio 503 no esta disponible por el momento
DEFAULT_CONCUR_REQ = 5
MAX_CONCUR_REQ = 1000
@asyncio.coroutine def
get_flag(base_url, cc):
url = '{}/{cc}/{cc}.gif'.format(base_url, cc=cc.lower()) resp = rendimiento de
aiohttp.request('GET', url) si resp.status == 200 : image = rendimiento de
resp.read() return image elif resp.status == 404:
aumentar web.HTTPNotFound()
más:
generar aiohttp.HttpProcessingError(
código=resp.estado, mensaje=resp.razón,
encabezados=resp.encabezados)
@asyncio.coroutine def
download_one(cc, base_url, semáforo, detallado):
si detallado y msg:
imprimir (cc, msg)
Un semáforo se usa como administrador de contexto en una expresión yield from para que el sistema en su
conjunto no se bloquee: solo se bloquea esta corrutina mientras el contador de semáforos está en el número
máximo permitido.
Cuando esta declaración with sale, el contador de semáforos se reduce, desbloqueando alguna otra instancia
Cualquier otra excepción se informará como FetchError con el código de país y la excepción original
encadenada usando la sintaxis de aumento de X de Y introducida en PEP 3134 — Encadenamiento de
excepciones y seguimientos integrados.
En el Ejemplo 18-7, puede ver que el código para get_flag y download_one cambió significativamente con respecto a
la versión secuencial porque estas funciones ahora son corrutinas que usan yield from para realizar llamadas
asincrónicas.
El código de cliente de red del tipo que estamos estudiando siempre debe usar algún mecanismo de limitación para
evitar golpear al servidor con demasiadas solicitudes simultáneas; el rendimiento general del sistema puede degradarse
si el servidor está sobrecargado. En flags2_threadpool.py (Ejemplo 17-14), la limitación se realizó creando una instancia
de ThreadPoolExecutor con el argumento max_workers requerido establecido en concur_req en la función
download_many , por lo que solo se inician subprocesos concur_req en el grupo. En flags2_asyncio.py, utilicé
asyncio.Semaphore, que se crea mediante la función download er_coro (que se muestra a continuación, en el ejemplo
18-8) y se pasa como argumento del semáforo a download_one en el ejemplo 18-7.
Un semáforo es un objeto que contiene un contador interno que se reduce cada vez
que llamamos al método de rutina .acquire() y se incrementa cuando llamamos al
método de rutina .release() . El valor inicial del contador se establece cuando se
instancia el Semáforo , como en esta línea de downloader_coro:
semáforo = asyncio.Semaphore(concur_req)
6. Gracias a Guto Maia quien notó que Semaphore no fue explicado en el borrador del libro.
Ahora echemos un vistazo al resto de la secuencia de comandos en el Ejemplo 18-8. Tenga en cuenta que la mayoría de las funciones
de la antigua función download_many ahora está en una rutina, downloader_coro. Esto era
necesario porque debemos usar el rendimiento de para recuperar los resultados de los futuros producidos
por asyncio.as_completed, por lo tanto , as_completed debe invocarse en una rutina.
Sin embargo, no podía simplemente convertir download_many en una corrutina, porque debo aprobar
a la función principal de flags2_common en la última línea del script, y ese principal
la función no espera una rutina, solo una función simple. Por lo tanto creé abajo
loader_coro para ejecutar el ciclo as_completed , y ahora download_many simplemente configura
el bucle de eventos y programa downloader_coro pasándolo a loop.run_until_com
completo
@asincio.coroutine
def downloader_coro(cc_list, base_url, detallado, concur_req):
contador = colecciones.Contador()
semáforo = asyncio.Semaphore(concur_req) to_do =
[download_one(cc, base_url, semaphore, detallado)
para cc en ordenados (cc_list)]
to_do_iter = asyncio.as_completed(to_do) si no es
detallado:
to_do_iter = tqdm.tqdm(to_do_iter, total=len(cc_list)) para el futuro en
to_do_iter: pruebe:
error_msg = exc.__cause__.args[0]
excepto IndexError:
error_msg = exc.__causa__.__clase__.__nombre__
si detallado y error_msg:
msg = '*** Error para {}: {}'
print(msg.format(country_code, error_msg))
estado = HTTPStatus.error
más:
estado = res.estado
contador[estado] += 1
contador de retorno
el regreso cuenta
si __nombre__ == '__principal__':
principal (descargar_muchos, DEFAULT_CONCUR_REQ, MAX_CONCUR_REQ)
Coroutine recibe los mismos argumentos que download_many, pero no se puede invocar
directamente desde main precisamente porque es una función coroutine y no una función simple
como download_many.
Cree un asyncio.Semaphore que permita hasta concur_req corrutinas activas entre aquellos que
usan este semáforo.
Cree una lista de objetos de corrutina, uno por llamada a la corrutina download_one .
Iterar sobre los futuros completados; este ciclo es muy similar al de down load_many en el Ejemplo
17-14; la mayoría de los cambios tienen que ver con el manejo de excepciones debido a las
diferencias en las bibliotecas HTTP (solicitudes versus aiohttp).
La forma más sencilla de recuperar el resultado de asyncio.Future es usar yield from en lugar de
llamar a future.result().
Conteo de resultados.
download_many simplemente crea una instancia de la rutina y la pasa al bucle de eventos con
run_until_complete.
Cuando haya terminado todo el trabajo, apague el bucle de eventos y devuelva los recuentos.
En el Ejemplo 18-8, no pudimos usar la asignación de futuros a códigos de países que vimos en el
Ejemplo 17-14 porque los futuros devueltos por asyncio.as_completed no son necesariamente los
mismos futuros que pasamos a la llamada as_completed . Internamente, la maquinaria asyncio
reemplaza los objetos futuros que proporcionamos por otros que, al final, producirán los mismos
resultados.7
Debido a que no podía usar los futuros como claves para recuperar el código de país de un dictado
en caso de falla, implementé la excepción FetchError personalizada (que se muestra en el Ejemplo
18-7). FetchError envuelve una excepción de red y mantiene el código de país asociado a ella, por
lo que el código de país se puede informar con el error en modo detallado.
Si no hay error, el código de país está disponible como resultado del rendimiento de la expresión
futura en la parte superior del ciclo for .
Mientras discutía el Ejemplo 18-7, noté que save_flag realiza E/S de disco y debe ejecutarse de
forma asíncrona. La siguiente sección muestra cómo.
comunidad de Python, tendemos a pasar por alto el hecho de que el acceso al sistema de archivos
local está bloqueando, racionalizando que no sufre la mayor latencia del acceso a la red (que también
es peligrosamente impredecible). Por el contrario, a los programadores de Node.js se les recuerda
constantemente que todas las funciones del sistema de archivos se bloquean porque sus firmas
requieren una devolución de llamada. Recuerde de la Tabla 18-1 que el bloqueo de E/S de disco
desperdicia millones de ciclos de CPU, y esto puede tener un impacto significativo en el rendimiento de la aplicación.
ción
En el ejemplo 18-7, la función de bloqueo es save_flag. En la versión con subprocesos del script
(Ejemplo 17-14), save_flag bloquea el subproceso que ejecuta la función download_one , pero ese
es solo uno de varios subprocesos de trabajo. Detrás de escena, la llamada de E/S de bloqueo libera
el GIL, por lo que puede continuar otro subproceso. Pero en flags2_asyncio.py, save_flag bloquea el
único subproceso que nuestro código comparte con el bucle de eventos asyncio .
7. Se puede encontrar una discusión detallada sobre esto en un hilo que comencé en el grupo python-tulip,
titulado "¿Qué otros futuros salen de asyncio.as_completed?". Guido responde y da una idea de la
implementación de as_completed, así como de la estrecha relación entre futuros y rutinas en asyncio.
Por lo tanto, toda la aplicación se congela mientras se guarda el archivo. La solución a este problema es el método
run_in_executor del objeto de bucle de eventos.
Detrás de escena, el bucle de eventos asyncio tiene un ejecutor de grupo de subprocesos, y puede enviar llamadas
para que las ejecute con run_in_executor. Para usar esta característica en nuestro ejemplo, solo se necesita
cambiar unas pocas líneas en la corrutina download_one , como se muestra en el Ejemplo 18-9.
Ejemplo 18-9. flags2_asyncio_executor.py: uso del ejecutor de grupo de subprocesos predeterminado para ejecutar
save_flag
@asyncio.coroutine
def download_one(cc, base_url, semáforo, detallado):
si detallado y msg:
imprimir (cc, msg)
La ventaja de las corrutinas sobre las devoluciones de llamada se hace evidente cuando necesitamos coordinar
solicitudes asincrónicas, y no solo hacer solicitudes completamente independientes. La siguiente sección explica el
problema y la solución.
Cualquiera con algo de experiencia en la programación orientada a eventos de estilo de devolución de llamada conoce el
término "infierno de devolución de llamada": el anidamiento de devoluciones de llamada cuando una operación depende
del resultado de la operación anterior. Si tiene tres llamadas asincrónicas que deben ocurrir en sucesión, debe codificar
las devoluciones de llamada anidadas en tres niveles de profundidad. El ejemplo 18-10 es un ejemplo en JavaScript.
Ejemplo 18-10. Infierno de devolución de llamada en JavaScript: funciones anónimas anidadas, también conocido como
Pyraÿ mid of Doom
}); });
});
En el Ejemplo 18-10, api_call1, api_call2 y api_call3 son funciones de biblioteca que usa su
código para recuperar resultados de forma asíncrona; tal vez api_call1 va a una base de datos y
api_call2 obtiene datos de un servicio web, por ejemplo. Cada uno de estos toma una función de
devolución de llamada, que en JavaScript a menudo son funciones anónimas (se denominan
etapa1, etapa2 y etapa3 en el siguiente ejemplo de Python). El paso 1, el paso 2 y el paso 3 aquí
representan funciones regulares de su aplicación que procesan las respuestas recibidas por las
devoluciones de llamada.
def etapa1(respuesta1):
solicitud2 = paso1(respuesta1)
api_call2(solicitud2, etapa2)
def etapa2(respuesta2):
solicitud3 = paso2 (respuesta2)
api_call3(solicitud3, etapa3)
def etapa3(respuesta3):
paso3(respuesta3)
Aunque el código del Ejemplo 18-11 está organizado de manera muy diferente al Ejemplo 18-10, hacen
exactamente lo mismo, y el ejemplo de JavaScript podría escribirse usando el mismo arreglo (pero el
código de Python no puede escribirse en el estilo de JavaScript debido a las limitaciones sintácticas de
lambda).
El código organizado como Ejemplo 18-10 o Ejemplo 18-11 es difícil de leer, pero es aún más difícil de
escribir: cada función hace parte del trabajo, configura la siguiente devolución de llamada y regresa
para permitir que el ciclo de eventos continúe. En este punto, todo el contexto local se pierde. Cuando
se ejecuta la siguiente devolución de llamada (por ejemplo, etapa2) , ya no tiene el valor de solicitud2 .
Si lo necesita, debe confiar en cierres o estructuras de datos externas para almacenarlo entre las
diferentes etapas del procesamiento.
Ahí es donde las rutinas realmente ayudan. Dentro de una rutina, para realizar tres acciones asíncronas
en sucesión, debe ceder tres veces para permitir que el ciclo de eventos continúe ejecutándose.
Cuando un resultado está listo, la rutina se activa con una llamada .send() . Desde la perspectiva del
bucle de eventos, es similar a invocar una devolución de llamada. Pero para los usuarios de una API
asincrónica de estilo corrutina, la situación mejora enormemente: la secuencia completa de tres
operaciones está en un cuerpo de función, como un código secuencial antiguo con variables locales
para retener el contexto de la tarea general en curso. Vea el Ejemplo 18-12.
Ejemplo 18-12. Las rutinas y el rendimiento de habilitar la programación asíncrona sin devoluciones de
llamada
@asyncio.coroutine
define tres_etapas(solicitud1):
respuesta1 = rendimiento de llamada_api1(solicitud1)
# etapa 1 solicitud2 = paso1(respuesta1) respuesta2
= rendimiento de llamada_api2(solicitud2) # etapa
2 solicitud3 = paso2 (respuesta2) respuesta3 =
rendimiento de llamada_api3(solicitud3) # etapa 3
paso3(respuesta3)
El ejemplo 18-12 es mucho más fácil de seguir que los ejemplos anteriores de JavaScript y Python: las tres
etapas de la operación aparecen una tras otra dentro de la misma función.
Esto hace que sea trivial usar resultados previos en el procesamiento de seguimiento. También proporciona un
contexto para el informe de errores a través de excepciones.
Suponga que en el Ejemplo 18-11 el procesamiento de la llamada api_call2(solicitud2, etapa2) genera una
excepción de E/S (esa es la última línea de la función etapa1 ). La excepción no se puede capturar en la etapa
1 porque api_call2 es una llamada asíncrona: regresa inmediatamente, antes de que se realice cualquier E/S.
En las API basadas en devolución de llamada, esto se soluciona registrando dos devoluciones de llamada para
cada llamada asíncrona: una para manejar el resultado de operaciones exitosas, otra para manejar errores.
Las condiciones de trabajo en el infierno de la devolución de llamada se deterioran rápidamente cuando se
trata de un manejo de errores.
Por el contrario, en el ejemplo 18-12, todas las llamadas asincrónicas para esta operación de tres etapas están
dentro de la misma función, three_stages, y si las llamadas asincrónicas api_call1, api_call2 y api_call3 generan
excepciones, podemos manejarlas poniendo los respectivos rendimiento de las líneas dentro de los bloques
try/except .
Este es un lugar mucho mejor que el infierno de la devolución de llamadas, pero no lo llamaría el paraíso de
las corrutinas porque hay un precio que pagar. En lugar de funciones regulares, debe usar rutinas y
acostumbrarse a ceder, así que ese es el primer obstáculo. Una vez que escribes yield from en una función,
ahora es una corrutina y no puedes simplemente llamarla, como llamamos api_call1(re quest1, stage1) en el
ejemplo 18-11 para iniciar la cadena de devolución de llamada. Debe programar explícitamente la ejecución de
la corrutina con el bucle de eventos, o activarla usando yield from en otra corrutina que esté programada para
su ejecución. Sin la llamada loop.cre ate_task(tres_etapas(solicitud1)) en la última línea, no sucedería nada en
el Ejemplo 18-12.
que desea guardar la bandera de cada país con el nombre del país y el código del país, en lugar de solo el
código del país. Ahora debe realizar dos solicitudes HTTP por bandera: una para obtener la imagen de la
bandera en sí, la otra para obtener el archivo metadata.json en el mismo directorio que la imagen: ahí es donde
se registra el nombre del país.
Articular múltiples solicitudes en la misma tarea es fácil en la secuencia de comandos: solo haga una solicitud
y luego la otra, bloquee el hilo dos veces y mantenga ambos datos (código de país y nombre) en variables
locales, listos para usar al guardar los archivos. . Si necesita hacer lo mismo en una secuencia de comandos
asincrónica con devoluciones de llamadas, comienza a oler el azufre del infierno de las devoluciones de
llamadas: el código de país y el nombre deberán pasarse en un
cierre o retenido en algún lugar hasta que pueda guardar el archivo porque cada devolución de llamada se
ejecuta en un contexto local diferente. Las rutinas y el rendimiento de proporcionan alivio de eso. La solución
no es tan simple como con los hilos, pero es más manejable que las devoluciones de llamada encadenadas
o anidadas.
El ejemplo 18-13 muestra el código de la tercera variación del script de descarga de banderas asyncio ,
usando el nombre del país para guardar cada bandera. download_many y download er_coro no han
cambiado desde flags2_asyncio.py (Ejemplos 18-7 y 18-8). Los cambios
son:
download_one
Esta corrutina ahora usa yield from para delegar en get_flag y la nueva corrutina get_count.
get_flag
La mayor parte del código de esta corrutina se movió a una nueva corrutina http_get para que
get_country también pueda usarla.
get_country
Esta rutina obtiene el archivo metadata.json para el código de país y obtiene el nombre del país.
http_get
Código común para obtener un archivo de la Web.
Ejemplo 18-13. flags3_asincio.py: más delegación de rutinas para realizar dos solicitudes por bandera
@asyncio.coroutine
def http_get(url): res
= rendimiento de aiohttp.request('GET', url) si
res.status == 200:
ctype = res.headers.get('Content-type', '').lower() if 'json' in ctype
or url.endswith('json'): data = yield from res.json() else: data =
rendimiento de res.read () datos de retorno
@asyncio.coroutine
def get_country(base_url, cc):
url = '{}/{cc}/metadata.json'.format(base_url, cc=cc.inferior())
@asyncio.coroutine
def get_flag(base_url, cc):
url = '{}/{cc}/{cc}.gif'.format(base_url, cc=cc.lower()) return (rendimiento
de http_get(url))
@asyncio.coroutine
def download_one(cc, base_url, semáforo, detallado):
mensaje = 'OK'
si detallado y msg:
imprimir (cc, msg)
Si el tipo de contenido tiene 'json' o la URL termina con .json, use el método de respuesta .json()
para analizarlo y devolver una estructura de datos de Python, en este caso, un dict.
De lo contrario, use .read() para obtener los bytes tal como están.
los metadatos recibirán un dict de Python creado a partir del contenido de JSON.
Los paréntesis exteriores aquí son necesarios porque el analizador de Python se confunde y
produce un error de sintaxis cuando ve que las palabras clave devuelven el rendimiento
alineadas de esa manera.
Pongo las llamadas a get_flag y get_country por separado con bloques controlados por el
semáforo porque quiero mantenerlo adquirido el menor tiempo posible.
El resultado de la sintaxis aparece nueve veces en el ejemplo 18-13. A estas alturas, ya debería entender
cómo se usa esta construcción para delegar de una rutina a otra sin bloquear el ciclo de eventos.
El desafío es saber cuándo tiene que usar el rendimiento y cuándo no puede usarlo.
La respuesta, en principio, es fácil, se produce a partir de corrutinas y asyncio . Instancias futuras ,
incluidas las tareas. Pero algunas API son engañosas, ya que mezclan corrutinas y funciones sencillas
de formas aparentemente arbitrarias, como la clase StreamWriter que usaremos en uno de los servidores
de la siguiente sección.
El ejemplo 18-13 concluye el conjunto de ejemplos flags2 . Lo animo a que juegue con ellos para
desarrollar una intuición de cómo funcionan los clientes HTTP simultáneos. Utilice las opciones de línea
de comandos -a, -e y -l para controlar la cantidad de descargas y la opción -m para establecer la cantidad
de descargas simultáneas. Ejecute pruebas contra los servidores LOCAL, REMOTO, DELAY y ERROR .
Descubra el número óptimo de descargas simultáneas para maximizar el rendimiento en cada servidor.
Modifique la configuración del script vaurien_error_delay.sh para agregar o eliminar errores y retrasos.
Figura 18-2. Una sesión de Telnet con el servidor tcp_charfinder.py: consultando "ajedrez negro" y "sol".
mayor parte de la lógica en estos ejemplos está en el módulo charfinder.py , que no tiene nada de
concurrente. Puede usar charfinder.py como un buscador de caracteres de línea de comandos, pero lo
que es más importante, fue diseñado para proporcionar contenido para nuestros servidores asyncio . El
código para charfinder.py está en el repositorio de código de Fluent Python .
El módulo charfinder indexa cada palabra que aparece en los nombres de los personajes en la base de
datos Unicode incluida con Python y crea un índice invertido almacenado en un dictado. Por ejemplo,
la entrada de índice invertida para la clave 'SUN' contiene un conjunto con los 10 caracteres Unicode
que tienen esa palabra en sus nombres. El índice invertido se guarda
importar asyncio
CRLF = b'\r\n'
PROMPT = b'?> '
índice = UnicodeNameIndex()
@asincio.coroutine
def handle_queries(lector, escritor): while
True:
escritor.escribir(PROMPT) # ¡no se puede producir!
rendimiento de escritor.drain() # debe rendirse de! data
= rendimiento de reader.readline() intente:
consulta = datos.decodificar().strip()
excepto UnicodeDecodeError:
consulta = '\x00'
cliente = escritor.get_extra_info('peername')
print('Recibido de {}: {!r}'.format(cliente, consulta)) if consulta:
rendimiento de escritor.drain()
print('Resultados enviados {} '. format(len(líneas)))
Este ciclo maneja una sesión que dura hasta que se recibe cualquier carácter de control del cliente.
El método StreamWriter.write no es una rutina, solo una función simple; esta línea envía el indicador ?
>.
StreamWriter.drain vacía el búfer del escritor ; es una rutina, por lo que debe
llamarse con yield from.
StreamWriter.readline es una rutina; devuelve bytes.
Un UnicodeDecodeError puede ocurrir cuando el cliente Telnet envía caracteres de control; si eso
sucede, pretendemos que se envió un carácter nulo, por simplicidad.
Esto devuelve la dirección remota a la que está conectado el socket.
Esto devuelve un generador que genera cadenas con el punto de código Unicode, el carácter real y
su nombre (p. ej., U+0039\t9\tDIGIT NINE); para simplificar, construyo una lista a partir de ella.
Envíe las líneas convertidas a bytes utilizando la codificación UTF-8 predeterminada , agregando un
retorno de carro y un avance de línea a cada una; tenga en cuenta que el argumento es una expresión
generadora.
La corrutina handle_queries tiene un nombre plural porque inicia una sesión interactiva y maneja múltiples
consultas de cada cliente.
8. Leonardo Rochael señaló que la creación de UnicodeNameIndex podría delegarse en otro subproceso mediante
loop.run_with_executor() en la función principal del Ejemplo 18-15, de modo que el servidor estaría listo para recibir
solicitudes de inmediato mientras se crea el índice. Eso es cierto, pero consultar el índice es lo único que hace esta
aplicación, por lo que no sería una gran victoria. Sin embargo, es un ejercicio interesante para hacer como sugiere
Leo. Adelante, hazlo, si quieres.
Tenga en cuenta que toda la E/S en el Ejemplo 18-14 está en bytes. Necesitamos decodificar las cadenas recibidas
de la red y codificar cadenas enviadas. En Python 3, la codificación predeterminada es
UTF-8, y eso es lo que estamos usando implícitamente.
Una advertencia es que algunos de los métodos de E/S son rutinas y deben controlarse con
rendimiento de, mientras que otros son funciones simples. Por ejemplo, StreamWriter.write es un
función simple, en el supuesto de que la mayoría de las veces no se bloquea porque
escribe en un búfer. Por otro lado, StreamWriter.drain, que vacía el búfer
y realiza la E/S real es una corrutina, al igual que Streamreader.readline. Mientras yo estaba
Al escribir este libro, una mejora importante en los documentos de la API de asyncio fue el etiquetado claro
de rutinas como tales.
El ejemplo 18-15 enumera la función principal del módulo iniciado en el ejemplo 18-14.
host = server.sockets[0].getsockname()
print('Serviendo en {}. Presiona CTRL-C para detener.'.format(host))
prueba:
loop.run_forever()
excepto KeyboardInterrupt: # CTRL+C presionado
pasar
print('Servidor apagándose.')
servidor.cerrar()
bucle.ejecutar_hasta_completo(servidor.esperar_cerrado())
bucle.cerrar()
si __nombre__ == '__principal__':
principal(*sys.argv[1:])
…mostrar en la consola del servidor. Esta es la primera salida generada por este script.
en la consola del servidor.
Ejecute el bucle de eventos; aquí es donde main se bloqueará hasta que se elimine cuando se
presione CTRL-C en la consola del servidor.
Cierra el servidor.
server.wait_closed() devuelve un futuro; usa loop.run_until_complete para dejar que el futuro haga
su trabajo.
Este es un atajo para manejar argumentos de línea de comandos opcionales: explotar sys.argv[1:] y
pasarlo a una función principal con argumentos predeterminados adecuados.
Observe cómo run_until_complete acepta una corrutina (el resultado de start_serv er) o un futuro (el resultado
de server.wait_closed). Si run_until_complete obtiene una rutina como argumento, envuelve la rutina en una
Tarea.
Puede que le resulte más fácil entender cómo fluye el control en tcp_charfinder.py si observa detenidamente
el resultado que genera en la consola del servidor, que se muestra en el ejemplo 18-16.
Ejemplo 18-16. tcp_charfinder.py: este es el lado del servidor de la sesión que se muestra en la Figura 18-2
$ python3 tcp_charfinder.py
Sirviendo en ('127.0.0.1', 2323). Presiona CTRL-C para detener.
Recibido de ('127.0.0.1', 62910): 'ajedrez negro'
Enviado 6
resultados Recibido de ('127.0.0.1', 62910): 'sol'
Enviado 10
resultados Recibido de ('127.0.0.1', 62910): '\x00'
Cerrar el socket del cliente
El socket del cliente está cerrado pero el servidor aún se está ejecutando, listo para atender a otro
cliente.
Observe cómo main muestra casi inmediatamente el mensaje Sirviendo en... y los bloques en la llamada
loop.run_forever() . En ese momento, el control fluye hacia el bucle de eventos y permanece allí, regresando
ocasionalmente a la rutina handle_queries , que devuelve el control al bucle de eventos cada vez que necesita
esperar a que la red envíe o reciba datos. Mientras el bucle de eventos esté activo, se iniciará una nueva
instancia de la rutina handle_queries para cada cliente que se conecte al servidor. De esta manera, múltiples
clientes pueden ser
manejado simultáneamente por este servidor simple. Esto continúa hasta que se produce una interrupción de
KeyboardInter o el sistema operativo elimina el proceso.
El código tcp_charfinder.py aprovecha la API de flujos asyncio de alto nivel que proporciona un servidor listo
para usar, por lo que solo necesita implementar una función de controlador, que puede ser una simple
devolución de llamada o una corrutina. También hay una API de Transportes y Protocolos de nivel inferior,
inspirada en las abstracciones de transporte y protocolos en el marco Twisted.
Consulte la documentación de transportes y protocolos de asyncio para obtener más información, incluido un
servidor de eco TCP implementado con esa API de nivel inferior.
biblioteca aiohttp que usamos para los ejemplos de banderas asyncio también es compatible con HTTP del lado
del servidor, así que eso es lo que usé para implementar el script http_charfinder.py . La Figura 18-3 muestra la
interfaz web simple del servidor, que muestra el resultado de una búsqueda de un emoji de "cara de gato".
Figura 18-3. Ventana del navegador que muestra los resultados de la búsqueda de "cara de gato" en el servidor
http_charÿ finder.py
Algunos navegadores son mejores que otros para mostrar Unicode. los
La captura de pantalla de la Figura 18-3 fue capturada con Firefox en OS X, y
Obtuve el mismo resultado con Safari. Pero Chrome actualizado y Opÿ
Los navegadores de la era en la misma máquina no mostraban caracteres emoji.
ters como las caras de gato. Otros resultados de búsqueda (p. ej., "ajedrez") buscaron
bien, por lo que es probable que sea un problema de fuente en Chrome y Opera en OSX.
loop.run_forever() excepto
KeyboardInterrupt: # CTRL+C presionado
pasar
print('Servidor apagándose.')
bucle.cerrar()
si __nombre__ == '__principal__':
principal(*sys.argv[1:])
…con rutas que asignan patrones de URL a funciones de controlador; aquí GET / se enruta
a la función de inicio (vea el Ejemplo 18-18).
Ejecute el bucle de eventos; main se bloqueará aquí mientras el bucle de eventos esté bajo control.
A medida que se familiarice con la API de asyncio , es interesante contrastar cómo se configuran los servidores
en el Ejemplo 18-17 y en el ejemplo de TCP (Ejemplo 18-15) que se mostró anteriormente.
En el ejemplo anterior de TCP, el servidor fue creado y programado para ejecutarse en la función principal con
estas dos líneas:
Pero init en sí mismo es una rutina, y lo que hace que se ejecute es la función principal , con esta línea:
Tanto asyncio.start_server como loop.create_server son rutinas que devuelven objetos asyncio.Server . Para
iniciar un servidor y devolverle una referencia, cada una de estas corrutinas debe completarse. En el ejemplo de
TCP, eso se hizo llamando a loop.run_until_complete(server_coro), donde server_coro fue el resultado de
asyncio.start_server. En el ejemplo de HTTP, create_server se invoca en una expresión yield_from dentro de la
corrutina init , que a su vez es impulsada por la función principal cuando llama a loop.run_until_complete(init(...)).
Menciono esto para enfatizar este hecho esencial que hemos discutido antes: una corrutina solo hace algo cuando
se activa, y para controlar una asyncio.coroutine , usa yield from o la pasa a una de varias funciones asyncio que
toman coroutine o argumentos futuros. mentos, como run_until_complete.
El ejemplo 18-18 muestra la función de inicio , que está configurada para manejar la URL / (raíz) en nuestro
servidor HTTP.
def inicio(solicitud):
consulta = solicitud.GET.get('consulta', '').strip()
print('Consulta: {!r}'.formato(consulta)) if consulta:
descripciones = lista(index. find_descriptions(query))
res = '\n'.join(ROW_TPL.format(**vars(descr)) for descr in
descriptions)
res = ''
msg = 'Ingrese palabras que describan caracteres.'
Si hubo una consulta, vincule res a las filas de la tabla HTML representadas desde el resultado de la consulta
al índice y msg a un mensaje de estado.
Tenga en cuenta que home no es una corrutina, y no necesita serlo si no hay rendimiento de expresiones en ella. La
documentación de aiohttp para el método add_route establece que el controlador "se convierte en rutina internamente
cuando es una función normal".
Hay una desventaja en la simplicidad de la función de inicio en el ejemplo 18-18. El hecho de que sea una función
simple y no una corrutina es un síntoma de un problema mayor: la necesidad de repensar cómo codificamos las
aplicaciones web para lograr una alta concurrencia. Consideremos esto
asunto.
función de inicio en el Ejemplo 18-18 se parece mucho a una función de vista en Django o Flask. No hay nada asíncrono
en su implementación: recibe una solicitud, obtiene datos de una base de datos y genera una respuesta al mostrar una
página HTML completa. En este ejemplo, la "base de datos" es el objeto UnicodeNameIndex , que está en la memoria.
Pero el acceso a una base de datos real debe hacerse de forma asíncrona, de lo contrario, está bloqueando el ciclo de
eventos mientras espera los resultados de la base de datos. Por ejemplo, el paquete aiopg proporciona un controlador
PostgreSQL asíncrono compatible con asyncio; le permite usar yield from para enviar consultas y obtener resultados,
por lo que su función de visualización puede comportarse como una rutina adecuada.
Además de evitar el bloqueo de llamadas, los sistemas altamente concurrentes deben dividir grandes porciones de
trabajo en partes más pequeñas para seguir respondiendo. El servidor http_charfinder.py ilustra esto
Importante: si busca “cjk”, obtendrá 75.821 ideogramas chinos, japoneses y coreanos.9 En este caso, la función de inicio
devolverá un documento HTML de 5,3 MB, con una tabla con 75.821 filas.
En mi máquina, toma 2 segundos obtener la respuesta a la consulta "cjk", usando el cliente HTTP de línea de comando
curl desde un servidor http_charfinder.py local. Un navegador tarda aún más en diseñar la página con una tabla tan
grande. Por supuesto, la mayoría de las consultas devuelven respuestas mucho más pequeñas: una consulta de "braille"
devuelve 256 filas en una página de 19 KB y tarda 0,017 s en mi máquina. Pero si el servidor dedica 2 segundos a
atender una sola consulta "cjk", todos los demás clientes esperarán al menos 2 segundos, y eso no es aceptable.
La forma de evitar el problema de la respuesta larga es implementar la paginación: devolver resultados con un máximo
de, digamos, 200 filas y hacer que el usuario haga clic o desplace la página para obtener más. Si busca el módulo
charfinder.py en el repositorio de código Fluent Python, verá que el método UnicodeNameIndex.find_descriptions toma
argumentos de inicio y finalización opcionales : son compensaciones para admitir la paginación. Por lo tanto, podría
devolver los primeros 200 resultados, luego usar AJAX o incluso WebSockets para enviar el siguiente lote cuando, y si,
el usuario desea verlo.
La mayor parte de la codificación necesaria para enviar los resultados por lotes estaría en el navegador.
Esto explica por qué Google y todas las propiedades de Internet a gran escala dependen de una gran cantidad de
codificación del lado del cliente para crear sus servicios: los clientes asincrónicos inteligentes hacen un mejor uso del servidor
recursos.
Aunque los clientes inteligentes pueden ayudar incluso a las aplicaciones Django de estilo antiguo, para que realmente
les sirvan bien, necesitamos marcos que admitan la programación asíncrona en todo momento: desde el manejo de
solicitudes y respuestas HTTP hasta el acceso a la base de datos. Esto es especialmente cierto si desea implementar
servicios en tiempo real como juegos y transmisión de medios con WebSockets.10
La mejora de http_charfinder.py para permitir la descarga progresiva se deja como ejercicio para el lector. Puntos de
bonificación si implementa el "desplazamiento infinito", como lo hace Twitter. Con este reto doy por finalizada nuestra
cobertura de programación concurrente con asyncio.
9. Eso es lo que significa CJK: el conjunto en constante expansión de caracteres chinos, japoneses y coreanos. Futuro
Las versiones de Python pueden admitir más ideogramas CJK que Python 3.4.
10. Tengo más que decir sobre esta tendencia en "Soapbox" en la página 580.
Luego discutimos los detalles de asyncio.Future, enfocándonos en su soporte para yield from y su relación
con coroutines y asyncio.Task. A continuación, analizamos el script de descarga de banderas basado en
asyncio .
Luego reflexionamos sobre los números de Ryan Dahl para la latencia de E/S y el efecto del bloqueo de
llamadas. Para mantener vivo un programa a pesar de las inevitables funciones de bloqueo, hay dos
soluciones: usar subprocesos o llamadas asincrónicas; estas últimas se implementan como devoluciones
de llamada o corrutinas.
En la práctica, las bibliotecas asíncronas dependen de subprocesos de nivel inferior para funcionar, hasta
subprocesos de nivel de kernel, pero el usuario de la biblioteca no crea subprocesos y no necesita conocer
su uso en la infraestructura. En el nivel de la aplicación, solo nos aseguramos de que ninguno de nuestros
códigos esté bloqueando, y el ciclo de eventos se encarga de la concurrencia bajo el capó. Evitar la
sobrecarga de los subprocesos a nivel de usuario es la razón principal por la que los sistemas asincrónicos
pueden administrar más conexiones simultáneas que los sistemas multiproceso.
artículos
Reanudar los ejemplos de descarga de banderas, agregar una barra de progreso y el manejo adecuado
de errores requirió una refactorización significativa, particularmente con el cambio de async cio.wait a
asyncio.as_completed, lo que nos obligó a mover la mayor parte de la funcionalidad de download_many a
una nueva corrutina downloader_coro , por lo que podríamos usar yield from para obtener los resultados
de los futuros producidos por asyncio.as_completed, uno por uno.
Luego vimos cómo delegar tareas de bloqueo, como guardar un archivo, a un grupo de subprocesos
mediante el método loop.run_in_executor .
Esto fue seguido por una discusión sobre cómo las corrutinas resuelven los principales problemas de las
devoluciones de llamada: pérdida de contexto al realizar tareas asíncronas de varios pasos y falta de un
contexto adecuado para el manejo de errores.
El siguiente ejemplo, obtener los nombres de los países junto con las imágenes de las banderas, demostró
cómo la combinación de rutinas y rendimiento evita el llamado infierno de devolución de llamada. Un
procedimiento de varios pasos que realiza llamadas asincrónicas con rendimiento de parece un código
secuencial simple, si no presta atención al rendimiento de las palabras clave.
Los ejemplos finales del capítulo fueron los servidores asyncio TCP y HTTP que permiten buscar caracteres
Unicode por nombre. El análisis del servidor HTTP finalizó con una discusión sobre la importancia del
JavaScript del lado del cliente para admitir una mayor concurrencia en el lado del servidor, al permitir que
el cliente realice solicitudes más pequeñas a pedido, en lugar de descargar páginas HTML grandes.
Otras lecturas
Nick Coghlan, un desarrollador central de Python, hizo el siguiente comentario sobre el borrador de
PEP-3156 — Soporte de E/S asíncrono reiniciado: el módulo “asyncio” en enero de 2013:
En algún momento temprano en el PEP, es posible que deba haber una descripción concisa de las dos API
para esperar un futuro asíncrono:
1. f.add_done_callback(…)
Por el momento, estos están enterrados entre API mucho más grandes, pero son clave para comprender la
forma en que interactúa todo lo que está por encima de la capa de bucle de eventos central.11
Guido van Rossum, el autor de PEP-3156, no siguió el consejo de Coghlan. A partir de PEP-3156, la
documentación de asyncio es muy detallada pero no fácil de usar. Los nueve archivos .rst que
componen los documentos del paquete asyncio tienen un total de 128 KB, es decir, aproximadamente
71 páginas. En la biblioteca estándar, solo el capítulo "Tipos incorporados" es más grande y cubre la
API para los tipos numéricos, tipos de secuencia, generadores, asignaciones, conjuntos, bool,
administradores de contexto, etc.
La mayoría de las páginas del manual de asyncio se centran en los conceptos y la API. Hay diagramas
y ejemplos útiles dispersos por todas partes, pero una sección que es muy práctica es “18.5.11.
Desarrollar con asyncio”, que presenta patrones de uso esenciales. Los documentos de asyncio
necesitan más contenido que explique cómo se debe usar asyncio .
Debido a que es muy nuevo, asyncio carece de cobertura impresa. Programación paralela con Python
de Jan Palach (Packt, 2014) es el único libro que encontré que tiene un capítulo sobre asyncio, pero
es un capítulo corto.
Hay, sin embargo, excelentes presentaciones sobre asyncio. Lo mejor que encontré es “Fan-In and
Fan-Out: The Crucial Components of Concurrency” de Brett Slatkin, subtitulado “¿Por qué necesitamos
Tulipán? (alias, PEP 3156—asyncio)”, que presentó en PyCon 2014 en Montreal (video). En 30
minutos, Slatkin muestra un ejemplo simple de rastreador web, destacando cómo se pretende usar
asyncio . Guido van Rossum está entre la audiencia y menciona que también escribió un rastreador
web como ejemplo motivador para asyncio; El código de Guido no depende de aiohttp, solo usa la
biblioteca estándar. Slatkin también escribió la perspicaz publicación "Asyncio de Python es para la
composición, no para el rendimiento en bruto".
11. Comente sobre PEP-3156 en un mensaje del 20 de enero de 2013 a la lista de ideas de python.
Otras charlas de asyncio imperdibles son las del propio Guido van Rossum: el discurso de apertura de PyCon
US 2013 y las charlas que dio en LinkedIn y Twitter University. También se recomienda “A Deep Dive into
PEP-3156 and the New asyncio Module” de Saúl Ibarra Corretgé (diapositivas, video).
Dino Viehland mostró cómo se puede integrar asyncio con el bucle de eventos de Tkinter en su charla "Uso de
futuros para la programación de GUI asíncrona en Python 3.3" en PyCon US 2013.
Viehland muestra lo fácil que es implementar las partes esenciales de la interfaz asyncio.Abstrac tEventLoop
encima de otro bucle de eventos. Su código fue escrito con Tulip, antes de la adición de asyncio a la biblioteca
estándar; Lo adapté para que funcione con la versión Python 3.4 de asyncio. Mi refactorización actualizada
está en GitHub.
Victor Stinner, un colaborador principal de asyncio y autor del backport de Trollius , actualiza regularmente una
lista de enlaces relevantes: El nuevo módulo asyncio de Python, también conocido como "tulipán".
Otras colecciones de recursos asyncio son Asyncio.org y aio-libs en Github, donde encontrará controladores
asíncronos para PostgreSQL, MySQL y varias bases de datos NoSQL.
No he probado estos controladores, pero los proyectos parecen muy activos mientras escribo esto.
Los servicios web van a ser un caso de uso importante para asyncio. Es probable que su código dependa de
la biblioteca aiohttp dirigida por Andrew Svetlov. También querrá configurar un entorno para probar su código
de manejo de errores, y el "caos TCP proxy" de Vaurien diseñado por Alexis Métaireau y Tarek Ziadé es
invaluable para eso. Vaurien se creó para el proyecto Mozilla Services y le permite introducir demoras y errores
aleatorios en el tráfico TCP entre su programa y los servidores backend, como bases de datos y proveedores
de servicios web.
Plataforma improvisada
El bucle único
Durante mucho tiempo, la programación asíncrona ha sido el enfoque preferido por la mayoría de Pythonistas
para aplicaciones de red, pero siempre existía el dilema de elegir una de las bibliotecas incompatibles entre
sí. Ryan Dahl cita a Twisted como fuente de inspiración para Node.js, y Tornado defendió el uso de rutinas
para la programación orientada a eventos en Python.
En el mundo de JavaScript, existe cierto debate entre los defensores de las devoluciones de llamada simples
y los defensores de varias abstracciones de alto nivel que compiten entre sí. Las primeras versiones de la
API de Node.js usaban Promises, similar a nuestro Futures, pero Ryan Dahl decidió estandarizar solo las
devoluciones de llamadas. James Coglan argumenta que esta fue la mayor oportunidad perdida de Node.
En Python, el debate ha terminado: la adición de asyncio a la biblioteca estándar establece rutinas y futuros
como la forma Pythonic de escribir código asíncrono. Además, el paquete asyncio define interfaces estándar
para futuros asíncronos y el bucle de eventos, proporcionando implementaciones de referencia para ellos.
Aunque esa manera puede no ser obvia al principio a menos que seas holandés.
Tal vez se necesita un pasaporte holandés para encontrar el rendimiento de lo obvio. Al principio no era obvio
para este brasileño, pero después de un tiempo le cogí el truco.
Más importante aún, asyncio fue diseñado para que su ciclo de eventos pueda ser reemplazado por un
paquete externo. Por eso existen las funciones asyncio.get_event_loop y set_event_loop ; forman parte de
una API de política de bucle de eventos abstracta.
Tornado ya tiene una clase AsyncIOMainLoop que implementa la interfaz asyncio.Ab stractEventLoop , por lo
que puede ejecutar código asíncrono usando ambas bibliotecas en el mismo bucle de eventos. También está
el intrigante proyecto Quamash que integra async cio al bucle de eventos Qt para desarrollar aplicaciones GUI
con PyQt o PySide. Estos son solo dos de un número creciente de paquetes interoperables orientados a
eventos que asyncio hace posibles.
Los clientes HTTP más inteligentes, como las aplicaciones web de una sola página (como Gmail) o las
aplicaciones para teléfonos inteligentes, exigen respuestas rápidas y livianas y actualizaciones automáticas.
Estas necesidades se satisfacen mejor con marcos asincrónicos en lugar de marcos web tradicionales como
Django, que están diseñados para servir páginas HTML completamente renderizadas y carecen de soporte
para el acceso asincrónico a la base de datos.
El protocolo WebSockets fue diseñado para habilitar actualizaciones en tiempo real para clientes que siempre
están conectados, desde juegos hasta aplicaciones de transmisión. Esto requiere servidores asíncronos
altamente concurrentes capaces de mantener interacciones continuas con cientos o miles de clientes.
WebSockets está muy bien soportado por la arquitectura asyncio y al menos dos bibliotecas ya lo implementan
sobre asyncio: Autobahn|Python y Webÿ Sockets.
Esta tendencia general, denominada "la Web en tiempo real", es un factor clave en la demanda de Node.js y
la razón por la cual reunir a asyncio es tan importante para el ecosistema de Python. Todavía hay mucho
trabajo por hacer. Para empezar, necesitamos un servidor HTTP asíncrono y una API de cliente en la biblioteca
estándar, un DBAPI 3.0 asíncrono y nuevos controladores de base de datos basados en asyncio.
La mayor ventaja que tiene Python 3.4 con asyncio sobre Node.js es Python en sí mismo: un lenguaje mejor
diseñado, con rutinas y rendimiento para hacer que el código asíncrono sea más fácil de mantener que las
devoluciones de llamada primitivas de JavaScript. Nuestra mayor desventaja son las bibliotecas: Python viene
con "baterías incluidas", pero nuestras baterías no están diseñadas para la programación asíncrona. El rico
ecosistema de bibliotecas para Node.js se basa completamente en llamadas asíncronas. Pero tanto Python
como Node.js tienen un problema que Go y Erlang han resuelto desde el principio: no tenemos una forma
transparente de escribir código que aproveche todos los núcleos de CPU disponibles.
Estandarizar la interfaz de bucle de eventos y una biblioteca asíncrona fue un gran logro, y
solo nuestro BDFL podría haberlo logrado, dado que había alternativas bien arraigadas y de
alta calidad disponibles. Lo hizo en consulta con los autores de los principales marcos
asincrónicos de Python. La influencia de Glyph Lefkowitz, el líder de Twisted, es más
evidente. La publicación de Guido "Deconstrucción de diferidos" para el grupo Python-tulip
es una lectura obligada si desea comprender por qué asyncio.Future no es como la clase
Twisted Deferred . Dejando en claro su respeto por el marco asincrónico de Python más
antiguo y más grande, Guido también comenzó el meme WWTD: ¿Qué haría Twisted? —
cuando se discutieron las opciones de diseño en el grupo de python-twisted.12
Afortunadamente, Guido van Rossum lideró la carga para que Python esté mejor posicionado
para enfrentar los desafíos de concurrencia del presente. Dominar asyncio requiere esfuerzo.
Pero si planea escribir aplicaciones de red simultáneas en Python, busque One Loop:
12. Véase el mensaje de Guido del 29 de enero de 2015, seguido inmediatamente por una respuesta de Glyph.
PARTE VI
Metaprogramación
Machine Translated by Google
Machine Translated by Google
CAPÍTULO 19
La importancia crucial de las propiedades es que su existencia hace que sea perfectamente seguro y,
de hecho, aconsejable que usted exponga atributos de datos públicos como parte de la interfaz pública
de su clase.1
— Alex Martelli
Colaborador de Python y autor del libro
Los atributos de datos y los métodos se conocen colectivamente como atributos en Python: un método
es solo un atributo al que se puede llamar. Además de los métodos y atributos de datos, también
podemos crear propiedades, que se pueden usar para reemplazar un atributo de datos públicos con
métodos de acceso (es decir, getter/setter), sin cambiar la interfaz de clase. Esto está de acuerdo con
el principio de acceso uniforme:
Todos los servicios ofrecidos por un módulo deben estar disponibles a través de una notación uniforme,
2
que no traicione si se implementan mediante almacenamiento o mediante computación.
Además de las propiedades, Python proporciona una rica API para controlar el acceso a los atributos e
implementar atributos dinámicos. El intérprete llama a métodos especiales como __get attr__ y
__setattr__ para evaluar el acceso a los atributos mediante la notación de puntos (p. ej., obj.attr). Una
clase definida por el usuario que implementa __getattr__ puede implementar "atributos virtuales"
calculando valores sobre la marcha siempre que alguien intente leer un atributo inexistente como
obj.no_such_attribute.
La codificación de atributos dinámicos es el tipo de metaprogramación que hacen los autores de marcos.
Sin embargo, en Python, las técnicas básicas son tan sencillas que cualquiera puede ponerlas a trabajar,
incluso para las tareas cotidianas de disputa de datos. Así es como comenzaremos este capítulo.
585
Machine Translated by Google
{ "Programa":
{ "conferencias": [{"serial": 115 }],
"eventos": [ { "serial": 34505, "name":
"Por qué las escuelas no usan código
abierto para enseñar programación", "event_type": "Sesión de conferencia de
40 minutos", "time_start": "2014-07-23 11:30:00", "time_stop": "2014-07-23
12:10:00", "venue_serial": 1462, "descripción": "Aparte del hecho de que la
programación de la escuela secundaria...", "website_url": "http://oscon.com/
oscon2014/public/schedule/detail/34505", "speakers": [157509 ], "categorías":
["Educación"] }
],
"altavoces":
[ { "serie": 157509,
"nombre": "Robert Lefkowitz",
"foto": nulo, "url": "http://
sharewave.com/", "posición": "CTO
", "afiliación": "Sharewave", "twitter":
"sharewaveteam", "bio": "Robert ´r0ml
´ Lefkowitz es el CTO de Sharewave,
una startup..." }
],
"lugares":
[ { "serie": 1462,
"nombre": "F151",
"categoría": "Sedes de conferencias" }
]
}
}
El ejemplo 19-1 muestra 4 de los 895 registros en el feed JSON. Como puede ver, todo el conjunto de
datos es un solo objeto JSON con la clave "Horario", y su valor es otro mapeo con cuatro claves:
"conferencias", "eventos", "conferencistas" y "lugares". Cada una de esas cuatro claves está
emparejada con una lista de registros. En el Ejemplo 19-1, cada lista tiene un registro, pero en el
conjunto de datos completo, esas listas tienen docenas o cientos de registros, con la excepción
3. Puede leer sobre este feed y las reglas para usarlo en "DIY: horario OSCON". El archivo JSON original de 744 KB todavía está
en línea mientras escribo esto. Se puede encontrar una copia llamada osconfeed.json en el directorio oscon-schedule/ data/
en el repositorio de código de Fluent Python .
de "conferencias", que contiene solo el único registro que se muestra. Cada elemento de esas cuatro listas
tiene un campo "serie" , que es un identificador único dentro de la lista.
El primer script que escribí para tratar con el feed OSCON simplemente descarga el feed, evitando el
tráfico innecesario al verificar si hay una copia local. Esto tiene sentido porque OSCON 2014 ya es historia,
por lo que ese feed no se actualizará.
No hay metaprogramación en el ejemplo 19-2; prácticamente todo se reduce a esta expresión: json.load(fp),
pero eso es suficiente para permitirnos explorar el conjunto de datos. La función osconfeed.load se utilizará
en los siguientes ejemplos.
URL = 'http://www.oreilly.com/pub/sc/osconfeed'
JSON = 'datos/osconfeed.json'
def load(): si
no os.path.exists(JSON): msg =
'downloading {} to {}'.format(URL, JSON)
warnings.warn(msg) with urlopen(URL) as remote,
open(JSON , 'wb') como local:
local.escribir(remoto.leer())
con el uso de dos administradores de contexto (permitidos desde Python 2.7 y 3.1) para leer el
archivo remoto y guardarlo.
Con el código del ejemplo 19-2, podemos inspeccionar cualquier campo de los datos. Vea el Ejemplo 19-3.
de "conferencias", que contiene solo el único registro que se muestra. Cada elemento de esas cuatro listas
tiene un campo "serie" , que es un identificador único dentro de la lista.
El primer script que escribí para tratar con el feed OSCON simplemente descarga el feed, evitando el
tráfico innecesario al verificar si hay una copia local. Esto tiene sentido porque OSCON 2014 ya es historia,
por lo que ese feed no se actualizará.
No hay metaprogramación en el ejemplo 19-2; prácticamente todo se reduce a esta expresión: json.load(fp),
pero eso es suficiente para permitirnos explorar el conjunto de datos. La función osconfeed.load se utilizará
en los siguientes ejemplos.
URL = 'http://www.oreilly.com/pub/sc/osconfeed'
JSON = 'datos/osconfeed.json'
def load(): si
no os.path.exists(JSON): msg =
'downloading {} to {}'.format(URL, JSON)
warnings.warn(msg) with urlopen(URL) as remote,
open(JSON , 'wb') como local:
local.escribir(remoto.leer())
con el uso de dos administradores de contexto (permitidos desde Python 2.7 y 3.1) para leer el
archivo remoto y guardarlo.
Con el código del ejemplo 19-2, podemos inspeccionar cualquier campo de los datos. Vea el Ejemplo 19-3.
1 conferencias
484 eventos
357 oradores
53 lugares
>>> feed['Horario']['conferencistas'][-1]['nombre']
'Carina C. Zona'
>>> feed['Horario']['conferencistas'][-1]['serial'] 141590
>>> feed['Horario']['eventos'][40]['nombre']
'Habrá *Habrá* Errores'
>>> feed['Schedule']['events'][40]['speakers'] [3471, 5199]
feed es un dictado que contiene dictados y listas anidados, con cadenas y valores enteros.
Enumere las cuatro colecciones de registros dentro de "Horario".
Navegue a través de los dictados anidados y las listas para obtener el nombre del último orador.
Cada evento tiene una lista de 'oradores' con 0 o más números de serie de oradores.
El ejemplo 19-4 es una demostración de FrozenJSON y el código fuente se encuentra en el ejemplo 19-5.
Ejemplo 19-4. FrozenJSON del Ejemplo 19-5 permite leer atributos como el nombre y llamar a métodos
como .keys() y .items()
4. Uno mencionado a menudo es AttrDict; otro, que permite la creación rápida de mapeos anidados, es adictivo.
...
1 conferencias
484 eventos
357 oradores
53 lugares
>>> feed.Schedule.speakers[-1].name
'Carina C. Zona' >>> talk =
feed.Schedule.events[40] >>> type(talk)
<class 'explore0.FrozenJSON'> >> >
talk.name 'There *Will* Be Bugs' >>>
talk.speakers [3471, 5199] >>> talk.flavor
Traceback (última llamada más reciente):
...
KeyError: 'sabor'
Cree una instancia de FrozenJSON a partir de raw_feed hecho de listas y dictados anidados.
FrozenJSON permite atravesar dictados anidados mediante el uso de notación de atributos; aquí mostramos la
longitud de la lista de oradores.
También se puede acceder a los métodos de los dictados subyacentes, como .keys(), para recuperar los nombres
de la colección de registros.
Usando items(), podemos recuperar los nombres de las colecciones de registros y sus contenidos, para mostrar
Una lista, como feed.Schedule.speakers, sigue siendo una lista, pero los elementos que contiene se convierten a
El elemento 40 en la lista de eventos era un objeto JSON; ahora es una instancia de FrozenJSON .
Los registros de eventos tienen una lista de oradores con números de serie de los oradores.
Intentar leer un atributo que falta genera KeyError, en lugar del Attrib uteError habitual.
La clave de la clase FrozenJSON es el método __getattr__ , que ya usamos en el ejemplo de Vector en “Vector Take #3:
Dynamic Attribute Access” en la página 284, para recuperar los componentes de Vector por letra: vx, vy, vz, etc. Es esencial
recordar que el método especial __getattr__ solo es invocado por el intérprete cuando el proceso habitual falla en recuperar
un atributo (es decir, cuando el atributo nombrado no se puede encontrar en la instancia, ni en la clase o en sus superclases).
La última línea del ejemplo 19-4 expone un problema menor con la implementación: idealmente, tratar de leer un atributo
faltante debería generar AttributeError. De hecho, implementé el manejo de errores, pero duplicó el tamaño del método
__getattr__ y me distrajo de la lógica más importante que quería mostrar, así que lo dejé por razones didácticas.
Como se muestra en el ejemplo 19-5, la clase FrozenJSON solo tiene dos métodos (__init__,
__getattr__) y un atributo de instancia de __data , por lo que intenta recuperar un atributo
cualquier otro nombre activará __getattr__. Este método primero buscará si el self.__da
ta dict tiene un atributo (¡no una clave!) con ese nombre; esto permite que las instancias de FrozenJSON
manejar cualquier método dict como elementos, delegando a self.__data.items(). Si
self.___data no tiene un atributo con el nombre dado, __getattr__ usa el nombre como
una clave para recuperar un elemento de self.__dict, y pasa ese elemento a FrozenJ
HIJO.construir. Esto permite navegar a través de estructuras anidadas en los datos JSON, ya que cada
La asignación anidada se convierte en otra instancia de FrozenJSON mediante el método de clase de compilación .
Ejemplo 19-5. explore0.py: convierta un conjunto de datos JSON en un contenedor FrozenJSON anidado
Objetos, listas y tipos simples de FrozenJSON
clase FrozenJSON:
"""Una fachada de solo lectura para navegar por un objeto similar a JSON
usando notación de atributos
"""
@métodoclase
def build(cls, obj): if
isinstance(obj, abc.Mapping): return cls(obj)
Cree un dict a partir del argumento de mapeo . Esto sirve para dos propósitos: asegura que
tiene un dict (o algo que se puede convertir en uno) y hace una copia para
la seguridad.
__getattr__ solo se llama cuando no hay ningún atributo con ese nombre.
Si el nombre coincide con un atributo de la instancia __data, devuélvalo. Así llaman
se manejan métodos como claves .
De lo contrario, obtenga el elemento con el nombre clave de self.__data y devuelva el resultado de llamar
a FrozenJSON.build() en eso.5 Este es un constructor alternativo, un uso común para el decorador
@classmethod .
Si es una MutableSequence, debe ser una lista,6 por lo que construimos una lista pasando cada elemento
en obj recursivamente a .build().
Tenga en cuenta que no se realiza el almacenamiento en caché ni la transformación del feed original. A medida
que se recorre el feed, las estructuras de datos anidadas se convierten una y otra vez en FrozenJSON.
Pero eso está bien para un conjunto de datos de este tamaño y para un script que solo se usará para explorar o
convertir los datos.
Cualquier script que genere o emule nombres de atributos dinámicos de fuentes arbitrarias debe lidiar con un
problema: las claves en los datos originales pueden no ser nombres de atributos adecuados. La siguiente sección
aborda esto.
La clase FrozenJSON tiene una limitación: no existe un manejo especial para los nombres de atributos que son
palabras clave de Python. Por ejemplo, si construyes un objeto como este:
>>> grad.class
Archivo "<stdin>", línea 1
grad.class
^
Pero la idea de FrozenJSON es brindar un acceso conveniente a los datos, por lo que una mejor solución es
verificar si una clave en el mapeo dado a FrozenJSON.__init__ es una palabra clave y, de ser así, agregar una
_ a él, por lo que el atributo se puede leer así:
5. Esta línea es donde puede ocurrir una excepción KeyError, en la expresión self.__data[name]. Debería
manejarse y generarse un AttributeError en su lugar, porque eso es lo que se espera de __getattr__. Se
invita al lector diligente a codificar el manejo de errores como ejercicio.
6. La fuente de los datos es JSON, y los únicos tipos de colección en los datos JSON son dict y list.
Esto se puede lograr reemplazando el __init__ de una sola línea del ejemplo 19-5 con la versión del ejemplo
19-6.
Ejemplo 19-6. explore1.py: agregue un _ para atribuir nombres que son palabras clave de Python
Puede surgir un problema similar si una clave en el JSON no es un identificador de Python válido:
Tales claves problemáticas son fáciles de detectar en Python 3 porque la clase str proporciona el método
s.isidentifier() , que te dice si s es un identificador de Python válido según la gramática del lenguaje. Pero
convertir una clave que no es un identificador válido en un nombre de atributo válido no es trivial. Dos
soluciones simples serían generar una excepción o reemplazar las claves no válidas con nombres genéricos
como attr_0, attr_1, etc. En aras de la simplicidad, no me preocuparé por este tema.
Después de pensar un poco en los nombres de atributos dinámicos, pasemos a otra característica esencial
de FrozenJSON: la lógica del método de clase de compilación , que __get attr__ usa para devolver un tipo
diferente de objeto según el valor del atributo al que se accede, para que las estructuras anidadas se
conviertan en instancias de FrozenJSON o listas de instancias de FrozenJSON .
En lugar de un método de clase, se podría implementar la misma lógica que el método especial __nuevo__ ,
como veremos a continuación.
referimos a __init__ como el método constructor, pero eso se debe a que adoptamos la jerga de otros
lenguajes. El método especial que realmente construye una instancia es __new__: es un método de clase
(pero recibe un tratamiento especial, por lo que el decorador @classmethod
no se utiliza), y debe devolver una instancia. Esa instancia, a su vez, se pasará como el primer
argumento self de __init__. Debido a que __init__ obtiene una instancia cuando se le llama, y en
realidad tiene prohibido devolver algo, __init__ es realmente un "inicializador". El constructor real es
__nuevo__, que rara vez necesitamos codificar porque la implementación heredada del objeto es
suficiente.
La ruta que acabamos de describir, de __new__ a __init__, es la más común, pero no la única. El
método __new__ también puede devolver una instancia de una clase diferente y, cuando eso sucede,
el intérprete no llama a __init__.
En otras palabras, el proceso de construcción de un objeto en Python se puede resumir con este
pseudocódigo:
El ejemplo 19-7 muestra una variación de FrozenJSON donde la lógica del método de clase de
compilación anterior se movió a __nuevo__.
Ejemplo 19-7. explore2.py: usar new en lugar de build para construir nuevos objetos que pueden o no
ser instancias de FrozenJSON
de colecciones importar abc
clase FrozenJSON:
"""Una fachada de solo lectura para navegar por un objeto similar a JSON
utilizando la notación de atributos
"""
self.__data[clave] = valor
Como método de clase, el primer argumento que obtiene __new__ es la clase misma, y los argumentos
restantes son los mismos que obtiene __init__ , excepto self.
Las líneas restantes de __new__ son exactamente como en el método de compilación anterior.
Aquí fue donde se llamó FrozenJSON.build antes; ahora solo llamamos al constructor Fro zenJSON .
El método __nuevo__ obtiene la clase como primer argumento porque, por lo general, el objeto creado será una
instancia de esa clase. Entonces, en FrozenJSON.__new__, cuando la expresión super().__new__(cls)
efectivamente llama a object.__new__(FrozenJSON), la instancia creada por la clase de objeto es en realidad
una instancia de FrozenJSON, es decir, el atributo __class__ de la nueva instancia contendrá una referencia a
FrozenJSON, aunque la construcción real la realiza object.__new__, implementado en C, en las entrañas del
intérprete.
Hay una deficiencia obvia en la forma en que está estructurado el feed OSCON JSON: el evento en el índice 40,
titulado 'There *Will* Be Bugs' tiene dos altavoces, 3471 y 5199, pero encontrarlos no es fácil, porque esos son
números de serie. , y la lista de oradores de Schedule.speakers no está indexada por ellos. El campo de lugar ,
presente en cada registro de evento , también contiene un número de serie, pero encontrar el registro de lugar
correspondiente requiere un escaneo lineal de la lista Schedule.venues . Nuestra siguiente tarea es reestructurar
los datos y luego automatizar la recuperación de registros vinculados.
del módulo shelve estándar tiene sentido cuando se da cuenta de que pickle es el nombre del formato de
serialización de objetos de Python y del módulo que convierte objetos a/desde ese formato. Debido a que los
frascos de encurtidos se guardan en estantes, tiene sentido que los estantes proporcionen almacenamiento de
encurtidos .
La función de alto nivel shelve.open devuelve una instancia shelve.Shelf , una base de datos de objetos de valor
clave simple respaldada por el módulo dbm , con estas características:
• Las claves y los valores se guardan cada vez que se asigna un nuevo valor a una clave.
• Los valores deben ser objetos que el módulo pickle pueda manejar.
Consulte la documentación de los módulos shelve, dbm y pickle para conocer los detalles y
advertencias Lo que nos importa ahora es que shelve proporcione una manera simple y eficiente de
organizar los datos de programación de OSCON: leeremos todos los registros del archivo JSON y guardaremos
a un estante.Estante. Cada clave se realizará a partir del tipo de registro y del serial.
número (por ejemplo, 'event.33950' o 'speaker.3471') y el valor será una instancia de
una nueva clase Record que estamos a punto de presentar.
El ejemplo 19-8 muestra las pruebas de documentos para el script schedule1.py usando estantería. para probarlo
de forma interactiva, ejecute el script como python -i schedule1.py para obtener un indicador de la consola con
el módulo cargado. La función load_db hace el trabajo pesado: llama a oscon
feed.load (del Ejemplo 19-2) para leer los datos JSON y guardar cada registro como un Registro
instancia en el objeto Shelf pasado como db. Después de eso, recuperar el registro de un orador es tan
fácil como altavoz = db['speaker.3471'].
Una forma rápida de determinar si la base de datos está llena es buscar una clave conocida,
en este caso , conferencia.115: la clave para el registro único de la conferencia.7
7. También podría hacer len(db), pero eso sería costoso en una gran base de datos dbm.
Cada instancia de registro implementa un conjunto personalizado de atributos que reflejan los campos
del registro JSON subyacente.
Recuerda siempre cerrar una estantería. Estantería. Si es posible, use un bloque con para hacer
asegúrese de que el estante esté cerrado.8
Ejemplo 19-9. schedule1.py: exploración de los datos de programación de OSCON guardados en una estantería. Estante
advertencias de importación
importar osconfeed
DB_NAME = 'datos/planificación1_db'
CONFERENCIA = 'conferencia.115'
registro de clase :
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
def cargar_db(db):
raw_data = osconfeed.load()
advertencias.warn('cargando ' + DB_NOMBRE)
para la colección, rec_list en raw_data['Schedule'].items():
record_type = colección[:-1] para registro
en rec_list:
clave = '{}.{}'.format(record_type, record['serial']) record['serial'] = key db[key]
= Record(**record)
Este es un atajo común para construir una instancia con atributos creados a partir de
argumentos de palabras clave (explicación detallada a continuación).
Esto puede obtener la fuente JSON de la Web, si no hay una copia local.
record_type se establece en el nombre de la colección sin la 's' final (es decir, 'eventos'
se convierte en 'evento').
8. Una debilidad fundamental de doctest es la falta de una configuración de recursos adecuada y un desmantelamiento garantizado. escribí
la mayoría de las pruebas para schedule1.py usando py.test, y puede verlas en el Ejemplo A-12.
El método Record.__init__ ilustra un truco popular de Python. Recuerde que el __dict__ de un objeto es
donde se guardan sus atributos, a menos que se declare __slots__ en la clase, como vimos en “Ahorro de
espacio con el atributo de clase __slots__” en la página 264.
Por lo tanto, actualizar una instancia __dict__ con una asignación es una forma rápida de crear una serie
de atributos en esa instancia.9
La definición de Record en el Ejemplo 19-9 es tan simple que quizás se pregunte por qué no la usamos
antes, en lugar del más complicado FrozenJSON. Hay un par de razones. En primer lugar, FrozenJSON
funciona convirtiendo recursivamente las asignaciones y listas anidadas; Record no lo necesita porque
nuestro conjunto de datos convertido no tiene asignaciones anidadas en asignaciones o listas. Los registros
contienen solo cadenas, enteros, listas de cadenas y listas de enteros. Una segunda razón es que
FrozenJSON brinda acceso a los atributos de dictado de __data incrustados , que usamos para invocar
métodos como claves, y ahora tampoco necesitamos esa funcionalidad.
Después de reorganizar el conjunto de datos del programa como acabamos de hacer, ahora podemos
extender la clase Record para brindar un servicio útil: recuperar automáticamente los registros del lugar y
del orador a los que se hace referencia en un registro de evento . Esto es similar a lo que hace Django
ORM cuando accede a un campo models.ForeignKey : en lugar de la clave, obtiene el objeto del modelo vinculado.
Usaremos propiedades para hacerlo en el siguiente ejemplo.
9. Por cierto, Bunch es el nombre de la clase utilizada por Alex Martelli para compartir este consejo en una receta de 2001 titulada
"El simple pero útil coleccionista de un montón de cosas con nombre ".
El objetivo de esta próxima versión es: dado un registro de evento recuperado del estante, la lectura de los
atributos del lugar o de los oradores no devolverá números de serie sino objetos de registro completos. Vea
la interacción parcial en el ejemplo 19-10 como ejemplo.
>>> DbRecord.set_db(db)
>>> evento = DbRecord.fetch('event.33950') >>>
evento <Evento 'Habrá *Habrá* Errores'> >>>
event.venue <DbRecord serial=' lugar.1449'>
>>> evento.lugar.nombre
'Portland 251'
>>> para altavoces en
... evento.altavoces : print('{0.serial}: {0.name}'.format(spkr))
...
locutor.3471: Anna Martelli Ravenscroft
locutor.5199: Alex Martelli
DbRecord amplía Record, agregando soporte de base de datos: para operar, DbRecord debe recibir
una referencia a una base de datos.
Tenga en cuenta que event es una instancia de la clase Event , que amplía DbRecord.
También podemos iterar sobre la lista event.speakers , recuperando DbRecords que representan a
cada orador.
La figura 19-1 proporciona una descripción general de las clases que estudiaremos en esta sección:
Registro
El método __init__ es el mismo que en schedule1.py (Ejemplo 19-9); se agregó el método __eq__
para facilitar las pruebas.
DbRecord
Subclase de registro que agrega un atributo de clase __db, métodos estáticos set_db y get_db para
establecer/obtener ese atributo, un método de clase de obtención para recuperar registros de la base
de datos y un método de instancia __repr__ para admitir la depuración y las pruebas.
Evento
Subclase de DbRecord que agrega propiedades de lugar y oradores para recuperar registros
vinculados y un método __repr__ especializado.
Figura 19-1. Diagrama de clases UML para una clase Record mejorada y dos subclases: DbRecord y
Event.
El atributo de clase DbRecord.__db existe para contener una referencia a la base de datos shelve.Shelf
abierta , por lo que puede ser utilizado por el método DbRecord.fetch y las propiedades Event.venue y
Event.speakers que dependen de él. Codifiqué __db como un atributo de clase privada con métodos
de obtención y configuración convencionales porque quería protegerlo de sobrescrituras accidentales.
No utilicé una propiedad para administrar __db debido a un hecho crucial: las propiedades son atributos
de clase diseñados para administrar atributos de instancia.10
El código para esta sección está en el módulo schedule2.py en el repositorio de código de Fluent
Python . Debido a que el módulo supera las 100 líneas, lo presentaré en partes.11 Las primeras
importar advertencias
importar inspeccionar
importar osconfeed
DB_NAME = 'datos/planificación2_db'
CONFERENCIA = 'conferencia.115'
registro de clase :
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
10. El tema de StackOverflow "Propiedades de solo lectura a nivel de clase en Python" tiene soluciones para los atributos de solo
lectura en las clases, incluida una de Alex Martelli. Las soluciones requieren metaclases, por lo que quizás desee leer el Capítulo
21 antes de estudiarlas.
11. La lista completa de schedule2.py se encuentra en el Ejemplo A-13, junto con los scripts de py.test en el “Capítulo 19: OSCON
Programar guiones y pruebas” en la página 708.
Debido a que estamos almacenando instancias de diferentes clases, creamos y usamos una diferente
archivo de base de datos, 'schedule2_db', en lugar del 'schedule_db' del ejemplo 19-9.
Las siguientes clases definidas en schedule2.py son un tipo de excepción personalizado y DbRecord. Ver
Ejemplo 19-12.
clase MissingDatabaseError(RuntimeError):
"""Generado cuando se requiere una base de datos pero no se configuró."""
clase DbRecord(Registro):
__db = Ninguno
@staticmethod
def set_db(db):
RegistroBD.__db = db
@staticmethod
def get_db():
devolver DbRecord.__db
@classmethod
def fetch(cls, ident):
db = cls.get_db()
probar:
12. La subclasificación explícita del objeto en Python 3 no está mal, solo es redundante porque todas las clases tienen un
nuevo estilo ahora. Este es un ejemplo en el que romper con el pasado hizo que el lenguaje fuera más limpio. Si el mismo código
debe ejecutarse en Python 2 y Python 3, la herencia del objeto debe ser explícita.
devuelve db[ident]
excepto TypeError: si
db es Ninguno:
msg = "base de datos no configurada; llame a '{}.set_db(my_db)'"
raise MissingDatabaseError(msg.format(cls.__name__)) else: raise
def __repr__(self): if
hasattr(self, 'serial'): cls_name =
self.__class__.__name__ return '<{} serial={!
r}>'.format(cls_name, self.serial) else:
devolver super().__repr__()
Las excepciones personalizadas suelen ser clases de marcador, sin cuerpo. Una cadena de documentación
que explique el uso de la excepción es mejor que una mera declaración de aprobación .
set_db es un método estático para hacer explícito que su efecto es siempre exactamente el mismo, sin
importar cómo se llame.
Incluso si este método se invoca como Event.set_db(my_db), el atributo __db se establecerá en la clase
DbRecord .
get_db también es un método estático porque siempre devolverá el objeto al que hace referencia
DbRecord.__db, sin importar cómo se invoque.
fetch es un método de clase, por lo que su comportamiento es más fácil de personalizar en las subclases.
Si obtenemos un TypeError y db es None, genere una excepción personalizada que explique que la base
de datos debe estar configurada.
Ahora llegamos al meollo del ejemplo: la clase Event , listada en el Ejemplo 19-13.
@property
def sede(self): clave
= 'lugar.{}'.format(self.lugar_serial)
volver self.__class__.fetch(clave)
@propiedad
hablantes def (uno mismo):
si no hasattr(self, '_speaker_objs'):
spkr_serials = self.__dict__['speakers'] fetch =
self.__class__.fetch self._speaker_objs = [fetch('speaker.
{}'.format(key))
para clave en spkr_serials]
devolver self._speaker_objs
La propiedad del lugar crea una clave a partir del atributo place_serial y la pasa
al método de clase fetch , heredado de DbRecord (vea la explicación después de esto
ejemplo).
Obtenga una referencia al método de clase de búsqueda (la razón de esto se explicará
dentro de poco).
def cargar_db(db):
raw_data = osconfeed.load()
advertencias.warn('cargando ' + DB_NOMBRE)
para la colección, rec_list en raw_data['Schedule'].items():
record_type = colección[:-1]
cls_name = record_type.capitalize() cls =
globals().get(cls_name, DbRecord) if
inspect.isclass(cls) and issubclass(cls, DbRecord): factory = cls
else:
factory = DbRecord
para registro en rec_list:
clave = '{}.{}'.format(record_type, record['serial'])
registro['serial'] = clave
db[clave] = fábrica(**registro)
Hasta ahora, no hay cambios con respecto a load_db en schedule1.py (Ejemplo 19-9).
Ponga en mayúsculas record_type para obtener un nombre de clase potencial (p. ej., 'evento' se convierte en
'Evento').
Obtenga un objeto con ese nombre del alcance global del módulo; obtener DbRecord si hay
no hay tal objeto.
…vincularle el nombre de la fábrica . Esto significa que la fábrica puede ser cualquier subclase de
El bucle for que crea la clave y guarda los registros es el mismo que antes,
excepto eso…
…el objeto almacenado en la base de datos se construye de fábrica, el cual puede ser DbRecord o
una subclase seleccionada de acuerdo al record_type.
Tenga en cuenta que el único record_type que tiene una clase personalizada es Event, pero si las clases
denominadas Speaker o Venue están codificadas, load_db usará automáticamente esas clases al crear y
guardar registros, en lugar de la clase DbRecord predeterminada .
Hasta ahora, los ejemplos de este capítulo se diseñaron para mostrar una variedad de técnicas
para implementar atributos dinámicos utilizando herramientas básicas como __getattr__,
hasattr, get attr, @property y __dict__.
Las propiedades se utilizan con frecuencia para hacer cumplir las reglas comerciales al cambiar un atributo
público en un atributo administrado por un getter y setter sin afectar el código del cliente, como se muestra en
la siguiente sección.
Hasta ahora, solo hemos visto el decorador @property utilizado para implementar propiedades de solo lectura.
En esta sección, crearemos una propiedad de lectura/escritura.
Imagine una aplicación para una tienda que vende alimentos orgánicos a granel, donde los clientes pueden
pedir nueces, frutas secas o cereales por peso. En ese sistema, cada pedido contendría una secuencia de
elementos de línea, y cada elemento de línea podría estar representado por una clase como en el ejemplo 19-15.
def subtotal(self):
return self.weight * self.price
Eso es agradable y simple. Quizás demasiado simple. El ejemplo 19-16 muestra un problema.
Este es un ejemplo de juguete, pero no tan fantasioso como podrías pensar. Aquí hay una historia real de
los primeros días de Amazon.com:
¡Descubrimos que los clientes podían pedir una cantidad negativa de libros! Y
acreditaríamos el precio en su tarjeta de crédito y, supongo, esperaríamos a que enviaran los libros.13
— Jeff Bezos
Fundador y CEO de Amazon.com
¿Cómo arreglamos esto? Podríamos cambiar la interfaz de LineItem para usar un getter y un setter para el
atributo de peso . Esa sería la forma de Java, y no está mal.
Por otro lado, es natural poder establecer el peso de un artículo simplemente asignándolo; y quizás el
sistema está en producción con otras partes que ya acceden directamente a item.weight . En este caso, la
forma de Python sería reemplazar el atributo de datos con una propiedad.
Implementar una propiedad nos permitirá usar un getter y un setter, pero la interfaz de LineItem no cambiará
(es decir, establecer el peso de un LineItem todavía se escribirá como pasas.peso = 12) .
def subtotal(self):
return self.weight * self.price
@property
def peso(self): return
self.__weight
@peso.setter def
peso(yo, valor):
if value > 0:
self.__weight = value
else: raise ValueError('el
valor debe ser > 0')
13. Cita directa de Jeff Bezos en el artículo del Wall Street Journal “Birth of a Salesman” (15 de octubre de 2011).
Aquí, el definidor de propiedades ya está en uso, asegurándose de que no se puedan crear instancias con
peso negativo. @property decora el método getter.
Todos los métodos que implementan una propiedad tienen el nombre del atributo público: peso.
El getter decorado tiene un atributo .setter , que también es un decorador; esto une al getter y al setter.
Tenga en cuenta que ahora no se puede crear un elemento de línea con un peso no válido:
Ahora hemos protegido el peso de los usuarios que proporcionan valores negativos. Aunque los compradores
normalmente no pueden establecer el precio de un artículo, un error administrativo o una falla pueden crear un artículo
de línea con un precio negativo . Para evitar eso, también podríamos convertir el precio en una propiedad, pero esto
implicaría cierta repetición en nuestro código.
Recuerde la cita de Paul Graham del Capítulo 14: “Cuando veo patrones en mis programas, lo considero una señal de
problemas”. La cura para la repetición es la abstracción. Hay dos formas de abstraer las definiciones de propiedades:
usando una fábrica de propiedades o una clase de descriptor. El enfoque de clase de descriptor es más flexible, y
dedicaremos el Capítulo 20 a una discusión completa al respecto. De hecho, las propiedades se implementan como
clases descriptoras.
Pero aquí continuaremos nuestra exploración de las propiedades implementando una factoría de propiedades como
función.
Pero antes de que podamos implementar una fábrica de propiedades, debemos tener una comprensión más profunda
de las propiedades.
Aunque a menudo se usa como decorador, la propiedad integrada es en realidad una clase. En Python, las funciones
y las clases a menudo son intercambiables, porque se puede llamar a ambas y no hay un nuevo operador para la
creación de instancias de objetos, por lo que invocar un constructor no es diferente a invocar una función de fábrica. Y
ambos pueden usarse como decoradores, siempre que devuelvan un nuevo invocable que sea un reemplazo adecuado
de la función decorada.
Todos los argumentos son opcionales, y si no se proporciona una función para uno de ellos, el
objeto de propiedad resultante no permite la operación correspondiente.
El tipo de propiedad se agregó en Python 2.2, pero la sintaxis @ decorator apareció solo en Python
2.4, por lo que durante algunos años, las propiedades se definieron pasando las funciones de
acceso como los dos primeros argumentos.
La sintaxis "clásica" para definir propiedades sin decoradores se ilustra en el ejemplo 19-18.
Ejemplo 19-18. bulkfood_v2b.py: igual que el Ejemplo 19-17 pero sin usar decoradores
elemento de línea de clase :
Un captador simple.
Un setter llano.
La forma clásica es mejor que la sintaxis del decorador en algunas situaciones; el código de la
fábrica de propiedades que discutiremos en breve es un ejemplo. Por otro lado, en un cuerpo de
clase con muchos métodos, los decoradores dejan explícito cuáles son los getters y setters, sin
depender de la convención de usar prefijos get y set en sus nombres.
La presencia de una propiedad en una clase afecta la forma en que se pueden encontrar los atributos en las instancias
de esa clase de una manera que puede resultar sorprendente al principio. La siguiente sección explica.
Las propiedades son siempre atributos de clase, pero en realidad gestionan el acceso a los atributos en el
instancias de la clase.
En “Sustitución de atributos de clase” en la página 267 vimos que cuando una instancia y su clase
ambos tienen un atributo de datos con el mismo nombre, el atributo de instancia anula o sombreado.
ows, el atributo de clase, al menos cuando se lee esa instancia. Ejemplo 19-19 ilusÿ
trata este punto.
Defina Clase con dos atributos de clase: el atributo de datos de datos y la prop
propiedad.
>>> Class.prop #
<objeto de propiedad en 0x1072b7408>
>>> obj.prop # 'el
valor de prop'
>>> obj.prop = 'foo' # Rastreo
(última llamada más reciente):
...
AttributeError: no se puede establecer el atributo
>>> obj.__dict__['prop'] = 'foo' # >>>
vars(obj) # {'prop': 'foo', 'attr': 'bar'}
Podemos ver que obj ahora tiene dos atributos de instancia: attr y prop.
Como demostración final, agregaremos una nueva propiedad a Class y veremos cómo anula un
atributo de instancia. El ejemplo 19-21 continúa donde lo dejó el ejemplo 19-20 .
Ejemplo 19-21. La nueva propiedad de clase sombrea el atributo de instancia existente (continuación)
del ejemplo 19-20)
>>> obj.datos #
'barra'
>>> Class.data # 'el
atributo de datos de clase'
>>> Class.data = property(lambda self: 'el valor de prop de "datos"') # >>> obj.data
# 'el valor de prop de "datos"'
>>> obj.datos #
'barra'
Eliminar la propiedad.
El punto principal de esta sección es que una expresión como obj.attr no busca attr que comience con obj. La
búsqueda en realidad comienza en obj.__class__, y solo si no hay una propiedad llamada attr en la clase, Python
busca en la instancia de obj . Esta regla se aplica no solo a las propiedades, sino a toda una categoría de
descriptores, los descriptores primordiales. El tratamiento adicional de los descriptores debe esperar al Capítulo
20, donde veremos que las propiedades son, de hecho, anulan los descriptores.
Ahora volvamos a las propiedades. Cada unidad de código de Python (módulos, funciones, clases, métodos)
puede tener una cadena de documentación. El siguiente tema es cómo adjuntar documentación a las propiedades.
Documentación de propiedades
Cuando herramientas como la función help() de la consola o los IDE necesitan mostrar la documentación de una
propiedad, extraen la información del atributo __doc__ de la propiedad.
Si se usa con la sintaxis de llamada clásica, la propiedad puede obtener la cadena de documentación como
argumento doc :
Cuando la propiedad se implementa como decorador, la cadena de documentación del método getter (la que
tiene el decorador @property en sí) se usa como documentación de la propiedad en su conjunto. La figura 19-2
muestra las pantallas de ayuda generadas a partir del código del ejemplo 19-22.
Figura 19-2. Capturas de pantalla de la consola de Python al emitir los comandos help(Foo.bar) y
help(Foo). Código fuente en el Ejemplo 19-22.
@property
def bar(self): '''El
atributo de la barra''' return
self.__dict__['bar']
@bar.setter
def bar(self, valor):
self.__dict__['bar'] = valor
Ahora que hemos cubierto estos aspectos esenciales de las propiedades, volvamos al tema de
proteger los atributos de peso y precio de LineItem para que solo acepten valores mayores que
cero, pero sin implementar a mano dos pares casi idénticos de getters/setters.
Crearemos una fábrica de propiedades de cantidad , llamada así porque los atributos administrados
representan cantidades que no pueden ser negativas o cero en la aplicación. El ejemplo 19-23
muestra el aspecto limpio de la clase LineItem utilizando dos instancias de propiedades de
cantidad : una para administrar el atributo de peso y la otra para el precio.
def subtotal(self):
return self.weight * self.price
Utilice la fábrica para definir la primera propiedad personalizada, el peso, como un atributo de clase.
Las propiedades también están en uso aquí, recuperando los valores almacenados en la instancia.
Recuerde que las propiedades son atributos de clase. Al construir cada propiedad de cantidad , debemos pasar el
nombre del atributo LineItem que será administrado por esa propiedad específica. Tener que escribir la palabra peso
dos veces en esta línea es desafortunado:
peso = cantidad('peso')
Pero evitar esa repetición es complicado porque la propiedad no tiene forma de saber qué nombre de atributo de clase
se vinculará a ella. Recuerde: el lado derecho de una asignación se evalúa primero, por lo que cuando se invoca la
cantidad () , el atributo de clase de precio ni siquiera existe.
def cantidad(nombre_de_almacenamiento):
def qty_getter(instancia):
devolver instancia.__dict__[nombre_de_almacenamiento]
14. Este código está adaptado de la “Receta 9.21. Cómo evitar los métodos de propiedades repetitivas” de Python
Cookbook, 3E de David Beazley y Brian K. Jones (O'Reilly).
else:
aumentar ValueError('el valor debe ser > 0')
El primer argumento de qty_getter podría llamarse self, pero sería extraño porque no es un cuerpo
de clase; instancia se refiere a la instancia de LineItem donde se almacenará el atributo.
qty_getter hace referencia a storage_name , por lo que se conservará en el cierre de esta función;
el valor se recupera directamente de la instancia.__dict__ para omitir la propiedad y evitar una
recursividad infinita. Se define qty_setter , tomando también la instancia como primer argumento.
Los fragmentos del ejemplo 19-24 que merecen un estudio cuidadoso giran en torno a la variable
nombre_almacenamiento . Cuando codifica cada propiedad de la manera tradicional, el nombre del atributo
donde almacenará un valor está codificado en los métodos getter y setter.
Pero aquí, las funciones qty_getter y qty_setter son genéricas y dependen de la variable storage_name
para saber dónde obtener/establecer el atributo administrado en la instancia __dict__. Cada vez que se
llama a la fábrica de cantidad para construir una propiedad, el nombre_de_almacenamiento debe
establecerse en un valor único.
Las funciones qty_getter y qty_setter serán envueltas por el objeto de propiedad creado en la última línea
de la función de fábrica. Más tarde, cuando se las llame para realizar sus tareas, estas funciones leerán el
nombre_almacenamiento de sus cierres, para determinar dónde recuperar/almacenar los valores de los
atributos administrados.
En el Ejemplo 19-25, creo e inspecciono una instancia de LineItem , exponiendo los atributos de
almacenamiento.
Leer el peso y el precio a través de las propiedades que ocultan los atributos de la instancia del
mismo nombre.
Usando vars para inspeccionar la instancia de nuez moscada : aquí vemos los atributos reales de la
instancia utilizados para almacenar los valores.
Observe cómo las propiedades creadas por nuestra fábrica aprovechan el comportamiento descrito en
“Propiedades anulan atributos de instancia” en la página 608: la propiedad de peso anula el atributo de instancia
de peso para que cada referencia a self.weight o nutmeg.weight sea manejada por la propiedad funciones, y la
única forma de omitir la lógica de la propiedad es acceder a la instancia __dict__ directamente.
El código en el Ejemplo 19-25 puede ser un poco complicado, pero es conciso: es idéntico en longitud al par
getter/setter decorado que define solo la propiedad de peso en el Ejemplo 19-17. La definición de LineItem en
el Ejemplo 19-23 se ve mucho mejor sin el ruido del getter/
setters
En un sistema real, ese mismo tipo de validación puede aparecer en muchos campos, en varias clases, y la
fábrica de cantidad se colocaría en un módulo de utilidad para usarse una y otra vez. Eventualmente, esa
fábrica simple podría refactorizarse en una clase de descriptor más extensible, con subclases especializadas
que realicen diferentes validaciones. Eso lo haremos en el Capítulo 20.
Recuerde del tutorial de Python que los atributos de los objetos se pueden eliminar usando la función del
declaración:
del mi_objeto.un_atributo
En la práctica, eliminar atributos no es algo que hacemos todos los días en Python, y el requisito de manejarlo
con una propiedad es aún más inusual. Pero es compatible, y puedo pensar en un ejemplo tonto para
demostrarlo.
En una definición de propiedad, el decorador @my_propety.deleter se usa para envolver el método encargado
de eliminar el atributo administrado por la propiedad. Como se prometió, el Ejemplo 19-26 es un ejemplo tonto
que muestra cómo codificar un eliminador de propiedades.
Ejemplo 19-26. blackknight.py: inspirado en el personaje Black Knight de “Monty Python and the Holy Grail”
def __init__(self):
self.members = ['un brazo', 'otro brazo', 'una pierna',
'otra pierna'] self.phrases =
["'No es más que un rasguño.",
"Es solo una herida superficial",
"¡Soy invencible!",
@property
def miembro(self):
print('el siguiente miembro es:')
return self.members[0]
@member.deleter def
miembro(self): text =
'BLACK KNIGHT (pierde {})\n-- {}'
print(text.format(self.members.pop(0), self.phrases.pop(0) ))
Ejemplo 19-27. blackknight.py: pruebas documentales para el Ejemplo 19-26 (el Caballero Negro nunca
reconoce la derrota)
Usando la sintaxis de llamada clásica en lugar de decoradores, el argumento fdel se usa para establecer
la función de eliminación. Por ejemplo, la propiedad del miembro se codificaría así en el cuerpo de la
clase BlackKnight :
Si no está utilizando una propiedad, la eliminación de atributos también se puede manejar implementando
el método especial __delattr__ de nivel inferior , presentado en “Métodos especiales para el manejo de
atributos” en la página 617. La codificación de una clase tonta con __delattr__ se deja como ejercicio . al
lector procrastinador.
Las propiedades son una característica poderosa, pero a veces son preferibles alternativas más simples
o de menor nivel. En la sección final de este capítulo, revisaremos algunas de las API principales que
ofrece Python para la programación de atributos dinámicos.
__class__
Una referencia a la clase del objeto (es decir, obj.__class__ es lo mismo que type(obj)).
Python busca métodos especiales como __getattr__ solo en la clase de un objeto, y no en las
instancias mismas.
__dict__
Una asignación que almacena los atributos de escritura de un objeto o clase. Un objeto que tiene
un __dict__ puede tener nuevos atributos arbitrarios establecidos en cualquier momento. Si una
clase tiene un atributo __slots__ , es posible que sus instancias no tengan un __dict__. Ver
__ranuras__ (siguiente).
__ranuras__
Un atributo que se puede definir en una clase para limitar los atributos que pueden tener sus
instancias. __slots__ es una tupla de cadenas que nombra los atributos permitidos.15 Si el nombre
'__dict__' no está en __slots__, entonces las instancias de esa clase no tendrán un __dict__ propio,
y solo se permitirán los atributos con nombre en ellas.
dir([objeto])
Enumera la mayoría de los atributos del objeto. Los documentos oficiales dicen que dir está
diseñado para uso interactivo, por lo que no proporciona una lista completa de atributos, sino un
conjunto de nombres "interesante". dir puede inspeccionar objetos implementados con o sin
__dict__. El atributo __dict__ en sí no está listado por dir, pero las claves __dict__ sí están listadas.
Varios atributos especiales de clases, como __mro__, __bases__ y
15. Alex Martelli señala que, aunque __slots__ se puede codificar como una lista, es mejor ser explícito y usar siempre
una tupla, porque cambiar la lista en __slots__ después de procesar el cuerpo de la clase no tiene ningún efecto, por
lo que sería engañoso. para usar una secuencia mutable allí.
__name__ tampoco están listados por dir . Si no se proporciona el argumento de objeto opcional , dir
enumera los nombres en el ámbito actual.
hasattr(objeto, nombre)
Devuelve True si el atributo mencionado existe en el objeto o se puede recuperar de algún modo a
través de él (por herencia, por ejemplo). La documentación explica: “Esto se implementa llamando a
getattr(objeto, nombre) y viendo si genera un AttributeError o no”.
vars([objeto])
Devuelve el __dict__ del objeto; vars no puede manejar instancias de clases que definen
__slots__ y no tienen un __dict__ (en contraste con dir, que maneja dichas instancias).
Sin un argumento, vars() hace lo mismo que locals(): devuelve un dict que representa el
ámbito local.
se implementan en una clase definida por el usuario, los métodos especiales enumerados aquí manejan la
recuperación, configuración, eliminación y listado de atributos.
El acceso a los atributos utilizando la notación de puntos o las funciones integradas getattr ,
hasattr y setattr activan los métodos especiales apropiados que se enumeran aquí. Leer y
escribir atributos directamente en la instancia __dict__ no activa estos métodos especiales,
y esa es la forma habitual de omitirlos si es necesario.
“Sección 3.3.9. Búsqueda de método especial” del capítulo “Modelo de datos” advierte:
Para las clases personalizadas, solo se garantiza que las invocaciones implícitas de métodos especiales
funcionen correctamente si se definen en el tipo de un objeto, no en el diccionario de instancias del objeto.
En otras palabras, suponga que los métodos especiales se recuperarán en la propia clase, incluso cuando
el objetivo de la acción sea una instancia. Por esta razón, los atributos de instancia con el mismo nombre no
ocultan los métodos especiales.
En los siguientes ejemplos, suponga que hay una clase llamada Class, obj es una instancia
de Class y attr es un atributo de obj.
Para cada uno de estos métodos especiales, no importa si el acceso a los atributos se
realiza mediante la notación de puntos o una de las funciones integradas enumeradas en
“Funciones integradas para el manejo de atributos” en la página 616. Por ejemplo, tanto
obj .attr y getattr(obj, 'attr', 42) activan Class.__getattribute__(obj, 'attr').
__delattr__(yo, nombre)
Siempre llamado cuando hay un intento de eliminar un atributo usando la instrucción
del ; por ejemplo, del obj.attr activa Class.__delattr__(obj, 'attr').
__dir__(uno mismo)
Llamado cuando se invoca dir en el objeto, para proporcionar una lista de atributos; por
ejemplo, dir(obj) activa Class.__dir__(obj).
__getattr__(yo, nombre)
Solo se llama cuando falla un intento de recuperar el atributo nombrado, después de
buscar obj, Class y sus superclases. Las expresiones obj.no_such_attr, get attr(obj,
'no_such_attr') y hasattr(obj, 'no_such_attr') pueden desencadenar
Class.__getattr__(obj, 'no_such_attr'), pero solo si no se puede encontrar un atributo
con ese nombre en obj o en Class y sus superclases.
__getattribute__(yo, nombre)
Siempre llamado cuando hay un intento de recuperar el atributo nombrado, excepto
cuando el atributo buscado es un atributo o método especial. La notación de puntos y
los incorporados get attr y hasattr activan este método. __getattr__ solo se invoca
después de __getattribute__, y solo cuando __getattribute__ genera AttributeError.
Para recuperar los atributos de la instancia obj sin desencadenar una recurrencia
infinita, las implementaciones de __getattribute__ deben usar super().__getattri
bute__(obj, nombre).
Esto concluye nuestra inmersión en propiedades, métodos especiales y otras técnicas para
codificar atributos dinámicos.
FrozenJSON demostró el uso del método especial __getattr__ para convertir estructuras de datos sobre la
marcha, siempre que se leyeron sus atributos. La última versión de FrozenJSON mostró el uso del método
constructor __new__ para transformar una clase en una fábrica flexible de objetos, sin limitarse a instancias
de sí mismo.
Luego, convertimos la fuente JSON en una base de datos shelve.Shelf que almacena instancias serializadas
de una clase Record . La primera versión de Record tenía unas pocas líneas e introdujo el modismo "bunch":
usar self.__dict__.update(**kwargs) para construir atributos arbitrarios a partir de argumentos de palabras
clave pasados a __init__. La segunda iteración de este ejemplo vio la extensión de Record con una clase
DbRecord para la integración de bases de datos y una clase Event que implementa la recuperación
automática de registros vinculados a través de propiedades.
La cobertura de propiedades continuó con la clase LineItem , donde se implementó una propiedad para
proteger un atributo de peso de valores negativos o cero que no tienen sentido comercial. Después de una
mirada más profunda a la sintaxis y la semántica de las propiedades, creamos una fábrica de propiedades
para aplicar la misma validación en el peso y el precio, sin codificar múltiples captadores y definidores. La
fábrica de propiedades aprovechó conceptos sutiles, como los cierres y el atributo de instancia que
reemplaza a las propiedades, para proporcionar una solución genérica elegante que usa la misma cantidad
de líneas que una sola definición de propiedad codificada a mano.
Finalmente, echamos un vistazo breve al manejo de la eliminación de atributos con propiedades, seguido de
una descripción general de los atributos especiales clave, las funciones integradas y los métodos especiales
que admiten la metaprogramación de atributos en el lenguaje principal de Python.
Otras lecturas
La documentación oficial para el manejo de atributos y las funciones integradas de introspección es el
Capítulo 2, "Funciones integradas" de la biblioteca estándar de Python. Los métodos especiales relacionados
y el atributo especial __slots__ están documentados en The Python Language Reference en “3.3.2.
Personalización del acceso a los atributos”. La semántica de cómo se invocan los métodos especiales sin
pasar por las instancias se explica en “3.3.9. Búsqueda de métodos especiales”. En el Capítulo 4, “Tipos
integrados”, de la Biblioteca estándar de Python, “4.13. Atributos especiales” cubre los atributos __class__ y
__dict__ .
Python Cookbook, 3E de David Beazley y Brian K. Jones (O'Reilly) tiene varias recetas que cubren los
temas de este capítulo, pero destacaré tres que son sobresalientes: “Receta 8.8. Extender una propiedad
en una subclase” aborda el espinoso problema de anular los métodos dentro de una propiedad heredada de
una superclase; “Receta 8.15. Delegar acceso de atributo” implementa una clase de proxy que muestra la
mayoría de los métodos especiales de “Speÿ
Métodos especiales para el manejo de atributos” en la página 617 de este libro; y la increíble
“Receta 9.21. Evitar los métodos de propiedades repetitivas”, que fue la base de la función de
fábrica de propiedades presentada en el ejemplo 19-24.
Python in a Nutshell, 2E (O'Reilly), de Alex Martelli, cubre solo Python 2.5, pero los fundamentos
aún se aplican a Python 3 y su tratamiento es riguroso y objetivo. Martelli dedica solo tres páginas
a las propiedades, pero eso se debe a que el libro sigue un estilo de presentación axiomático: las
15 páginas anteriores brindan una descripción detallada de la semántica de las clases de Python
desde cero, incluidos los descriptores, que es cómo se implementan realmente las propiedades.
bajo el capó. Entonces, cuando llega a las propiedades, puede incluir muchas ideas en esas tres
páginas, incluida la que seleccioné para abrir este capítulo.
Bertrand Meyer, citado en la definición del Principio de acceso uniforme en la apertura de este
capítulo, escribió el excelente Object-Oriented Software Construction, 2E (Prentice-Hall). El libro
tiene más de 1.250 páginas y confieso que no lo leí todo, pero los primeros seis capítulos brindan
una de las mejores introducciones conceptuales al análisis y diseño OO que he visto, el Capítulo
11 presenta el Diseño por Contrato (Meyer inventó el método y acuñó el término), y el Capítulo
35 ofrece sus evaluaciones de algunos lenguajes OO clave: Simula, Smalltalk, CLOS (la extensión
Lisp OO), Objective-C, C++ y Java, con breves comentarios sobre algunos otros. Meyer también
es el inventor del pseudo-pseudocódigo: solo en la última página del libro revela que la "notación"
que usa como pseudocódigo es de hecho Eiffel.
Plataforma improvisada
El principio de acceso uniforme de Meyer (a veces llamado UAP por los amantes de las siglas) es
estéticamente atractivo. Como programador que usa una API, no debería importarme si coco.precio
simplemente obtiene un atributo de datos o realiza un cálculo. Como consumidor y ciudadano, me
importa: en el comercio electrónico actual, el valor del coco . El precio a menudo depende de quién
pregunta, por lo que ciertamente no es un mero atributo de datos. De hecho, es una práctica común
que el precio sea más bajo si la consulta proviene de fuera de la tienda, por ejemplo, de un motor de
comparación de precios. Esto castiga efectivamente a los clientes leales a quienes les gusta navegar
dentro de una tienda en particular. Pero yo divago.
La digresión anterior plantea un punto relevante para la programación: aunque el principio de acceso
uniforme tiene mucho sentido en un mundo ideal, en realidad los usuarios de una API pueden necesitar
saber si leer coco.precio es potencialmente demasiado costoso o requiere mucho tiempo. Como es
habitual en cuestiones de ingeniería de software, el Wiki original de Ward Cunningham alberga
argumentos perspicaces sobre los méritos del Principio de Acceso Uniforme.
En los lenguajes de programación orientados a objetos, la aplicación o las violaciones del principio de
acceso uniforme generalmente giran en torno a la sintaxis de leer atributos de datos públicos versus
invocar métodos de obtención/establecimiento.
Smalltalk y Ruby abordan este problema de una manera simple y elegante: no admiten atributos de datos públicos
en absoluto. Cada atributo de instancia en estos lenguajes es privado, por lo que cada acceso a ellos debe ser a
través de métodos. Pero su sintaxis hace que esto sea sencillo: en Ruby, coco.precio invoca al captador de precios ;
en Smalltalk, es simplemente precio de coco.
En el otro extremo del espectro, el lenguaje Java permite al programador elegir entre cuatro modificadores de nivel
de acceso.16 Sin embargo, la práctica general no está de acuerdo con la sintaxis establecida por los diseñadores
de Java. Todo el mundo en Java está de acuerdo en que los atributos deben ser privados, y debe deletrearlo todo
el tiempo, porque no es el valor predeterminado.
Cuando todos los atributos son privados, todo acceso a ellos desde fuera de la clase debe pasar por los accesores.
Los IDE de Java incluyen accesos directos para generar métodos de acceso automáticamente.
Desafortunadamente, el IDE no es tan útil cuando debe leer el código seis meses después.
Depende de usted navegar a través de un mar de accesores que no hacen nada para encontrar aquellos que
agregan valor al implementar alguna lógica de negocios.
Alex Martelli habla en nombre de la mayoría de la comunidad de Python cuando llama a los accesores "modismos
ridículos" y luego proporciona estos ejemplos que se ven muy diferentes pero hacen lo mismo:17
algunaInstancia.widgetCounter += 1 #
en lugar de...
algunaInstancia.setWidgetCounter(algunInstancia.getWidgetCounter() + 1)
A veces, al diseñar API, me he preguntado si todos los métodos que no toman un argumento (además de uno
mismo), devuelven un valor (que no sea Ninguno) y son una función pura (es decir, no tienen efectos secundarios)
deben ser reemplazados por un propiedad de solo lectura. En este capítulo, el método LineItem.subtotal (como en
el Ejemplo 19-23) sería un buen candidato para convertirse en una propiedad de solo lectura. Por supuesto, esto
excluye los métodos que están diseñados para cambiar el objeto, como my_list.clear(). ¡Sería una idea terrible
convertir eso en una propiedad, de modo que el simple acceso a my_list.clear eliminaría el contenido de la lista!
En la biblioteca GPIO de Pingo.io (mencionada en “El método __missing__” en la página 72), gran parte de la API
a nivel de usuario se basa en propiedades. Por ejemplo, para leer el valor actual de un pin analógico, el usuario
escribe pin.value, y la configuración de un modo de pin digital se escribe como pin.mode = OUT. Detrás de escena,
leer un valor de pin analógico o configurar un modo de pin digital puede implicar una gran cantidad de código,
según el controlador de placa específico. Decidimos usar propiedades en Pingo porque queremos que la API sea
cómoda de usar incluso en entornos interactivos como iPython Notebook, y sentimos que pin.mode = OUT es más
fácil para los ojos y los dedos que pin.set_mode(OUT ).
Aunque encuentro la solución de Smalltalk y Ruby más limpia, creo que el enfoque de Python tiene más sentido
que el de Java. Se nos permite comenzar a codificar memorias de datos simples.
16. Incluido el valor predeterminado sin nombre que el Tutorial de Java llama "paquete privado".
bras como atributos públicos, porque sabemos que siempre pueden estar envueltos por propiedades (o
descriptores, de los que hablaremos en el próximo capítulo).
Otro ejemplo del principio de acceso uniforme (o una variación del mismo) es el hecho de que las
llamadas a funciones y la creación de instancias de objetos usan la misma sintaxis en Python: my_obj =
foo(), donde foo puede ser una clase o cualquier otra llamada.
Otros lenguajes influenciados por la sintaxis de C++ tienen un nuevo operador que hace que la creación
de instancias se vea diferente a una llamada. La mayoría de las veces, al usuario de una API no le
importa si foo es una función o una clase. Hasta hace poco, tenía la impresión de que la propiedad era
una función. En uso normal, no hace ninguna diferencia.
Hay muchas buenas razones para reemplazar constructores con fábricas.18 Un motivo popular es limitar
el número de instancias, devolviendo las construidas previamente (como en el patrón Singleton). Un uso
relacionado es almacenar en caché la construcción de objetos costosos. Además, a veces es conveniente
devolver objetos de diferentes tipos dependiendo de los argumentos dados.
Codificar un constructor es más simple; proporcionar una fábrica agrega flexibilidad a expensas de más
código. En los lenguajes que tienen un operador nuevo , el diseñador de una API debe decidir de
antemano si se queda con un constructor simple o si invierte en la fábrica. Si la elección inicial es
incorrecta, la corrección puede ser costosa, todo porque new es un operador.
A veces también puede ser conveniente ir por el otro lado y reemplazar una función simple con una
clase.
En Python, las clases y funciones son intercambiables en muchas situaciones. No solo porque no hay
operador new , sino también porque existe el método especial __new__ , que puede convertir una clase
en una fábrica que produce objetos de diferentes tipos (como vimos en “Creación flexible de objetos con
__new__” en la página 592) o devolver instancias precompiladas en lugar de crear una nueva cada vez.
Esta dualidad de clase de función sería más fácil de aprovechar si PEP 8 — Guía de estilo para el
código de Python no recomendara CamelCase para los nombres de clase. Por otro lado, docenas de
clases en la biblioteca estándar tienen nombres en minúsculas (p. ej., property, str, defauldict, etc.).
Entonces, tal vez el uso de nombres de clase en minúsculas es una característica y no un error. Pero,
como quiera que lo miremos, el uso inconsistente de las clases en mayúsculas en la biblioteca estándar
de Python plantea un problema de usabilidad.
Aunque llamar a una función no es diferente a llamar a una clase, es bueno saber cuál es cuál debido a
otra cosa que podemos hacer con una clase: crear subclases. Así que personalmente uso CamelCase
en cada clase que codifico, y deseo que todas las clases estén en el estándar de Python
18. Las razones que estoy a punto de mencionar se dan en el artículo del Dr. Dobbs Journal titulado “Java's new
Considered Harmful”, de Jonathan Amsterdam y en “Consider static factory Methods lugar of constructors”,
que es el artículo 1 del galardonado libro Java eficaz (Addison-Wesley) de Joshua Bloch.
CAPÍTULO 20
Descriptores de atributos
Aprender acerca de los descriptores no solo brinda acceso a un conjunto de herramientas más grande, sino
que crea una comprensión más profunda de cómo funciona Python y una apreciación de la elegancia de su
diseño.1 — Raymond
Hettinger Desarrollador central y gurú de Python
Los descriptores son una forma de reutilizar la misma lógica de acceso en múltiples atributos. Por ejemplo, los
tipos de campo en los ORM, como Django ORM y SQL Alchemy, son descriptores que administran el flujo de
datos desde los campos en un registro de la base de datos hasta los atributos de los objetos de Python y
viceversa.
Un descriptor es una clase que implementa un protocolo que consta de los métodos __get__ , __set__ y
__delete__ . La clase de propiedad implementa el protocolo descriptor completo.
Como es habitual con los protocolos, las implementaciones parciales están bien. De hecho, la mayoría de los
descriptores que vemos en el código real implementan solo __get__ y __set__, y muchos implementan solo uno
de estos métodos.
Los descriptores son una característica distintiva de Python, implementada no solo a nivel de aplicación sino
también en la infraestructura del lenguaje. Además de las propiedades, otras características de Python que
aprovechan los descriptores son los métodos y los decoradores classmethod y staticmethod . Comprender los
descriptores es clave para el dominio de Python. De esto trata este capítulo.
625
Machine Translated by Google
funciones de acceso y crea una instancia de propiedad personalizada a partir de ellas, con cierres para
contener configuraciones como storage_name. La forma orientada a objetos de resolver el mismo
problema es una clase descriptor.
Crearemos un descriptor de cantidad y la clase LineItem utilizará dos instancias de cantidad: una para
administrar el atributo de peso y la otra para el precio. Un diagrama ayuda, así que eche un vistazo a la
Figura 20-1.
Figura 20-1. Diagrama de clase UML para LineItem usando una clase de descriptor llamada Cantidad.
Los atributos subrayados en UML son atributos de clase. Tenga en cuenta que el peso y el precio son
instancias de Cantidad adjuntas a la clase LineItem, pero las instancias de LineItem también tienen sus
propios atributos de peso y precio donde se almacenan esos valores.
Tenga en cuenta que la palabra peso aparece dos veces en la figura 20-1, porque en realidad hay dos
atributos distintos llamados peso: uno es un atributo de clase de LineItem, el otro es un atributo de
instancia que existirá en cada objeto LineItem . Esto también se aplica al precio.
Clase administrada
La clase en la que las instancias del descriptor se declaran como atributos de clase: elemento
LineI en la figura 20-1.
Instancia de descriptor
Cada instancia de una clase de descriptor, declarada como un atributo de clase de la clase gestionada.
En la figura 20-1, cada instancia de descriptor está representada por una flecha de composición con un
nombre subrayado (el subrayado significa atributo de clase en UML). Los diamantes negros tocan la
clase LineItem , que contiene las instancias del descriptor.
Instancia administrada
Una instancia de la clase administrada. En este ejemplo, las instancias de LineItem serán las instancias
administradas (no se muestran en el diagrama de clases).
Atributo de
almacenamiento Un atributo de la instancia administrada que contendrá el valor de un atributo
administrado para esa instancia en particular. En la Figura 20-1, los atributos peso y precio de la
instancia de LineItem serán los atributos de almacenamiento. Son distintos de las instancias del
descriptor, que siempre son atributos de clase.
Atributo administrado
Un atributo público en la clase administrada que será manejado por una instancia de descriptor, con
valores almacenados en atributos de almacenamiento. En otras palabras, una instancia de descriptor y
un atributo de almacenamiento proporcionan la infraestructura para un atributo administrado.
Es importante darse cuenta de que las instancias de cantidad son atributos de clase de LineItem. Este punto
crucial está resaltado por los molinos y artilugios en la Figura 20-2.
Figura 20-2. Diagrama de clase UML anotado con MGN (notación Mills & Gizmos): las clases son molinos
que producen gizmos: las instancias. El molino Cantidad produce dos artilugios rojos, que se adjuntan al
molino LineItem: peso y precio. El molino LineItem produce artilugios azules que tienen sus propios atributos
de peso y precio donde se almacenan esos valores.
Después de explicar los descriptores muchas veces, me di cuenta de que UML no es muy bueno
para mostrar las relaciones entre clases e instancias, como la relación entre una clase administrada
y las instancias del descriptor.2 Así que inventé mi propio "lenguaje", el Mills & Gizmos Notation
(MGN), que utilizo para anotar diagramas UML.
MGN está diseñado para dejar muy clara la distinción entre clases e instancias. Consulte la Figura
20-3. En MGN, una clase se dibuja como un "molino", una máquina complicada que produce
artilugios. Las clases/molinos son siempre máquinas con palancas y diales. Los artilugios son las
instancias, y parecen mucho más simples. Un artilugio es del mismo color que el molino que lo
fabricó.
Figura 20-3. Boceto de MGN que muestra la clase LineItem creando tres instancias y Cantidad
creando dos. Una instancia de Cantidad está recuperando un valor almacenado en una instancia
de LineItem.
Para este ejemplo, dibujé las instancias de LineItem como filas en una factura tabular, con tres
celdas que representan los tres atributos (descripción, peso y precio). Debido a que las instancias
de Cantidad son descriptores, tienen una lupa para __obtener__ valores y una garra para
__establecer__ valores. Cuando lleguemos a las metaclases, me agradecerán estos garabatos.
Basta de garabatos por ahora. Aquí está el código: El ejemplo 20-1 muestra la clase de descriptor de
cantidad y una nueva clase de elemento de línea que usa dos instancias de cantidad.
cantidad de clase :
2. Las clases y las instancias se dibujan como rectángulos en los diagramas de clases UML. Hay diferencias visuales, pero en
las posiciones rara vez se muestran en los diagramas de clases, por lo que es posible que los desarrolladores no las reconozcan como tales.
class LineItem:
peso = Cantidad('peso') precio =
Cantidad('precio')
def subtotal(self):
return self.weight * self.price
Cada instancia de Cantidad tendrá un atributo storage_name : ese es el nombre del atributo que
contendrá el valor en las instancias administradas. __set__ se llama cuando hay un intento de
Aquí, debemos manejar la instancia administrada __dict__ directamente; tratar de usar el setattr
incorporado activaría el método __set__ nuevamente, lo que llevaría a una recursividad infinita.
El resto del cuerpo de la clase es tan simple y limpio como el código original en bulkfood_v1.py
(Ejemplo 19-15).
En el Ejemplo 20-1, cada atributo administrado tiene el mismo nombre que su atributo de almacenamiento
y no hay una lógica de obtención especial, por lo que Cantidad no necesita un método __obtener__ .
El código del ejemplo 20-1 funciona según lo previsto, evitando la venta de trufas por $0:3
3. Las trufas blancas cuestan miles de dólares por libra. Desautorizar la venta de trufas a $0,01 se deja como ejercicio
para el lector emprendedor. Conozco a una persona que en realidad compró una enciclopedia de estadísticas de $
1,800 por $ 18 debido a un error en una tienda en línea (no en Amazon.com).
Puede ser tentador, pero erróneo, almacenar el valor de cada atributo administrado en la propia
instancia del descriptor. En otras palabras, en el método __set__ , en lugar de codificar:
instancia.__dict__[self.nombre_de_almacenamiento] = valor
self.__dict__[self.nombre_de_almacenamiento] = valor
Para entender por qué esto estaría mal, piensa en el significado de los dos primeros argumentos
para __set__: self e instancia. Aquí, self es la instancia del descriptor, que en realidad es un
atributo de clase de la clase administrada. Puede tener miles de instancias de elementos de
LineItem en la memoria al mismo tiempo, pero solo tendrá dos instancias de los descriptores:
LineItem.weight y LineItem.price. Por lo tanto, cualquier cosa que almacene en las instancias del
descriptor es en realidad parte de un atributo de clase LineItem y, por lo tanto, se comparte entre
todas las instancias de LineItem .
Un inconveniente del ejemplo 20-1 es la necesidad de repetir los nombres de los atributos cuando
se instancian los descriptores en el cuerpo de la clase administrada. Sería bueno si la clase tem
LineI pudiera declararse así:
clase LineItem:
peso = Cantidad()
precio = Cantidad()
El problema es que, como vimos en el Capítulo 8, el lado derecho de una asignación se ejecuta
antes de que exista la variable. La expresión Cantidad() se evalúa para crear una instancia de
descriptor y, en este momento, no hay forma de que el código de la clase Cantidad pueda adivinar
el nombre de la variable a la que se vinculará el descriptor (p. ej., peso o precio).
Tal como está, el Ejemplo 20-1 requiere nombrar cada Cantidad explícitamente, lo que no solo es
inconveniente sino peligroso: si un programador copia y pega el código se olvida de editar ambos
nombra y escribe algo como precio = Cantidad('peso'), el programa se comportará mal, golpeando el valor del
peso siempre que se establezca el precio .
A continuación se presenta una solución no tan elegante pero viable para el problema de los nombres repetidos.
Las mejores soluciones requieren un decorador de clases o una metaclase, así que las dejaré para el Capítulo
21.
evitar volver a escribir el nombre del atributo en las declaraciones del descriptor, generaremos una cadena única
para el nombre_de_almacenamiento de cada instancia de Cantidad . La figura 20-4 muestra el diagrama UML
actualizado para las clases Cantidad y Elemento de línea .
Figura 20-4. Diagrama de clases UML para el ejemplo 20-2. Ahora Cantidad tiene métodos de obtención y
configuración, y las instancias de LineItem tienen atributos de almacenamiento con nombres generados:
_Quantity#0 y _Quantity#1.
Ejemplo 20-2. bulkfood_v4.py: cada descriptor de cantidad obtiene un nombre de almacenamiento único
Clase Cantidad:
__contador = 0
clase LineItem:
peso = Cantidad()
precio = Cantidad()
def subtotal(self):
return self.weight * self.price
Aquí podemos usar los incorporados getattr y setattr de nivel superior para almacenar el valor, en
lugar de recurrir a instancia.__dict__, porque el atributo administrado y el atributo de almacenamiento
tienen nombres diferentes, por lo que llamar a getattr en el atributo de almacenamiento no activará
el descriptor, evitando la recursividad infinita discutida en el ejemplo 20-1.
Si prueba bulkfood_v4.py, puede ver que los descriptores de peso y precio funcionan como se
esperaba, y los atributos de almacenamiento también se pueden leer directamente, lo que es útil
para la depuración:
Tenga en cuenta que __get__ recibe tres argumentos: self, instancia y propietario. El argumento del
propietario es una referencia a la clase administrada (por ejemplo, LineItem) y es útil cuando se usa
el descriptor para obtener atributos de la clase. Si un atributo administrado, como el peso, se
recupera a través de una clase como LineItem.weight, el método __get__ del descriptor recibe
Ninguno como valor para el argumento de la instancia . Esto explica el error de atributo en la próxima
sesión de consola:
Generar AttributeError es una opción al implementar __get__, pero si elige hacerlo, el mensaje debe
corregirse para eliminar la mención confusa de NoneType y _Quantity#0, que son detalles de
implementación. Un mejor mensaje sería "La clase 'LineI tem' no tiene tal atributo". Idealmente, el
nombre del atributo que falta debería estar escrito, pero el descriptor no conoce el nombre del
atributo administrado en este ejemplo, por lo que no podemos hacerlo mejor en este punto.
Por otro lado, para apoyar la introspección y otros trucos de metaprogramación por parte del usuario,
es una buena práctica hacer que __get__ devuelva la instancia del descriptor cuando el manÿ
Se accede al atributo envejecido a través de la clase. El ejemplo 20-3 es una variación menor del ejemplo 20-2, que
agrega un poco de lógica a Cantidad.__obtener__.
Ejemplo 20-3. bulkfood_v4b.py (listado parcial): cuando se invoca a través de la clase administrada, obtiene una
referencia al descriptor en sí
Clase Cantidad:
__contador = 0
else:
devuelve getattr(instancia, self.storage_name)
Mirando el Ejemplo 20-2, puede pensar que es mucho código solo para administrar un par de atributos, pero es
importante darse cuenta de que la lógica del descriptor ahora se abstrae en una unidad de código separada: la clase
Cantidad . Por lo general, no definimos un descriptor en el mismo módulo donde se usa, sino en un módulo de utilidad
separado diseñado para usarse en toda la aplicación, incluso en muchas aplicaciones, si está desarrollando un marco.
Con esto en mente, el ejemplo 20-4 representa mejor el uso típico de un descriptor.
Ejemplo 20-4. bulkfood_v4c.py: definición de LineItem despejada; la clase de descriptor de cantidad ahora
reside en el módulo model_v4c importado
clase LineItem:
peso = modelo.Cantidad() precio
= modelo.Cantidad()
Los usuarios de Django notarán que el Ejemplo 20-4 se parece mucho a una definición de modelo. No es
coincidencia: los campos del modelo de Django son descriptores.
Debido a que los descriptores se definen en clases, podemos aprovechar la herencia para reutilizar parte del
código que tenemos para nuevos descriptores. Eso es lo que haremos en la siguiente sección.
No es difícil volver a implementar la clase de descriptor mejorada del Ejemplo 20-2 agregando
algunas líneas a la fábrica de propiedades que se muestra en el Ejemplo 19-24. La variable
__counter presenta una dificultad, pero podemos hacer que persista entre las invocaciones de la
fábrica definiéndola como un atributo del propio objeto de función de fábrica, como se muestra en el ejemplo 20-5.
Ejemplo 20-5. bulkfood_v4prop.py: misma funcionalidad que el Ejemplo 20-2 con una fábrica de
propiedades en lugar de una clase de descriptor
def cantidad():
prueba:
cantidad.contador += 1
excepto AttributeError:
cantidad.contador = 0
def qty_getter(instancia):
devuelve getattr(instancia, nombre_de_almacenamiento)
No podemos confiar en los atributos de clase para compartir el contador entre invocaciones, por
lo que lo definimos como un atributo de la función de cantidad en sí.
Tampoco tenemos atributos de instancia, por lo que creamos storage_name como una variable
local y confiamos en los cierres para mantenerlos vivos para su uso posterior por qty_getter y
qty_setter.
El código restante es idéntico al Ejemplo 19-24, excepto que aquí podemos usar los incorporados
getattr y setattr en lugar de jugar con instance.__dict__.
• Una clase de descriptor puede extenderse mediante subclases; reutilizar el código de una función
de fábrica sin copiar y pegar es mucho más difícil. • Es más sencillo mantener el estado en atributos
de clase e instancia que en atributos de función y cierres como tuvimos que hacer en el Ejemplo 20-5.
Por otro lado, cuando explico el Ejemplo 20-5, no siento la necesidad de dibujar molinos y artilugios. El
código de la fábrica de propiedades no depende de relaciones de objetos extrañas evidenciadas por
métodos descriptores que tienen argumentos llamados self e instancia.
Para resumir, el patrón de fábrica de propiedades es más simple en algunos aspectos, pero el enfoque
de clase de descriptor es más extensible. También es más utilizado.
tienda imaginaria de alimentos orgánicos se topa con un inconveniente: de alguna manera, se creó una instancia
de línea de pedido con una descripción en blanco y no se pudo completar el pedido. Para evitar eso, crearemos
un nuevo descriptor, NonBlank. A medida que diseñamos NonBlank, nos damos cuenta de que será muy parecido
al descriptor de cantidad , excepto por la lógica de validación.
Reflexionando sobre la funcionalidad de Cantidad, notamos que hace dos cosas diferentes: se ocupa de los
atributos de almacenamiento en las instancias administradas y valida el valor utilizado para establecer esos
atributos. Esto provoca una refactorización, produciendo dos clases base:
AutoStorage
Clase de descriptor que administra los atributos de almacenamiento automáticamente.
Validado
Subclase abstracta de AutoStorage que anula el método __set__ , llamando a un método de validación que
deben implementar las subclases.
Figura 20-5. Una jerarquía de clases de descriptores. La clase base AutoStorage administra el almacenamiento
automático del atributo, Validated maneja la validación delegando a un método de validación abstracto, Cantidad
y NonBlank son subclases concretas de Validated.
La relación entre Validado, Cantidad y No en blanco es una aplicación del patrón de diseño Método de plantilla.
En particular, Validated.__set__ es un claro ejemplo de lo que Gang of Four describe como un método de plantilla:
Un método de plantilla define un algoritmo en términos de operaciones abstractas que las subclases anulan
para proporcionar un comportamiento concreto.4
4. Gamma et al., Patrones de diseño: elementos de software orientado a objetos reutilizable, pág. 326.
importar abc
clase
AutoAlmacenamiento: __contador = 0
@abc.abstractmethod def
validar(auto, instancia, valor):
"""devolver valor validado o generar ValueError"""
value = value.strip() if
len(value) == 0: raise
ValueError('el valor no puede estar vacío o en blanco')
valor devuelto
Requerir que los métodos de validación concretos devuelvan el valor validado les da la
oportunidad de limpiar, convertir o normalizar los datos recibidos. En este caso, el valor se
devuelve sin espacios en blanco iniciales y finales.
Los usuarios de model_v5.py no necesitan conocer todos estos detalles. Lo que importa es que
pueden usar Cantidad y No en blanco para automatizar la validación de atributos de instancia. Consulte
la clase LineItem más reciente en el ejemplo 20-7.
clase LineItem:
descripción = modelo.NonBlank() peso
= modelo.Cantidad() precio =
modelo.Cantidad()
Los ejemplos de LineItem que hemos visto en este capítulo demuestran un uso típico de descriptores
para administrar atributos de datos. Este descriptor también se denomina descriptor de reemplazo
porque su método __set__ reemplaza (es decir, interrumpe y anula) la configuración de un atributo
con el mismo nombre en la instancia administrada. Sin embargo, también hay descriptores no
predominantes. Exploraremos esta distinción en detalle en la siguiente sección.
Esta asimetría también afecta a los descriptores, creando de hecho dos amplias categorías de
descriptores dependiendo de si se define el método __set__ . Observar los diferentes comportamientos
requiere algunas clases, por lo que usaremos el código del Ejemplo 20-8 como nuestro banco de
pruebas para las siguientes secciones.
def cls_name(obj_or_cls):
cls = tipo(obj_or_cls) si cls
es tipo: cls = obj_or_cls
return
cls.__name__.split('.')[-1]
más:
devuelve '<{} objeto>'.format(cls_name(obj))
Anulación de clase :
"""también conocido como descriptor de datos o descriptor forzado"""
clase OverridingNoGet:
"""un descriptor anulado sin ``__get__``"""
class NonOverriding:
"""también conocido como descriptor no data o shadowable"""
Clase
administrada: over =
Overriding( ) over_no_get =
OverridingNoGet( ) non_over = NonOverriding()
def spam(auto):
print('-> Managed.spam({})'.format(display(self)))
No hay ningún método __set__ aquí, por lo que este es un descriptor no predominante.
La clase administrada, utilizando una instancia de cada una de las clases descriptoras.
El método de spam está aquí para comparar, porque los métodos también son descriptores.
Descriptor de anulación Un
general, los descriptores sobreescritos implementan tanto __set__ como __get__, pero
también es posible implementar solo __set__, como vimos en el ejemplo 20-1. En este
caso, solo la escritura es manejada por el descriptor. Leer el descriptor a través de una
instancia devolverá el objeto del descriptor porque no hay __get__ para manejar ese
acceso. Si se crea un atributo de instancia del mismo nombre con un nuevo valor a través
del acceso directo a la instancia __dict__, el método __set__ aún anulará los intentos
posteriores de establecer ese atributo, pero leer ese atributo simplemente devolverá el
nuevo valor de la instancia, en lugar de devolver el objeto descriptor. En otras palabras, el
atributo de instancia sombreará el descriptor, pero solo durante la lectura. Vea el Ejemplo 20-10.
9
>>> obj.over_no_get = 7 ->
OverridingNoGet.__set__(<OverridingNoGet object>, < Objeto administrado >, 7) >>>
obj.over_no_get
9
Este descriptor principal no tiene un método __get__ , por lo que leer obj.over_no_get
recupera la instancia del descriptor de la clase.
Lo mismo sucede si recuperamos la instancia del descriptor directamente de la clase
administrada.
Intentar establecer un valor para obj.over_no_get invoca el método descriptor
__set__ .
Ahora ese atributo de instancia over_no_get sombrea el descriptor, pero solo para
lectura.
Pero para la lectura, ese descriptor está sombreado siempre que haya un atributo de
instancia del mismo nombre.
Descriptor no anulado Si un
obj.non_over activa el método __get__ del descriptor , pasando obj como segundo argumento.
El descriptor Managed.non_over todavía está allí y detecta este acceso a través de la clase.
Luego, leer obj.non_over golpea el método __get__ del descriptor en la clase, pero tenga en
cuenta que el segundo argumento es la instancia administrada.
En los ejemplos anteriores, vimos varias asignaciones a un atributo de instancia con el mismo nombre
que un descriptor y diferentes resultados según la presencia de un método __set__ en el descriptor.
La configuración de atributos en la clase no puede ser controlada por descriptores adjuntos a la misma
clase. En particular, esto significa que los propios atributos del descriptor se pueden eliminar asignándolos
a la clase, como se explica en la siguiente sección.
El ejemplo 20-12 revela otra asimetría con respecto a los atributos de lectura y escritura: aunque la
lectura de un atributo de clase puede ser controlada por un descriptor con __get__ adjunto a la clase
administrada, la escritura de un atributo de clase no puede ser manejada por un descriptor con __set__
adjunto a la misma clase.
Centrémonos ahora en cómo se utilizan los descriptores para implementar métodos en Python.
el usuario tienen un método __get__ , por lo tanto, funcionan como descriptores cuando se adjuntan a una clase.
El ejemplo 20-13 demuestra la lectura del método spam de la clase Managed presentada en el ejemplo 20-8.
Asignar un valor a obj.spam sombrea el atributo de clase, lo que hace que el método de spam sea
inaccesible desde la instancia de obj .
Debido a que las funciones no implementan __set__, son descriptores no superpuestos, como muestra la última
línea del ejemplo 20-13 .
La otra conclusión clave del Ejemplo 20-13 es que obj.spam y Managed.spam recuperan diferentes objetos.
Como es habitual con los descriptores, el __get__ de una función devuelve una referencia a sí mismo cuando el
acceso ocurre a través de la clase administrada. Pero cuando el acceso pasa por una instancia, el __get__ de
la función devuelve un objeto de método vinculado: un invocable que envuelve la función y vincula la instancia
administrada (por ejemplo, obj) al primer argumento de la función (es decir, self), como la función functools.partial
sí lo hace (como se ve en “Congelación de argumentos con functools.partial” en la página 159).
Para una comprensión más profunda de este mecanismo, observe el Ejemplo 20-14.
importar colecciones
clase Texto(colecciones.CadenaUsuario):
def __repr__(self):
devuelve 'Texto({!r})'.format(self.data)
def inversa(auto):
devolver uno mismo[::-1]
La repr de una instancia de Text parece una llamada al constructor de Text que haría
una instancia igual.
Text.reverse opera como una función, incluso trabajando con objetos que no son
instancias de Texto.
El objeto del método enlazado tiene un atributo __self__ que contiene una referencia al
instancia en la que se llamó al método.
El atributo __func__ del método enlazado es una referencia a la función original adjunta a la clase administrada.
El objeto de método enlazado también tiene un método __call__ , que maneja la invocación real. Este método llama a
la función original a la que se hace referencia en __func__, pasando el atributo __self__ del método como primer
argumento. Así es como funciona la vinculación implícita del autoargumento convencional .
La forma en que las funciones se convierten en métodos vinculados es un excelente ejemplo de cómo los descriptores
se utilizan como infraestructura en el lenguaje.
Después de esta inmersión profunda en cómo funcionan los descriptores y los métodos, veamos algunos consejos
prácticos sobre su uso.
La siguiente lista aborda algunas consecuencias prácticas de las características del descriptor recién descritas:
5. Python no es consistente en tales mensajes. Intentar cambiar el atributo c.real de un número complejo
obtiene AttributeError: atributo de solo lectura, pero un intento de cambiar c.conjugate (un método de
complejo), da como resultado AttributeError: se lee el atributo de objeto 'complex' 'conjugate' -solamente.
El hecho de que los métodos no especiales puedan anularse tan fácilmente en instancias puede
sonar frágil y propenso a errores, pero personalmente nunca me ha molestado esto en más de 15
años de codificación de Python. Por otro lado, si está creando muchos atributos dinámicos, donde
los nombres de los atributos provienen de datos que no controla (como hicimos en las partes
anteriores de este capítulo), entonces debe tener esto en cuenta y quizás implemente algún
filtrado o escape de los nombres de atributos dinámicos para preservar su cordura.
Para cerrar este capítulo, cubriremos dos características que vimos con propiedades que no
hemos abordado en el contexto de los descriptores: documentación y manejo de intentos de
eliminar un atributo administrado.
La cadena de documentación de una clase de descriptor se utiliza para documentar cada instancia del
descriptor en la clase administrada. Consulte la Figura 20-6 para ver las pantallas de ayuda para la
clase LineItem con los descriptores Cantidad y No en blanco de los Ejemplos 20-6 y 20-7.
Figura 20-6. Capturas de pantalla de la consola de Python al ejecutar los comandos help(LineItem.weight)
y help(LineItem)
Eso es algo insatisfactorio. En el caso de LineItem, sería bueno agregar, por ejemplo, la información de
que el peso debe estar en kilogramos. Eso sería trivial con las propiedades, porque cada propiedad
maneja un atributo administrado específico. Pero con los descriptores, se utiliza la misma clase de
6
descriptor de cantidad para el peso y el precio.
El segundo detalle que discutimos con las propiedades pero que no hemos abordado con los
descriptores es el manejo de los intentos de eliminar un atributo administrado. Eso se puede hacer
implementando un método __delete__ junto o en lugar de los habituales __get__ y/o __set__ en el
6. Personalizar el texto de ayuda para cada instancia del descriptor es sorprendentemente difícil. Una solución requiere la construcción
dinámica de una clase contenedora para cada instancia de descriptor.
clase de descriptor. La codificación de una clase de descriptor tonta con __delete__ se deja como un ejercicio para el
lector pausado.
En “LineItem Take #4: Automatic Storage Attribute Names” en la página 631, eliminamos el requisito de que los
descriptores de cantidad se declararan con un stor age_name explícito, que era redundante y propenso a errores,
porque ese nombre siempre debe coincidir con el nombre del atributo en el a la izquierda de la asignación en la
instanciación del descriptor.
La solución fue generar nombres de almacenamiento únicos combinando el nombre de clase del descriptor con un
contador en el nivel de clase (por ejemplo, '_Cantidad #1').
A continuación, comparamos el tamaño del código, las fortalezas y las debilidades de una clase de descriptor con una
fábrica de propiedades basada en modismos de programación funcional. Este último funciona perfectamente bien y es
más simple en algunos aspectos, pero el primero es más flexible y es la solución estándar. Una ventaja clave de la
clase de descriptor se aprovechó en “LineItem Take #5: Un nuevo tipo de descriptor” en la página 637: creación de
subclases para compartir código mientras se crean descriptores especializados con alguna funcionalidad común.
Luego observamos el comportamiento diferente de los descriptores que proporcionan u omiten el método __set__ ,
haciendo la distinción crucial entre descriptores anulados y no anulados. A través de pruebas detalladas, descubrimos
cuándo los descriptores están bajo control y cuándo se sombrean, se omiten o se sobrescriben.
Para concluir el capítulo, “Sugerencias para el uso de descriptores” en la página 648 proporcionó una breve descripción
de cómo funcionan la documentación y la eliminación de descriptores.
A lo largo de este capítulo, nos enfrentamos a algunos problemas que solo la metaprogramación de clases puede
resolver, y los postergamos para el Capítulo 21.
Otras lecturas
Además de la referencia obligatoria al capítulo "Modelo de datos", la Guía práctica de descriptores de Raymond
Hettinger es un recurso valioso, parte de la colección de procedimientos en la documentación oficial de Python.
Como es habitual con los temas del modelo de objetos de Python, Python in a Nutshell, 2E
(O'Reilly) de Alex Martelli tiene autoridad y es objetivo, aunque algo anticuado: los mecanismos
clave discutidos en este capítulo se introdujeron en Python 2.2, mucho antes de que se cubriera la
versión 2.5. por ese libro. Martelli también tiene una presentación titulada Modelo de objetos de
Python, que cubre propiedades y descriptores en profundidad (diapositivas, video). Muy recomendable.
Para la cobertura de Python 3 con ejemplos prácticos, Python Cookbook, 3E de David Beazley y
Brian K. Jones (O'Reilly), tiene muchas recetas que ilustran los descriptores, de los cuales quiero
destacar “6.12. Lectura de estructuras binarias anidadas y de tamaño variable”, “8.10. Uso de
propiedades calculadas de forma diferida”, “8.13. Implementación de un modelo de datos o sistema
de tipos”, y “9.9. Definición de decoradores como clases”, el último de los cuales aborda problemas
profundos con la interacción de los decoradores, descriptores y métodos de funciones, explicando
cómo un decorador de funciones implementado como una clase con __call__ también necesita
implementar __get__ si quiere trabajar con métodos de decoración como así como funciones.
Plataforma improvisada
“Peor es mejor” es una filosofía de diseño descrita por Richard P. Gabriel en “The Rise of Worse is
Better”. La primera prioridad de esta filosofía es la “Simplicidad”, que Gabriel
estados como:
El diseño debe ser simple, tanto en implementación como en interfaz. Es más importante que la
implementación sea simple que la interfaz. La simplicidad es la consideración más importante en un
diseño.
Creo que el requisito de declararse uno mismo explícitamente como primer argumento en los métodos
es una aplicación de "Peor es mejor" en Python. La implementación es simple, incluso elegante, a
expensas de la interfaz de usuario: una firma de método como def zfill(self, width): no coincide
visualmente con la invocación pobox.zfill(8).
Modula-3 introdujo esa convención, y el uso del autoidentificador , pero hay una diferencia: en
Modula-3, las interfaces se declaran por separado de su implementación, y en la declaración de la
interfaz se omite el argumento self , por lo que del usuario por En perspectiva, un método aparece en
una declaración de interfaz exactamente con el mismo número de argumentos explícitos que necesita.
Una mejora a este respecto han sido los mensajes de error: para un método definido por el usuario
con un argumento además de sí mismo, si el usuario invoca obj.meth(), Python 2.7 genera TypeError:
meth() toma exactamente 2 argumentos (1 dado), pero en Python 3.4 el mensaje es menos confuso,
eludiendo el tema del conteo de argumentos y nombrando el argumento faltante: meth() falta 1
argumento posicional requerido: 'x'.
Además del uso de self como argumento explícito, también se critica el requisito de calificar todos
los atributos de acceso a la instancia con self.7 Personalmente, no me importa escribir el calificador
self : es bueno distinguir las variables locales de los atributos. Mi problema es con el uso de self en
la declaración de definición . Pero me acostumbré.
Cualquiera que no esté contento con el yo explícito en Python puede sentirse mucho mejor si
considera la desconcertante semántica del esto implícito en JavaScript. Guido tenía algunas buenas
razones para hacer que funcionara solo como lo hace, y escribió sobre ellas en "Agregar soporte
para clases definidas por el usuario", una publicación en su blog, The History of Python.
7. Véase, por ejemplo, la famosa publicación Python Warts de AM Kuchling (archivada); El propio Kuchling no está tan molesto
por el autocalificador, pero lo menciona, probablemente haciéndose eco de las opiniones de comp.lang.python.
CAPÍTULO 21
Metaprogramación de clases
[Las metaclases] son una magia más profunda de lo que debería preocupar al 99 % de los
usuarios. Si te preguntas si los necesitas, no es así (las personas que realmente los necesitan
saben con certeza que los necesitan y no necesitan una explicación de por qué).1
—Tim Peters
Inventor del algoritmo timsort y colaborador prolífico de Python
La metaprogramación de clases es el arte de crear o personalizar clases en tiempo de ejecución. Las clases
son objetos de primera clase en Python, por lo que se puede usar una función para crear una nueva clase
en cualquier momento, sin usar la palabra clave class . Los decoradores de clase también son funciones,
pero capaces de inspeccionar, cambiar e incluso reemplazar la clase decorada con otra clase. Finalmente,
las metaclases son la herramienta más avanzada para la metaprogramación de clases: te permiten crear
categorías completamente nuevas de clases con características especiales, como las clases base abstractas
que ya hemos visto.
Las metaclases son poderosas, pero difíciles de acertar. Los decoradores de clase resuelven muchos de los
mismos problemas de forma más sencilla. De hecho, las metaclases ahora son tan difíciles de justificar en
código real que mi ejemplo motivador favorito perdió gran parte de su atractivo con la introducción de los
decoradores de clases en Python 2.6.
También se cubre aquí la distinción entre el tiempo de importación y el tiempo de ejecución: un requisito
previo crucial para la metaprogramación efectiva de Python.
1. Mensaje a comp.lang.python, asunto: “Acrimony in clp”. Esta es otra parte del mismo mensaje del 23
de diciembre de 2002, citado en el Prefacio. El TimBot se inspiró ese día.
655
Machine Translated by Google
La biblioteca estándar tiene una fábrica de clases que hemos visto varias veces en este libro:
collections.namedtuple. Es una función que, dado un nombre de clase y nombres de atributos, crea una
subclase de tupla que permite recuperar elementos por nombre y proporciona un buen __repr__ para la
depuración.
A veces he sentido la necesidad de una fábrica similar para objetos mutables. Supongamos que estoy
escribiendo una aplicación para una tienda de mascotas y quiero procesar datos para perros como registros
simples. Es malo tener que escribir repetitivo como este:
clase Perro:
def __init__(self, nombre, peso, dueño): self.name
= nombre
self.peso = peso
self.propietario = propietario
Aburrido... los nombres de los campos aparecen tres veces cada uno. Todo ese repetitivo ni siquiera nos
compra una buena repr:
Tomando una pista de collections.namedtuple, creemos una record_factory que crea clases simples como Dog
sobre la marcha. El ejemplo 21-1 muestra cómo debería funcionar.
Buena repr.
La clase recién creada hereda del objeto, sin relación con nuestra fábrica.
2
El código para record_factory está en el ejemplo 21-2.
def __repr__(self):
valores = ', '.join('{}={!r}'.format(*i) for i in zip(self.__slots__,
self)) return '{}
({})' .format(self.__class__.__name__, valores)
Práctica de tipeo de pato: intente dividir los nombres de los campos por comas o espacios; si
eso falla, suponga que ya es iterable, con un nombre por elemento.
Cree una tupla de nombres de atributos, este será el atributo __slots__ de la nueva clase;
esto también establece el orden de los campos para desempaquetar y __repr__.
Implemente un __iter__, por lo que las instancias de clase serán iterables; producir los valores
de campo en el orden dado por __slots__.
Por lo general, pensamos en el tipo como una función, porque lo usamos como tal, por ejemplo, tipo
(mi_objeto) para obtener la clase del objeto, igual que mi_objeto.__clase__. Sin embargo, el tipo es
una clase. Se comporta como una clase que crea una nueva clase cuando se invoca con tres argumentos:
Los tres argumentos de tipo son named name, bases y dict; este último es un mapeo de nombres de
atributos y atributos para la nueva clase. El código anterior es funcionalmente equivalente a esto:
def x2(self):
return self.x * 2
La novedad aquí es que las instancias de tipo son clases, como MiClase aquí, o la clase Perro en el
Ejemplo 21-1.
En resumen, la última línea de record_factory en el ejemplo 21-2 crea una clase nombrada por el valor
de cls_name, con object como su única superclase inmediata y con atributos de clase denominados
__slots__, __init__, __iter__ y __repr__, de los cuales los últimos tres son métodos de instancia.
Podríamos haber nombrado el atributo de clase __slots__ de otra forma, pero luego tendríamos que
implementar __setattr__ para validar los nombres de los atributos que se asignan, porque para
nuestras clases similares a registros queremos que el conjunto de atributos sea siempre el mismo y
en el la misma orden. Sin embargo, recuerde que la función principal de __slots__ es ahorrar memoria
cuando se trata de millones de instancias, y el uso de __slots__ tiene algunos inconvenientes, que se
analizan en “Ahorro de espacio con el atributo de clase __slots__” en la página 264.
Invocar tipo con tres argumentos es una forma común de crear una clase dinámicamente.
Si observa el código fuente de collections.namedtuple, verá un enfoque diferente: hay _class_template,
una plantilla de código fuente como una cadena, y la función namedtuple llena sus espacios en blanco
llamando a _class_template.format(…). La cadena de código fuente resultante se evalúa luego con la
función integrada exec .
Las instancias de clases creadas por record_factory tienen una limitación: no son serializables, es
decir, no se pueden usar con las funciones de volcado/carga del módulo pickle .
Resolver este problema está más allá del alcance de este ejemplo, cuyo objetivo es mostrar la clase
de tipo en acción en un caso de uso simple. Para obtener la solución completa, estudie el código
fuente de collections.nameduple; busque la palabra "decapado".
>>> LineItem.weight.storage_name
'_Quantity#0'
Sin embargo, sería mejor si los nombres de almacenamiento incluyeran el nombre del atributo
administrado, así:
>>> LineItem.weight.storage_name
'_Quantity#weight'
Recuerde de “LineItem Take #4: Automatic Storage Attribute Names” en la página 631 que no
podíamos usar nombres descriptivos de almacenamiento porque cuando se crea una instancia del
descriptor, no tiene forma de saber el nombre del atributo administrado (es decir, el atributo de clase
al que pertenece). el descriptor estará ligado, como el peso en los ejemplos anteriores). Pero una vez
que se ensambla toda la clase y los descriptores están vinculados a los atributos de la clase, podemos
inspeccionar la clase y establecer los nombres de almacenamiento adecuados para los descriptores.
Esto podría hacerse en el método __new__ de la clase LineItem , de modo que cuando se utilicen los
descriptores en el método __init__ , se establezcan los nombres de almacenamiento correctos. El
problema de usar __new__ para ese propósito es un esfuerzo desperdiciado: la lógica de __new__
se ejecutará cada vez que se cree una nueva instancia de LineItem , pero el enlace del descriptor al
atributo administrado nunca cambiará una vez que se construya la clase LineItem . Así que tenemos que configurar el
nombres de almacenamiento cuando se crea la clase. Eso se puede hacer con un decorador de clase o una
metaclase. Lo haremos primero de la manera más fácil.
Un decorador de clases es muy similar a un decorador de funciones: es una función que obtiene un objeto de
clase y devuelve la misma clase o una modificada.
En el Ejemplo 21-3, el intérprete evaluará la clase LineItem y el objeto de clase resultante se pasará a la
función model.entity . Python vinculará el nombre global LineItem a lo que devuelva la función model.entity .
En este ejemplo, model.entity devuelve la misma clase LineItem con el atributo storage_name de cada
instancia de descriptor cambiado.
@model.entity
class LineItem:
descripción = modelo.NonBlank()
peso = modelo.Cantidad() precio =
modelo.Cantidad()
def subtotal(self):
return self.weight * self.price
El ejemplo 21-4 muestra la implementación del decorador. Aquí solo se incluye el nuevo código en la parte
inferior de model_v6.py ; el resto del módulo es idéntico a model_v5.py (Ejemplo 20-6).
def entidad(cls):
for key, attr in cls.__dict__.items(): if
isinstance(attr, Validated):
type_name = type(attr).__name__
attr.storage_name = '_{}#{}'.format(type_name, key)
volver cls
Las pruebas documentales en bulkfood_v6.py prueban que los cambios son exitosos. Por ejemplo, el Ejemplo
21-5 muestra los nombres de los atributos de almacenamiento en una instancia de LineItem .
Ejemplo 21-5. bulkfood_v6.py: pruebas de documentos para los nuevos atributos del descriptor de nombre_de_almacenamiento
'Pasas doradas'
Eso no es demasiado complicado. Los decoradores de clase son una forma más sencilla de hacer algo que
antes requería una metaclase: personalizar una clase en el momento en que se crea.
Un inconveniente importante de los decoradores de clase es que actúan solo en la clase donde se aplican
directamente. Esto significa que las subclases de la clase decorada pueden o no heredar los cambios
realizados por el decorador, dependiendo de cuáles sean esos cambios. Exploraremos el problema y veremos
cómo se resuelve en las siguientes secciones.
Para una metaprogramación exitosa, debe saber cuándo el intérprete de Python evalúa cada bloque de
código. Los programadores de Python hablan de "tiempo de importación" versus "tiempo de ejecución", pero
los términos no están estrictamente definidos y hay un área gris entre ellos.
En el momento de la importación, el intérprete analiza el código fuente de un módulo .py en una sola pasada
de arriba a abajo y genera el código de bytes que se ejecutará. Ahí es cuando pueden ocurrir errores de
sintaxis. Si hay un archivo .pyc actualizado disponible en el __pycache__ local, esos pasos se omiten porque
el código de bytes está listo para ejecutarse.
Aunque la compilación es definitivamente una actividad de tiempo de importación, otras cosas pueden
suceder en ese momento, porque casi todas las declaraciones en Python son ejecutables en el sentido de
que potencialmente ejecutan el código de usuario y cambian el estado del programa de usuario. En particular,
la declaración de importación no es simplemente una declaración3 , sino que en realidad ejecuta todo el
código de nivel superior del módulo importado cuando se importa por primera vez en el proceso; las
importaciones posteriores del mismo módulo usarán un caché y solo el nombre Entonces se produce la unión. Que
3. Contraste con la declaración de importación en Java, que es solo una declaración para que el compilador sepa que ciertas
se requieren paquetes.
el código de nivel superior puede hacer cualquier cosa, incluidas las acciones típicas del "tiempo de ejecución", como
conectarse a una base de datos.4 Es por eso que el límite entre "tiempo de importación" y "tiempo de ejecución" es borroso:
En el párrafo anterior, escribí que la importación "ejecuta todo el código de nivel superior", pero el "código
de nivel superior" requiere cierta elaboración. El intérprete ejecuta una declaración de definición en el nivel
superior de un módulo cuando se importa el módulo, pero ¿qué se logra con eso? El intérprete compila el
cuerpo de la función (si es la primera vez que se importa ese módulo) y vincula el objeto de la función a su
nombre global, pero obviamente no ejecuta el cuerpo de la función. En el caso habitual, esto significa que
el intérprete define funciones de nivel superior en el momento de la importación, pero ejecuta sus cuerpos
solo cuando, y si, las funciones se invocan en tiempo de ejecución.
Para las clases, la historia es diferente: en el momento de la importación, el intérprete ejecuta el cuerpo de
cada clase, incluso el cuerpo de las clases anidadas en otras clases. La ejecución del cuerpo de una clase
significa que se definen los atributos y métodos de la clase y luego se construye el objeto de la clase. En
este sentido, el cuerpo de las clases es “código de nivel superior”: se ejecuta en el momento de la importación.
Todo esto es bastante sutil y abstracto, así que aquí hay un ejercicio para ayudarlo a ver qué sucede
cuando.
Considere un script, evaltime.py, que importa un módulo evalsupport.py. Ambos módulos tienen varias
llamadas de impresión a marcadores de salida en el formato <[N]>, donde N es un número.
El objetivo de este par de ejercicios es determinar cuándo se realizará cada una de estas llamadas.
Los estudiantes han informado que estos ejercicios son útiles para
apreciar mejor cómo Python evalúa el código fuente. Tómese el
tiempo para resolverlos con papel y lápiz antes de ver la “Solución
para el escenario n.º 1” en la página 664.
Los listados son los ejemplos 21-6 y 21-7. Tome papel y lápiz y, sin ejecutar el código, escriba los
marcadores en el orden en que aparecerán en la salida, en dos escenarios:
Escenario 1
4. No estoy diciendo que iniciar una conexión de base de datos solo porque se importe un módulo sea una buena idea, solo señalo
fuera se puede hacer.
Escenario #2
El módulo evaltime.py se ejecuta desde el shell de comandos:
$ python3 evaltime.py
Ejemplo 21-6. evaltime.py: escriba los marcadores numerados <[N]> en el orden en que
aparecerán en la salida
de evalsupport importar deco_alpha
clase ClassOne():
print('<[2]> cuerpo ClassOne')
def __init__(self):
print('<[3]> ClassOne.__init__')
def __del__(self):
print('<[4]> ClassOne.__del__')
def method_x(self):
print('<[5]> ClassOne.method_x')
class ClassTwo(objeto):
print('<[6]> ClassTwo body')
@deco_alpha
clase ClassThree():
print('<[7]> cuerpo ClassThree')
def método_y(self):
print('<[8]> ClassThree.method_y')
def método_y(self):
print('<[10]> ClassFour.method_y')
si __nombre__ == '__principal__':
print('<[11]> Pruebas ClassOne', 30 * '.') uno =
ClassOne() one.method_x() print('<[12]> Pruebas
ClassThree', 30 * '.') tres = ClassThree( )
tres.method_y() print('<[13]> ClassFour tests', 30 * '.')
four = ClassFour()
cuatro.método_y()
def deco_alpha(cls):
print('<[200]> deco_alpha')
def interior_1(self):
print('<[300]> deco_alpha:inner_1')
# BEGIN META_ALEPH
clase MetaAleph(tipo):
print('<[400]> cuerpo MetaAleph')
def interior_2(self):
print('<[600]> MetaAleph.__init__:interior_2')
cls.method_z = interior_2 #
FIN META_ALEPH
Todo el código de nivel superior en evalsupport se ejecuta cuando se importa el módulo; la función
deco_alpha se compila, pero su cuerpo no se ejecuta.
En este escenario, se importa evaltime , por lo que el bloque if __name__ == '__main__': nunca
se ejecuta.
2. El intérprete ejecuta cada cuerpo de clase del módulo importado y su dependencia, evalsupport.
3. Tiene sentido que el intérprete evalúe el cuerpo de una clase decorada antes de invocar la función de
decorador que se adjunta encima: el decorador debe obtener un objeto de clase para procesar, por
lo que el objeto de clase debe construirse primero.
4. La única función o método definido por el usuario que se ejecuta en este escenario es deco_al
decorador pha .
...............................
ClassThree.method_y fue cambiado por el decorador deco_alpha , por lo que la llamada three.method_y()
ejecuta el cuerpo de la función inner_1 .
La instancia de ClassOne vinculada a una variable global se recolecta como basura solo cuando finaliza el
programa.
El punto principal del escenario #2 es mostrar que los efectos de un decorador de clase pueden no afectar a las
subclases. En el ejemplo 21-6, ClassFour se define como una subclase de ClassThree.
El decorador @deco_alpha se aplica a ClassThree, reemplazando su method_y, pero eso no afecta a ClassFour en
absoluto. Por supuesto, si ClassFour.method_y invocara ClassThree.method_y con super(…), veríamos el efecto del
decorador, ya que se ejecuta la función inner_1 .
Por el contrario, la siguiente sección mostrará que las metaclases son más efectivas cuando queremos personalizar
Metaclases 101
Una metaclase es una fábrica de clases, excepto que en lugar de una función, como record_factory del Ejemplo 21-2,
una metaclase se escribe como una clase. La figura 21-1 muestra una metaclase que utiliza la notación Mills &
Gizmos: una fábrica que produce otra fábrica.
Considere el modelo de objetos de Python: las clases son objetos, por lo tanto, cada clase debe ser una instancia
de alguna otra clase. De forma predeterminada, las clases de Python son instancias de tipo. En otras palabras,
el tipo es la metaclase para la mayoría de las clases integradas y definidas por el usuario:
Para evitar la regresión infinita, el tipo es una instancia de sí mismo, como muestra la última línea.
Tenga en cuenta que no estoy diciendo que str o LineItem hereden de type. Lo que digo es que str y LineItem
son instancias de tipo. Todos ellos son subclases de objeto.
La figura 21-2 puede ayudarlo a enfrentar esta extraña realidad.
Figura 21-2. Ambos diagramas son verdaderos. El de la izquierda enfatiza que str, type y LineItem
son subclases de object. El de la derecha deja en claro que str, object y LineItem son instancias de
tipo, porque todas son clases.
Las clases objeto y tipo tienen una relación única: objeto es una instancia de tipo y
tipo es una subclase de objeto. Esta relación es "mágica": no se puede expresar
en Python porque cualquiera de las clases tendría que existir antes de que la otra
pudiera ser
definido. El hecho de que el tipo sea una instancia de sí mismo también es mágico.
Además del tipo, existen algunas otras metaclases en la biblioteca estándar, como ABCMeta y
Enum. El siguiente fragmento muestra que la clase de collections.Iterable es abc.ABCMeta.
La clase Iterable es abstracta, pero ABCMeta no lo es; después de todo, Iterable es una instancia de
ABC Meta:
En última instancia, la clase de ABCMeta también es tipo. Cada clase es una instancia de tipo,
directa o indirectamente, pero solo las metaclases son también subclases de tipo. Esa es la relación
más importante para entender las metaclases: una metaclase, como ABCMeta, hereda del tipo el
poder de construir clases. La figura 21-3 ilustra esta relación crucial.
Figura 21-3. Iterable es una subclase de objeto y una instancia de ABCMeta. Tanto object como
ABCMeta son instancias de tipo, pero la relación clave aquí es que ABCMeta también es una subclase
de tipo, porque ABCMeta es una metaclase. En este diagrama, Iterable es la única clase abstracta.
La conclusión importante aquí es que todas las clases son instancias de tipo, pero las metaclases
también son subclases de tipo, por lo que actúan como fábricas de clases. En particular, una metaclase
puede personalizar sus instancias implementando __init__. Un método __init__ de metaclase puede
hacer todo lo que puede hacer un decorador de clases, pero sus efectos son más profundos, como lo
demuestra el siguiente ejercicio.
@deco_alpha
clase ClassThree():
print('<[2]> cuerpo ClassThree')
def método_y(self):
print('<[3]> ClassThree.method_y')
def método_y(self):
print('<[5]> ClassFour.method_y')
class ClassFive(metaclass=MetaAleph):
print('<[6]> ClassFive body')
def __init__(self):
print('<[7]> ClassFive.__init__')
def method_z(self):
print('<[8]> ClassFive.method_y')
class ClassSix(ClassFive):
print('<[9]> ClassSix body')
def method_z(self):
print('<[10]> ClassSix.method_y')
if __name__ == '__main__':
print('<[11]> ClassThree tests', 30 * '.') tres =
ClassThree() three.method_y() print('<[12]>
ClassFour tests', 30 * '.') cuatro = ClassFour()
four.method_y() print('<[13]> ClassFive tests', 30 *
'.') cinco = ClassFive() five.method_z() print('<[14]>
Pruebas ClassSix', 30 * '.') seis = ClassSix()
seis.method_z()
Nuevamente, tome lápiz y papel y escriba los marcadores numerados <[N]> en el orden en que
aparecerán en la salida, considerando estos dos escenarios:
Escenario #3
El módulo evaltime_meta.py se importa de forma interactiva en la consola de Python.
Escenario #4
El módulo evaltime_meta.py se ejecuta desde el shell de comandos.
La diferencia clave con el escenario n.º 1 es que se invoca el método MetaAleph.__init__ para
inicializar el ClassFive recién creado.
El intérprete de Python evalúa el cuerpo de ClassFive pero luego, en lugar de llamar al tipo para construir el
cuerpo de la clase real, llama a MetaAleph. Mirando la definición de MetaAleph en el Ejemplo 21-12, verá
que el método __init__ obtiene cuatro argumentos:
uno mismo
clase MetaAleph(tipo):
print('<[400]> cuerpo MetaAleph')
def interior_2(self):
print('<[600]> MetaAleph.__init__:interior_2')
cls.método_z = interior_2
El cuerpo de __init__ define una función inner_2 y luego la vincula a cls.method_z. El nombre cls
en la firma de MetaAleph.__init__ se refiere a la clase que se está creando (por ejemplo,
ClassFive). Por otro lado, el nombre self en la firma de inner_2 eventualmente se referirá a una
instancia de la clase que estamos creando (por ejemplo, una instancia de ClassFive).
El ejemplo 21-13 muestra el resultado de ejecutar python evaltime.py desde la línea de comandos.
$ python3 evaltime.py
<[100]> inicio del módulo evalsupport
<[400]> cuerpo MetaAleph <[700]> final
del módulo evalsupport <[1]> inicio del
módulo evaltime_meta <[2]> cuerpo
ClassThree <[200]> deco_alpha < [4]>
ClassFour body <[6]> ClassFive body
<[500]> MetaAleph.__init__ <[9]>
ClassSix body <[500]> MetaAleph.__init__
<[11]> ClassThree tests <[300]>
deco_alpha: inner_1 <[12]> Pruebas
ClassFour <[5]> ClassFour.method_y
<[13]> Pruebas ClassFive <[7]> ...............................
ClassFive.__init__ <[600]>
...............................
MetaAleph.__init__:inner_2 <[14]>
Pruebas ClassSix <[ 7]> ClassFive.__init__
...............................
<[600]> MetaAleph.__init__:inner_2
<[15]> fin del módulo evaltime_meta
...............................
Lo mismo sucede con la subclase ClassFive, ClassSix : su method_z se reemplaza por inner_2.
Tenga en cuenta que ClassSix no hace referencia directa a MetaAleph, pero se ve afectado porque es una
subclase de ClassFive y, por lo tanto, también es una instancia de MetaAleph, por lo que MetaAleph.__init__
lo inicializa.
Ahora podemos poner en práctica toda esta teoría creando una metaclase para proporcionar una solución
definitiva a los descriptores con nombres de atributos de almacenamiento automático.
Ejemplo 21-14. bulkfood_v7.py: heredando de model.Entity puede funcionar, si una metaclase está detrás
de escena
clase LineItem(modelo.Entidad):
descripción = modelo.NonBlank()
peso = modelo.Cantidad() precio =
modelo.Cantidad()
def subtotal(self):
return self.weight * self.price
El ejemplo 21-14 parece bastante inofensivo. No se ve ninguna sintaxis extraña. Sin embargo, solo
funciona porque model_v7.py define una metaclase y model.Entity es una instancia de esa metaclase. El
ejemplo 21-15 muestra la implementación de la clase Entity en el módulo model_v7.py .
clase EntityMeta(tipo):
"""Metaclase para entidades de negocio con campos validados"""
clase Entidad(metaclase=EntidadMeta):
"""Entidad comercial con campos validados"""
Esta clase existe solo por conveniencia: el usuario de este módulo puede crear una subclase de
Entity y no preocuparse por EntityMeta, o incluso ser consciente de su existencia.
El código del ejemplo 21-14 pasa las pruebas del ejemplo 21-3. El módulo de soporte, model_v7.py, es
más difícil de entender que model_v6.py, pero el código de nivel de usuario es más simple: simplemente
herede de model_v7.entity y obtendrá nombres de almacenamiento personalizados para sus campos
validados .
La figura 21-4 es una descripción simplificada de lo que acabamos de implementar. Están sucediendo
muchas cosas, pero la complejidad está oculta dentro del módulo model_v7 . Desde la perspectiva del
usuario, LineItem es simplemente una subclase de Entity, como se codifica en el Ejemplo 21-14. Este es
el poder de la abstracción.
Figura 21-4. Diagrama de clase UML anotado con MGN (Mills & Gizmos Notation): el meta-mill
EntityMeta construye el molino LineItem. La configuración de los descriptores (por ejemplo, peso y
precio) la realiza EntityMeta.__init__. Tenga en cuenta el límite del paquete de model_v7.
Excepto por la sintaxis para vincular una clase a la metaclase,5 todo lo escrito hasta ahora sobre las
metaclases se aplica a las versiones de Python desde la 2.2, cuando los tipos de Python se sometieron
a una revisión importante. La siguiente sección cubre una función que solo está disponible en Python 3.
Como hemos visto, tanto el constructor de tipos como los métodos __nuevo__ e __init__ de las
metaclases reciben el cuerpo de la clase evaluado como una asignación de nombres a atributos.
Sin embargo, por defecto, ese mapeo es un dict, lo que significa que el orden de los atributos tal como
aparecen en el cuerpo de la clase se pierde cuando nuestra metaclase o decorador de clase puede
verlos.
La solución a este problema es el método especial __prepare__ , introducido en Python 3. Este método
especial es relevante solo en metaclases, y debe ser un método de clase (es decir, definido con el
decorador @classmethod ). Se invoca el método __prepare__
5. Recuerde de “Detalles de sintaxis ABC” en la página 328 que en Python 2.7 se usa el atributo de clase __metaclass__,
y el argumento de palabra clave metaclass= no se admite en la declaración de clase.
por el intérprete antes del método __new__ en la metaclase para crear el mapeo que se llenará con los
atributos del cuerpo de la clase. Además de la metaclase como primer argumento, __prepare__ obtiene
el nombre de la clase a construir y su tupla de clases base, y debe devolver un mapeo, que será recibido
como último argumento por __new__ y luego __init__ cuando la metaclase construya una nueva clase.
Suena complicado en teoría, pero en la práctica, cada vez que he visto usar __prepare__ , fue muy
simple. Observe el Ejemplo 21-16.
Ejemplo 21-16. model_v8.py: la metaclase EntityMeta usa prepare, y Entity ahora tiene un método de
clase field_names
clase EntityMeta(tipo):
"""Metaclase para entidades de negocio con campos validados"""
@classmethod
def __prepare__(cls, nombre, bases):
devolver colecciones.OrderedDict()
type_name = type(attr).__name__
attr.storage_name = '_{}#{}'.format(type_name, key)
cls._field_names.append(key)
clase Entidad(metaclase=EntidadMeta):
"""Entidad comercial con campos validados"""
@classmethod
def field_names(cls): para
nombre en cls._field_names: nombre
de rendimiento
Devuelve una instancia de OrderedDict vacía , donde se almacenarán los atributos de la clase.
Esta línea no ha cambiado desde la versión anterior, pero attr_dict aquí es el OrderedDict
obtenido por el intérprete cuando llamó a __prepare__ antes de llamar a __init__. Por lo tanto,
este ciclo for repasará los atributos en el orden en que fueron agregados.
El método de la clase field_names simplemente genera los nombres de los campos en el orden
en que se agregaron.
Con las adiciones simples realizadas en el Ejemplo 21-16, ahora podemos iterar sobre los campos
Validados de cualquier subclase de Entidad utilizando el método de clase field_names .
El ejemplo 21-17 demuestra esta nueva característica.
Ejemplo 21-17. bulkfood_v8.py: doctest que muestra el uso de field_names; no se necesitan cambios
en la clase LineItem; field_names se hereda de model.Entity
Esto concluye nuestra cobertura de metaclases. En el mundo real, las metaclases se utilizan en
marcos y bibliotecas que ayudan a los programadores a realizar, entre otras tareas:
• Validación de atributos
lenguajes
Ahora tendremos una breve descripción general de los métodos definidos en el modelo de datos de Python para todas
las clases.
cls.__bases__ La
tupla de clases base de la clase.
cls.__qualname__ Un
nuevo atributo en Python 3.3 que contiene el nombre calificado de una clase o función, que es
una ruta punteada desde el alcance global del módulo hasta la definición de la clase.
Por ejemplo, en el Ejemplo 21-6, el __qualname__ de la clase interna ClassTwo es la cadena
'ClassOne.ClassTwo', mientras que su __name__ es solo 'ClassTwo'. La especificación para
este atributo es PEP-3155 — Nombre calificado para clases y funciones.
cls.__subclases__()
Este método devuelve una lista de las subclases inmediatas de la clase. La implementación utiliza
referencias débiles para evitar referencias circulares entre la superclase y sus subclases, que contienen
una fuerte referencia a las superclases en su atributo __bases__ . El método devuelve la lista de subclases
que existen actualmente en la memoria.
cls.mro()
El intérprete llama a este método cuando construye una clase para obtener la tupla de superclases que se
almacena en el atributo __mro__ de la clase. Una metaclase puede anular este método para personalizar
el orden de resolución del método de la clase bajo control.
construccion
Con esto finaliza nuestro estudio de la metaprogramación de clases. Este es un tema muy amplio y solo arañé la
superficie. Es por eso que tenemos secciones de “Lecturas adicionales” en este libro.
En la siguiente sección, volvimos a la clase LineItem con descriptores del Capítulo 20 para resolver un problema
pendiente: cómo generar nombres para los atributos de almacenamiento que reflejaban los nombres de los
atributos administrados (p. ej., _Cantidad#precio en lugar de _Cantidad). ciudad#1). La solución fue usar un
decorador de clases, esencialmente una función que obtiene una clase recién construida y tiene la oportunidad
de inspeccionarla, cambiarla e incluso reemplazarla con una clase diferente.
Luego pasamos a una discusión sobre cuándo se ejecutan realmente las diferentes partes del código fuente de
un módulo. Vimos que hay cierta superposición entre el llamado "tiempo de importación" y el "tiempo de
ejecución", pero claramente se ejecuta una gran cantidad de código desencadenado por la declaración de
importación . Comprender qué se ejecuta y cuándo es crucial, y existen algunas reglas sutiles, por lo que
utilizamos los ejercicios de tiempo de evaluación para cubrir este tema.
El siguiente tema fue una introducción a las metaclases. Vimos que todas las clases son instancias de tipo,
directa o indirectamente, por lo que esa es la "metaclase raíz" del lenguaje.
Se diseñó una variación del ejercicio del tiempo de evaluación para mostrar que una metaclase puede
personalizar una jerarquía de clases, en contraste con un decorador de clases, que afecta a una sola
clase y puede no tener impacto en sus descendientes.
La primera aplicación práctica de una metaclase fue resolver el problema de los nombres de los atributos
de almacenamiento en LineItem. El código resultante es un poco más complicado que la solución del
decorador de clases, pero se puede encapsular en un módulo para que el usuario simplemente
subclasifique una clase aparentemente simple (modelo.Entidad) sin ser consciente de que es una
instancia de una metaclase personalizada. (modelo.EntityMeta). El resultado final recuerda a las API de
ORM en Django y SQLAlchemy, que usan metaclases en sus implementaciones pero no requieren que
el usuario sepa nada sobre ellas.
La última sección fue una breve descripción de los atributos y métodos disponibles en todas las clases
de Python.
Las metaclases son desafiantes, emocionantes y, a veces, abusadas por programadores que intentan
ser demasiado inteligentes. Para concluir, recordemos el consejo final de Alex Martelli de su ensayo
“Aves acuáticas y ABC” en la página 314:
Otras lecturas
Las referencias esenciales para este capítulo en la documentación de Python son “3.3.3. Personalización
de la creación de clases” en el capítulo “Modelo de datos” de The Python Language Reference, la
documentación de la clase de tipo en la página “Funciones integradas” y “4.13. Atributos especiales” del
capítulo “Tipos incorporados” en la Referencia de la biblioteca. Además, en la Referencia de la biblioteca,
la documentación del módulo de tipos cubre dos funciones que son nuevas en
Los decoradores de clase se formalizaron en PEP 3129 - Decoradores de clase, escrito por Collin
Winter, con la implementación de referencia escrita por Jack Diederich. La charla de PyCon 2009
“Class Decorators: Radially Simple” (video), también de Jack Diederich, es una breve introducción
a la función.
Python en pocas palabras, 2E de Alex Martelli presenta una excelente cobertura de metaclases,
incluida una metaclase metaMetaBunch que tiene como objetivo resolver el mismo problema que
nuestra fábrica de registros simple del Ejemplo 21-2 , pero es mucho más sofisticada. Martelli no
se dirige a los decoradores de clase porque el artículo apareció más tarde que su libro. Beazley
y Jones brindan excelentes ejemplos de decoradores de clases y metaclases en su Python
Cookbook, 3E (O'Reilly). Michael Foord escribió una publicación intrigante titulada "Metaclases
simplificadas: eliminarse a sí mismo con metaclases". El subtítulo lo dice todo.
Para las metaclases, las referencias principales son PEP 3115 — Metaclases en Python 3000,
en el que se introdujo el método especial __prepare__ y Unificación de tipos y clases en Python
2.2, escrito por Guido van Rossum. El texto también se aplica a Python 3 y cubre lo que entonces
se denominaba semántica de clases de "nuevo estilo", incluidos los descriptores y las metaclases.
Es una lectura obligada. Una de las referencias citadas por Guido es Putting Metaclasses to
Work: a New Dimension in Object-Oriented Programming, de Ira R. Forman y Scott H. Danforth
(Addison-Wesley, 1998), libro al que otorgó 5 estrellas en Amazon.com, añadiendo la siguiente
reseña:
Para Python 3.5, en alfa mientras escribo esto, PEP 487: la personalización más simple de la
creación de clases presenta un nuevo método especial, __init_subclass__ que permitirá que una
clase normal (es decir, no una metaclase) personalice la inicialización de sus subclases. Al igual
que con los decoradores de clases, __init_subclass__ hará que la metaprogramación de clases
sea más accesible y también hará que sea mucho más difícil justificar el despliegue de la opción
nuclear —metaclases.
Si le gusta la metaprogramación, es posible que desee que Python tenga la última función de
metaprogramación: macros sintácticas, como las que ofrece Elixir y la familia de lenguajes Lisp.
Tener cuidado con lo que deseas. Solo diré una palabra: MacroPy.
6. Página del catálogo de Amazon.com para Putting Metaclasses to Work. Todavía puedes comprarlo usado. Lo compré y lo encontré
difícil de leer, pero probablemente volveré a leerlo más tarde.
Caja de
jabón Comenzaré la última caja de jabón del libro con una larga cita de Brian Harvey y Matthew Wright,
dos profesores de informática de la Universidad de California (Berkeley y Santa Bárbara). En su libro,
Simply Scheme, Harvey y Wright escribieron:
Hay dos escuelas de pensamiento sobre la enseñanza de las ciencias de la computación. Podríamos caricaturizar
las dos vistas de esta manera:
1. La visión conservadora: los programas de computadora se han vuelto demasiado grandes y complejos
para abarcarlos en una mente humana. Por lo tanto, el trabajo de la educación en informática es enseñar a
las personas cómo disciplinar su trabajo de tal manera que 500 programadores mediocres puedan unirse y
producir un programa que cumpla correctamente con sus especificaciones.
2. La visión radical: los programas de computadora se han vuelto demasiado grandes y complejos para
abarcar en una mente humana. Por lo tanto, el trabajo de la educación en ciencias de la computación es
enseñar a las personas cómo expandir sus mentes para que los programas puedan encajar, aprendiendo a
pensar en un vocabulario de ideas más grandes, poderosas y flexibles que las obvias. Cada unidad de
pensamiento de programación debe tener una gran recompensa en las capacidades del programa.7
exageradas de Harvey y Wright se refieren a la enseñanza de las ciencias de la computación, pero también se aplican al diseño
de lenguajes de programación. A estas alturas, debería haber adivinado que me suscribo a la vista "radical", y creo que Python
fue diseñado con ese espíritu.
La idea de la propiedad es un gran paso adelante en comparación con el enfoque de acceso desde el
principio prácticamente exigido por Java y respaldado por los IDE de Java que generan captadores/
establecedores con un atajo de teclado. La principal ventaja de las propiedades es que nos permite
iniciar nuestros programas simplemente exponiendo atributos como públicos, en el espíritu de KISS,
sabiendo que un atributo público puede convertirse en una propiedad en cualquier momento sin mucho
dolor. Pero la idea del descriptor va mucho más allá, proporcionando un marco para abstraer la lógica
de acceso repetitiva. Ese marco es tan efectivo que las construcciones esenciales de Python lo usan detrás del
escenas
Otra idea poderosa son las funciones como objetos de primera clase, allanando el camino hacia
funciones de orden superior. Resulta que la combinación de descriptores y funciones de orden superior
permite la unificación de funciones y métodos. El __get__ de una función produce un objeto de método
sobre la marcha vinculando la instancia al argumento self . Esto es elegante.8
7. Brian Harvey y Matthew Wright, Simply Scheme (MIT Press, 1999), pág. xvii. Texto completo disponible
en Berkeley.edu.
8. Machine Beauty de David Gelernter (Basic Books) es un intrigante libro corto sobre la elegancia y la estética en
obras de ingeniería, desde puentes hasta software.
Finalmente, tenemos la idea de las clases como objetos de primera clase. Es una hazaña sobresaliente
del diseño que un lenguaje amigable para principiantes proporcione abstracciones poderosas, como
decoradores de clase y metaclases completas definidas por el usuario. Lo mejor de todo: las
características avanzadas están integradas de una manera que no complica la idoneidad de Python para
la programación informal (en realidad, lo ayudan, en secreto). La comodidad y el éxito de marcos como
Django y SQLAlchemy se deben en gran medida a las metaclases, incluso si muchos usuarios de estas
herramientas no las conocen. Pero siempre pueden aprender y crear la próxima gran biblioteca.
Todavía no he encontrado un lenguaje que logre ser fácil para los principiantes, práctico para los
profesionales y emocionante para los piratas informáticos en la forma en que lo es Python. Gracias,
Guido van Rossum y todos los demás que lo hacen así.
Epílogo
—Alan Runyan
Cofundador de Plone
La concisa definición de Alan expresa una de las mejores cualidades de Python: se aparta del camino
y te permite hacer lo que debes. Esto también significa que no le brinda herramientas para restringir
lo que otros pueden hacer con su código y los objetos que construye.
Por supuesto, Python no es perfecto. Entre los principales irritantes para mí está el uso inconsistente
de CamelCase, snake_case y palabras unidas en la biblioteca estándar. Pero la definición del
lenguaje y la biblioteca estándar son solo parte de un ecosistema. La comunidad de usuarios y
colaboradores es la mejor parte del ecosistema de Python.
Aquí hay un ejemplo de la comunidad en su mejor momento: una mañana, mientras escribía sobre
asyncio , estaba frustrado porque la API tiene muchas funciones, docenas de las cuales son
corrutinas, y tienes que llamar a las corrutinas con rendimiento pero no puedes hacer eso. con
funciones regulares. Esto estaba documentado en las páginas de asyncio , pero a veces había que
leer algunos párrafos para saber si una función en particular era una corrutina.
Así que envié un mensaje a python-tulip titulado "Propuesta: hacer que las rutinas se destaquen en
los documentos de asyncio". Victor Stinner, desarrollador central de asyncio , Andrew Svetlov, autor
principal de aiohttp, Ben Darnell, desarrollador principal de Tornado y Glyph Lefkowitz, inventor de
Twisted, se unieron a la conversación. Darnell sugirió una solución, Alexander Shorin explicó cómo
implementarla en Sphinx y Stinner agregó la configuración y el marcado necesarios. Menos de 12
horas después de que planteé el problema, todo el conjunto de documentación de asyncio en línea
se actualizó con las etiquetas coroutine que puede ver hoy.
Esa historia no sucedió en un club exclusivo. Cualquiera puede unirse a la lista python-tulip, y solo
había publicado unas pocas veces cuando escribí la propuesta. La historia ilustra una comunidad
que está realmente abierta a nuevas ideas y nuevos miembros. Guido van Rossum pasa el rato en
python-tulip y se le puede ver regularmente respondiendo incluso preguntas simples.
683
Machine Translated by Google
Otro ejemplo de apertura: Python Software Foundation (PSF) ha estado trabajando para aumentar
la diversidad en la comunidad de Python. Ya se han obtenido algunos resultados alentadores. La
junta de PSF de 2013–2014 vio a las primeras mujeres directoras elegidas: Jessica McKellar y
Lynn Root. Y en PyCon North America 2015 en Montreal, presidida por Diana Clarke, alrededor
de 1/3 de los oradores fueron mujeres. No tengo conocimiento de ninguna otra conferencia de TI
importante que haya ido tan lejos en la búsqueda de la igualdad de género.
Si eres Pythonista pero no te has comprometido con la comunidad, te animo a que lo hagas.
Busque el grupo de usuarios de Python (PUG) en su área. Si no hay uno, créalo.
Python está en todas partes, así que no estarás solo. Viaja a eventos si puedes. Venga a una
conferencia de PythonBrasil: hemos tenido oradores internacionales regularmente durante muchos
años. Conocer a otros pitonistas en persona supera cualquier interacción en línea y se sabe que
brinda beneficios reales además de compartir todo el conocimiento. Como trabajos reales y
amistades reales.
Sé que no podría haber escrito este libro sin la ayuda de muchos amigos que hice a lo largo de los
años en la comunidad de Python.
Mi padre, Jairo Ramalho, solía decir “Só erra quem trabalha”, que en portugués significa “Solo
quien trabaja comete errores”, un gran consejo para no quedar paralizado por el miedo a cometer
errores. Ciertamente cometí mi parte de errores mientras escribía este libro. Los revisores, editores
y lectores de Early Release captaron muchos de ellos. A las pocas horas del primer lanzamiento
anticipado, un lector estaba informando errores tipográficos en la página de erratas del libro. Otros
lectores contribuyeron con más informes y amigos me contactaron directamente para ofrecerme
sugerencias y correcciones. Los correctores de estilo de O'Reilly detectarán otros errores durante
el proceso de producción, que comenzará tan pronto como logre dejar de escribir. Asumo la
responsabilidad y me disculpo por cualquier error y prosa subóptima que quede.
Estoy muy feliz de llevar este trabajo a la conclusión, errores y todo, y estoy muy agradecido a
todos los que ayudaron en el camino.
Espero verte pronto en algún evento en vivo. ¡Ven a saludarme si me ves por aquí!
Otras lecturas
Terminaré el libro con referencias sobre lo que es "Pythonic", la pregunta principal que este libro
trató de abordar.
Brandon Rhodes es un increíble profesor de Python, y su charla "A Python Æsthetic: Beauty and
Why I Python" es hermosa, comenzando con el uso de Unicode U+00C6 (LATIN CAP ITAL
LETTER AE) en el título. Otro maestro increíble, Raymond Hettinger, habló sobre la belleza en
Python en PyCon US 2013: "Transformar el código en Python hermoso e idiomático".
Vale la pena leer el hilo Evolution of Style Guides que Ian Lee inició en Python-ideas. Lee es el
mantenedor del paquete pep8 que verifica el código fuente de Python para
684 | Epílogo
Machine Translated by Google
Cumplimiento PEP 8. Para comprobar el código de este libro, utilicé flake8, que incluye pep8 ,
pyflakes y el complemento de complejidad McCabe de Ned Batchelder .
Además de PEP 8, otras guías de estilo influyentes son la Guía de estilo de Google Python y la
guía de estilo de Pocoo, del equipo que nos trae Flake, Sphinx, Jinja 2 y otras excelentes
bibliotecas de Python.
¡La guía del autoestopista de Python! es un trabajo colectivo sobre la escritura de código Pythonic.
Su colaborador más prolífico es Kenneth Reitz, un héroe de la comunidad gracias a su hermoso
paquete de solicitudes Pythonic. David Goodger presentó un tutorial en PyCon US 2008 titulado
“Code Like a Pythonista: Idiomatic Python”. Si están impresas, las notas del tutorial tienen 30
páginas. Por supuesto, la fuente reStructuredText está disponible y docutils puede representarla
en diapositivas HTML y S5 . Después de todo, Goodger creó tanto reStructuredText como
docutils, los cimientos de Sphinx, el excelente sistema de documentación de Python (que, por
cierto, también es el sistema de documentación oficial de MongoDB y muchos otros proyectos).
Martijn Faassen aborda la pregunta de frente en "¿Qué es Pythonic?" En la lista de python, hay
un hilo con ese mismo título. La publicación de Martijn es de 2005 y el hilo de 2003, pero el ideal
de Pythonic no ha cambiado mucho, ni tampoco el idioma. Un gran hilo con "Pythonic" en el título
es "¿Pythonic way to sum n-th list element?", del cual cité extensamente en "Soapbox" en la
página 302.
PEP 3099 — Cosas que no cambiarán en Python 3000 explica por qué muchas cosas son como
son, incluso después de la gran revisión que fue Python 3. Durante mucho tiempo, Python 3
recibió el apodo de Python 3000, pero llegó unos siglos antes. —para consternación de algunos.
PEP 3099 fue escrito por Georg Brandl, recopilando muchas opiniones expresadas por la BDFL,
Guido van Rossum. La página de Ensayos de Python enumera varios textos del propio Guido.
Epílogo | 685
Machine Translated by Google
Machine Translated by Google
APÉNDICE A
Guiones de soporte
Aquí hay listas completas de algunos guiones que eran demasiado largos para caber en el texto principal.
También se incluyen los scripts utilizados para generar algunas de las tablas y accesorios de datos utilizados
en este libro.
Estos scripts también están disponibles en el repositorio de código de Fluent Python , junto con casi todos los
demás fragmentos de código que aparecen en el libro.
Mientras codificaba el Ejemplo A-1, encontré algo que realmente pone en perspectiva el rendimiento de dict .
Si el script se ejecuta en "modo detallado" (con la opción de línea de comando -v ), los tiempos que obtengo
son casi el doble de los de la Tabla 3-5. Pero tenga en cuenta que, en este script, "modo detallado" significa
solo cuatro llamadas para imprimir mientras configura la prueba, y una impresión adicional para mostrar la
cantidad de agujas encontradas cuando finaliza cada prueba. No ocurre ninguna salida dentro del ciclo que
hace la búsqueda real de las agujas en el pajar, pero estas cinco llamadas de impresión toman tanto tiempo
como la búsqueda de 1000 agujas.
Ejemplo A-1. container_perftest.py: ejecútelo con el nombre de un tipo de colección integrado como argumento
de la línea de comandos (por ejemplo, container_perftest.py dict)
"""
Prueba de rendimiento del operador del contenedor ``in``
"""
tiempo de
CONFIGURACIÓN =
'''
687
Machine Translated by Google
importar matriz
seleccionada = matriz.array('d')
with open('selected.arr', 'rb') as fp:
selected.fromfile(fp, {size})
if {container_type} es dict: pajar =
dict.fromkeys(seleccionado, 1) else: pajar =
{container_type}(seleccionado) if {verbose}:
print(type(pajar), end=' ') print('pajar: % 10d'
% len(pajar), end=' ')
'''
'''
PRUEBA =
encontrado = 0 para
n en agujas: si n en
pajar: encontrado
+= 1 si {verbose}: print('
encontrado: %10d' % encontrado)
'''
if __name__=='__main__': if
'-v' in sys.argv:
sys.argv.remove('-v')
verbose = True else:
verbose = False if
len(sys.argv) != 2:
print('Usage: %s <container_type>' % sys.argv[0]) else:
test(sys.argv[1], verbose)
Ejemplo A-2. container_perftest_datagen.py: generar archivos con matrices de números únicos de punto
flotante para usar en el Ejemplo A-1
"""
importar matriz de
importación aleatoria
EXPONENTE_MAX = 7
HAYSTACK_LEN = 10 ** MAX_EXPONENTE
AGUJAS_LEN = 10 ** (MAX_EXPONENTE - 1)
SAMPLE_LEN = HAYSTACK_LEN + AGUJAS_LEN // 2
agujas = array.array('d')
Ejemplo A-3. hashdiff.py: muestra la diferencia de los patrones de bits de los valores hash
sistema de importación
si __nombre__ == '__principal__':
imprimir(hash_diff(1, 1.0))
imprimir(hash_diff(1.0, 1.0001))
imprimir(hash_diff(1.0001, 1.0002))
imprimir(hash_diff(1.0002, 1.0003))
El script memtest.py toma un nombre de módulo en la línea de comando y lo carga. Asumiendo que el
módulo define una clase llamada Vector, memtest.py crea una lista con 10 millones de instancias,
informando el uso de la memoria antes y después de crear la lista.
Ejemplo A-4. memtest.py: crea muchas instancias de Vector que informan el uso de la memoria
importar importlib
import sys recurso
de importación
NUM_VECTORES = 10**7
si len(sys.argv) == 2:
module_name = sys.argv[1].replace('.py', '') module =
importlib.import_module(module_name) else: print('Usage:
{} <vector-module-to-test>'.format() ) sys.exit(1)
mem_init = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
print('Creando {:,} instancias de Vector2d'.format(NUM_VECTORS))
Tenga en cuenta que este es un script de Python 2, diseñado para ejecutarse en CPython o Jython, versiones 2.5
a 2.7, pero no en Python 3. En CPython solo puede leer archivos .iso ; con Jython también puede leer archivos .mst ,
utilizando la biblioteca Bruma disponible en el repositorio fluentpython/ isis2json en GitHub. Consulte la
documentación de uso en ese repositorio.
importar
sistema importar
argparse desde uuid importar
uuid4 importar sistema operativo
intente: import
json excepto
ImportError: if os.name == 'java': # ejecutando
Jython desde com.xhaus.jyson import JysonCodec como
json else: importe simplejson como json
SKIP_INACTIVE = Verdadero
CANTIDAD_DEFAULT = 2**31
ISIS_MFN_KEY = 'mfn'
ISIS_ACTIVE_KEY = 'activo'
'^'
SUBFIELD_DELIMITER =
INPUT_ENCODING = 'cp1252'
if isis_json_type == 1:
field_occurrences.append(contenido)
elif isis_json_type == 2:
field_occurrences.append(expand(contenido)) elif
isis_json_type == 3:
field_occurrences.append(dict(expand(content))) else:
raise NotImplementedError('ISIS-JSON type %s conversion
'
'aún no implementado para la entrada .iso' % isis_json_type)
campos de
rendimiento iso.close()
subir SistemaSalir
mst = MasterFactory.getInstance(master_file_name).open() para
registro en mst:
campos = {}
if SKIP_INACTIVE:
if record.getStatus() != Record.Status.ACTIVE: continuar
else: # guardar estado solo hay registros inactivos
fields[ISIS_ACTIVE_KEY] = (record.getStatus() ==
Record.Status .ACTIVO)
campos[ISIS_MFN_KEY] = record.getMfn()
para el campo en record.getFields():
field_key = str(field.getId())
field_occurrences = fields.setdefault(field_key, []) if isis_json_type
== 3:
content = {}
for subfield in field.getSubfields(): subfield_key
= subfield.getId() if subfield_key == '*':
content['_'] = subfield.getContent() else:
subfield_occurrences =
content.setdefault(subfield_key, [])
subfield_occurrences.append(subfield.getContent())
field_occurrences.append(content) elif isis_json_type == 1: content = []
for subfield in field.getSubfields(): subfield_key = subfield.getId() if subfield_key
== ' *':
subcampo.getContent())
field_occurrences.append(''.join(contenido)) más:
'
aumentar NotImplementedError('ISIS-JSON tipo %s conversión
'aún no implementado para la entrada .mst' % isis_json_type) campos
de rendimiento mst.close()
''
id_tag =
para i, registrar en enumerar (input_gen): si i >=
final: romper si no mongo: si i == 0:
salida.escribir('[') elif
i > inicio: salida.escribir(',')
si inicio <= i < fin:
if id_tag:
ocurrencias = record.get(id_tag, None) if
ocurrencias es None:
msg = 'etiqueta de identificación #%s no encontrada en el
registro %s' si ISIS_MFN_KEY en el registro:
mensaje = mensaje + (' ( mfn =%s)' % registro[ISIS_MFN_KEY])
aumentar KeyError(mensaje % (id_tag, i)) si len(ocurrencias) > 1:
record['_id'] = id
ids.add(id) elif gen_uuid:
record['_id'] = unicode(uuid4()) elif
mfn: record['_id'] = record[ISIS_MFN_KEY]
if prefix: # iterar sobre una secuencia fija de
etiquetas para etiqueta en tupla (registro):
if str(tag).isdigit():
record[prefix+tag] = record[tag] del
record[tag] # es por eso que iteramos sobre una tupla # con las etiquetas,
y no directamente en el registro dict si es constante:
def main(): #
crear el analizador
analizador = argparse.ArgumentParser(
description='Convertir un archivo ISIS .mst o .iso en una matriz JSON')
'
por línea para inserción masiva en MongoDB a través de la utilidad mongoimport')
parser.add_argument( '-t',
'--type', type=int, metavar='ISIS_JSON_TYPE', default=1, help='ISIS-JSON type,
establece la estructura del campo: 1=string, 2=alist,' ' 3=dict (predeterminado=1)')
parser.add_argument( '-q', '--qty', type=int, default=DEFAULT_QTY,
help='cantidad máxima de registros para leer (predeterminado=TODOS)')
analizador.add_argument(
'''
# TODO: implementar esto para exportar grandes cantidades de registros a CouchDB
parser.add_argument( '-r', '--repeat', type=int, default=1, help='repetir operación, guardar
múltiples archivos JSON'
si args.mfn:
print('NO COMPATIBLE: -n/--mfn opción solo disponible para entrada .mst.') raise
SystemExit input_gen_func = iter_iso_records input_gen =
input_gen_func(args.file_name, args.type) if args.couch: args.out.write(' { "docs" : ')
si __nombre__ == '__principal__':
principal()
Ejecución de muestra con dos autos, semilla aleatoria 10. Esta es una prueba de documento válida:
"""
importar
aleatoriamente importar
colecciones cola de
importación importar
argparse tiempo de importación
DEFAULT_NUMBER_OF_TAXIS = 3
DEFAULT_END_TIME = 180
BÚSQUEDA_DURACIÓN = 5
VIAJE_DURACIÓN = 20
SALIDA_INTERVALO = 5
# BEGIN TAXI_PROCESS
def taxi_process(ident, viajes, start_time=0):
"""Ceda el paso al evento de emisión del simulador en cada cambio de estado""" tiempo
= produce el evento (hora de inicio, ident, 'salir del garaje') para i en el rango (viajes):
tiempo = produce el evento (hora, ident, 'recoger al pasajero ') tiempo = rendimiento
Evento (tiempo, ident, 'dejar pasajero')
# BEGIN TAXI_SIMULATOR
clase Simulador:
más:
print(msg.format(self.events.qsize()))
'*** fin del tiempo de simulación: {} eventos pendientes ***' msg =
# FINALIZAR TAXI_SIMULATOR
def calcular_duración(acción_anterior):
"""Calcular la duración de la acción usando una distribución exponencial""" if
acción_anterior en ['salir del garaje', 'dejar pasajero']: # nuevo estado es intervalo de ronda
= DURACIÓN_BÚSQUEDA elif acción_anterior == 'recoger pasajero':
si __nombre__ == '__principal__':
analizador = argparse.ArgumentParser(
description=' Simulador de flota de taxis.')
parser.add_argument('-e', '--end-time', type=int,
predeterminado=DEFAULT_END_TIME,
help='hora de finalización de la simulación; predeterminado = %s'
% DEFAULT_END_TIME)
analizador.add_argument('-t', '--taxis', type=int,
predeterminado=DEFAULT_NUMBER_OF_TAXIS,
help='número de taxis circulando; predeterminado = %s'
% DEFAULT_NUMBER_OF_TAXIS)
parser.add_argument('-s', '--seed', type=int, default=Ninguno,
help='generador aleatorio de semillas (para pruebas)')
argumentos = analizador.parse_args()
main(args.end_time, args.taxis, args.seed)
"""
Ejecución de muestra desde la línea de comando, semilla = 3, tiempo máximo transcurrido = 120 ::
# COMENZAR TAXI_SAMPLE_RUN
$ python3 taxi_sim.py -s 3 -e 120
taxi: 0 Evento (tiempo = 0, proc = 0, acción = 'salir del garaje')
taxi: 0 Evento (tiempo = 2, proc = 0, acción = 'recoger pasajero')
taxi: 1 Evento (tiempo = 5, proceso = 1, acción = 'salir del garaje')
taxi: 1 Evento(tiempo=8, proc=1, acción='recoger pasajero')
taxi: 2 Evento (tiempo = 10, proceso = 2, acción = 'salir del garaje')
taxi: 2 Evento(tiempo=15, proc=2, acción='recoger pasajero')
taxi: 2 Evento (hora = 17, proc = 2, acción = 'dejar al pasajero')
taxi: 0 Evento (tiempo = 18, proc = 0, acción = 'dejar pasajero')
taxi: 2 Evento(tiempo=18, proc=2, acción='recoger pasajero')
taxi: 2 Evento (tiempo = 25, proc = 2, acción = 'dejar pasajero')
taxi: 1 Evento (hora = 27, proc = 1, acción = 'dejar al pasajero')
taxi: 2 Evento(tiempo=27, proc=2, acción='recoger pasajero')
taxi: 0 Evento (tiempo = 28, proc = 0, acción = 'recoger pasajero')
taxi: 2 Evento (hora = 40, proc = 2, acción = 'dejar al pasajero')
taxi: 2 Evento(tiempo=44, proc=2, acción='recoger pasajero')
taxi: 1 Evento(tiempo=55, proc=1, acción='recoger pasajero')
taxi: 1 Evento (tiempo = 59, proc = 1, acción = 'dejar pasajero')
taxi: 0 Evento (tiempo = 65, proc = 0, acción = 'dejar pasajero')
taxi: 1 Evento(tiempo=65, proc=1, acción='recoger pasajero')
taxi: 2 Evento (hora = 65, proc = 2, acción = 'dejar al pasajero')
"""
El ejemplo A-7 cifra y descifra matrices de bytes aleatorios con el algoritmo RC4. Eso
depende del módulo arcfour.py (Ejemplo A-8) para ejecutar.
sistema de importación
tiempo de importación
EMPLEOS = 12
TAMAÑO = 2**18
def main(trabajadores=Ninguno):
si los trabajadores:
trabajadores = int(trabajadores)
t0 = tiempo.tiempo()
if __name__ == '__main__': if
len(sys.argv) == 2: trabajadores
= int(sys.argv[1]) else: trabajadores
= Ninguno
principal (trabajadores)
j = len(clave)
para i en rango(j, 256): # repetir hasta completar kbox[i] = kbox[ij]
for i in range(256): j = (j +
sbox[i] + kbox[i]) % 256 sbox[i], sbox[j] = sbox[j],
sbox[i]
# ciclo principal
i=0j=0
out_bytes =
bytearray()
coche = coche
devolver out_bytes
si __nombre__ == '__principal__':
prueba()
El ejemplo A-9 aplica el algoritmo hash SHA-256 a matrices de bytes aleatorios. Utiliza
hash lib de la biblioteca estándar, que a su vez utiliza la biblioteca OpenSSL escrita en C.
EMPLEOS = 12
TAMAÑO = 2**20
ESTADO = '{} trabajadores, tiempo transcurrido: {:.2f}s'
def sha(tamaño):
data = bytearray(randrange(256) for i in range(size)) algo = hashlib.new('sha256')
algo.update(data) return algo.hexdigest()
def main(trabajadores=Ninguno):
si trabajadores:
trabajadores = int(trabajadores)
t0 = tiempo.tiempo()
if __name__ == '__main__': if
len(sys.argv) == 2: trabajadores
= int(sys.argv[1]) else: trabajadores
= Ninguno
principal (trabajadores)
import o
import time
import sys
import string
import argparse
from collections import namedtuple from
enum import Enum
'
POP20_CC = ('CN IN ID DE EE. UU. BR PK NG BD RU JP
'MX PH VN ET EG DE IR TR CD FR'). dividir ()
DEFAULT_CONCUR_REQ = 1
MAX_CONCUR_REQ = 1
SERVIDORES = {
'REMOTO': 'http://flupy.org/data/flags', 'LOCAL': 'http://
localhost:8001/flags', 'DELAY': 'http://localhost:8002/flags',
'ERROR': 'http://localhost:8003/flags',
}
SERVIDOR_DEFAULT = 'LOCAL'
DEST_DIR = 'descargas/'
COUNTRY_CODES_FILE = 'códigos_país.txt'
def process_args(default_concur_req):
server_options = ', '.join(sorted(SERVERS)) parser =
argparse.ArgumentParser( description='Descargar
banderas para códigos de países. '
'Predeterminado: 20 países principales por población.')
parser.add_argument('cc', metavar='CC', nargs='*', help='código de
país o primera letra (por ejemplo, B para BA...BZ) ')
parser.add_argument('-a', '--all', action='store_true', help='get all
available flags (AD to ZW)') parser.add_argument('-e',
'--every', acción='almacenar_verdadero',
help='obtener banderas para cada código posible (AA...ZZ)')
analizador.add_argument('-l', '--limit', metavar='N', type=int,
help='limitar a N primeros códigos', default=sys.maxsize)
analizador.add_argument('-m', '--max_req', metavar='CONCURRENT', type=int,
default=default_concur_req,
help='máximo de solicitudes simultáneas
(predeterminado={})' .format(default_concur_req))
analizador.add_argument('-s', '--server', metavar='LABEL',
default=DEFAULT_SERVER,
help='Servidor a golpear; uno de {} (predeterminado={})'
.format(opciones_servidor, SERVIDOR_DEFAULT ))
parser.add_argument('-v', '--verbose', action='store_true',
help='salir información de progreso detallada')
args = parser.parse_args() if args.max_req < 1: print('*** Error
de uso: --max_req CONCURRENT debe ser >= 1')
parser.print_usage() sys. exit(1) si args.limit < 1: print('*** Error de uso: --
limit N debe ser >= 1') parser.print_usage() sys.exit(1)
args.server = args.server.upper() si
args.server no está en SERVIDORES:
print('*** Error de uso: --server LABEL debe ser una de', server_options)
parser.print_usage() sys.exit(1) try: cc_list =
expand_cc_args(args.every, args.all, args.cc, límite de argumentos)
imprimir(exc.args[0])
parser.print_usage() sys.exit(1)
si no es cc_list: cc_list
= sorted(POP20_CC) devolver
argumentos , cc_list
Versión secuencial
Ejemplo de ejecución::
--------------------
17 banderas descargadas. 9
no encontrado.
Tiempo transcurrido: 13,36 s
"""
importar colecciones
solicitudes de
importación importar tqdm
DEFAULT_CONCUR_REQ = 1
MAX_CONCUR_REQ = 1
# BEGIN FLAGS2_BASIC_HTTP_FUNCTIONS
def get_flag(base_url, cc):
url = '{}/{cc}/{cc}.gif'.format(base_url, cc=cc.lower()) resp = request.get
(url) if resp.status_code != 200: resp.raise_for_status() devolver contenido
resp.
else:
save_flag(image, cc.lower() + '.gif') status =
HTTPStatus.ok msg = 'OK'
si detallado:
imprimir (cc, msg)
# BEGIN FLAGS2_DOWNLOAD_MANY_SEQUENTIAL
def download_many(cc_list, base_url, verbose, max_req): counter =
collections.Counter() cc_iter = sorted(cc_list) si no es detallado:
cc_iter = tqdm.tqdm(cc_iter) for cc in cc_iter: try: res =
download_one (cc, base_url, detallado)
''
error_msg =
estado = res.status
si error_msg:
estado = HTTPStatus.error
counter[status] += 1 si es
detallado y error_msg:
imprimir ('*** Error para {}: {}'. formato (cc, error_msg))
contador de retorno
# END FLAGS2_DOWNLOAD_MANY_SEQUENTIAL
si __nombre__ == '__principal__':
principal (descargar_muchos, DEFAULT_CONCUR_REQ, MAX_CONCUR_REQ)
importar archivar
importar pytest
@pytest.yield_fixture def
db(): with
shelve.open(schedule.DB_NAME) as the_db: if
schedule.CONFERENCE not in the_db:
schedule.load_db(the_db)
producir el_db
def test_conference_record(db):
afirmar horario.CONFERENCIA en db
def test_speaker_record(db):
hablante = db['hablante.3471'] afirmar
hablante.nombre == 'Anna Martelli Ravenscroft'
def test_event_record(db):
event = db['event.33950'] afirmar
event.name == 'Habrá *Habrá* errores'
def test_event_venue(db):
evento = db['event.33950']
afirmar evento.venue_serial == 1449
El ejemplo A-13 es la lista completa del ejemplo schedule2.py presentado en “Recuperación de registros
vinculados con propiedades” en la página 598 en cuatro partes.
# COMENZAR HORARIO2_DEMO
# FINALIZAR PROGRAMA2_DEMO
>>> db.cerrar()
"""
# BEGIN SCHEDULE2_RECORD
importar advertencias importar
inspeccionar
importar osconfeed
DB_NAME = 'datos/planificación2_db'
CONFERENCIA = 'conferencia.115'
registro de clase :
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
volver No implementado
# FINALIZAR PROGRAMA2_REGISTRO
# BEGIN SCHEDULE2_DBRECORD
clase MissingDatabaseError(RuntimeError):
"""Generado cuando se requiere una base de datos pero no se configuró."""
clase DbRecord(Registro):
__db = Ninguno
@staticmethod
def set_db(db):
DbRecord.__db = db
@staticmethod
def get_db():
devuelve DbRecord.__db
@classmethod
def fetch(cls, ident): db =
cls.get_db() try: return
db[ident] excepto
TypeError: si db es
Ninguno:
def __repr__(self): if
hasattr(self, 'serial'): cls_name =
self.__class__.__name__ return '<{} serial={!r}
>'.format(cls_name, self.serial) else: return super( ).__repr__()
# FINALIZAR HORARIO2_DBRREGISTRO
# BEGIN SCHEDULE2_EVENT
clase Evento(DbRecord):
@property
def lugar(self): clave =
'lugar.{}'.format(self.venue_serial) return
self.__class__.fetch(clave)
@property
def altavoces(self): si
no hasattr(self, '_speaker_objs'):
spkr_serials = self.__dict__['speakers'] fetch =
self.__class__.fetch self._speaker_objs =
[fetch('speaker.{}'.format(key)) for key in spkr_serials]
devolver self._speaker_objs
def __repr__(self): if
hasattr(self, 'name'): cls_name
= self.__class__.__name__ return '<{} {!r}
>'.format(cls_name, self.name) else: return super().
__repr__()
# FINALIZAR PROGRAMA2_EVENTO
# BEGIN SCHEDULE2_LOAD
def load_db(db): raw_data
= osconfeed.load()
advertencias.warn('cargando ' + DB_NAME)
para la colección, rec_list en raw_data['Schedule'].items():
record_type = colección[:-1]
cls_name = record_type.capitalize() cls =
globals().get(cls_name, DbRecord) if
inspect.isclass(cls) and issubclass(cls, DbRecord): factory = cls
else: factory = DbRecord para registro en rec_list: clave = '{}.
{}'.format(record_type, record['serial']) record['serial'] = key db[key]
= factory(**record)
# FINALIZAR HORARIO2_CARGAR
@pytest.yield_fixture def
db(): with
shelve.open(schedule.DB_NAME) as the_db: if
schedule.CONFERENCE not in the_db:
schedule.load_db(the_db)
producir el_db
def test_record_attr_access():
rec = horario.Record (spam=99, huevos=12)
afirmar rec.spam == 99 afirmar rec.eggs == 12
def test_record_repr():
rec = horario.DbRecord(spam=99, huevos=12)
afirmar 'Objeto DbRecord en 0x' en repr(rec) rec2 =
horario.DbRecord (serial=13) afirmar repr(rec2) ==
"<DbRecord serial=13> "
def test_conference_record(db):
afirmar horario.CONFERENCIA en db
def test_speaker_record(db):
hablante = db['hablante.3471']
afirmar hablante.nombre == 'Anna Martelli Ravenscroft'
def test_dbrecord(db):
horario.DbRecord.set_db( db)
lugar = horario.DbRecord.fetch('lugar.1585') afirmar
lugar.nombre == 'Exhibición Hall B'
def test_event_record(db):
evento = db['evento.33950']
afirmar repr(evento) == "<Evento 'Habrá *habrá* errores'>"
def test_event_venue(db):
horario.Evento.set_db (db)
evento = db['evento.33950']
afirmar evento.lugar_serial == 1449
afirmar evento.lugar == db['lugar.1449']
afirmar evento.lugar.nombre == 'Portland 251 '
def test_event_speakers(db):
horario.Evento.set_db (db)
evento = db['event.33950']
afirmar len(event.speakers) == 2
def test_event_no_speakers(db):
horario.Evento.set_db( db) event
= db['event.36848'] afirmar
len(event.speakers) == 0
Jerga de Python
Muchos términos aquí no son exclusivos de Python, por supuesto, pero particularmente en las definiciones
puede encontrar significados que son específicos de la comunidad de Python.
Una clase que no se puede instanciar, solo argumento Una expresión que se pasa a una función
subclasificar. Los ABC son cómo se formalizan las cuando se la llama. En lenguaje pitónico, argumento
interfaces en Python. En lugar de heredar de ABC, y parámetro son casi siempre sinónimos. Consulte el
una clase también puede declarar que cumple con la parámetro para obtener más información sobre la
interfaz registrándose en ABC para convertirse en distinción y el uso de estos términos.
una subclase virtual.
atributo
accesorio Los métodos y los atributos de datos (es decir,
Un método implementado para proporcionar acceso "campos" en términos de Java) se conocen como
a un único atributo de datos. Algunos autores usan atributos en Python. Un método es solo un atributo
acessor como un término genérico que engloba los que resulta ser un objeto invocable (generalmente
métodos getter y setter, otros lo usan para referirse una función, pero no necesariamente).
solo a los getters, refiriéndose a los setters como
mutadores.
715
Machine Translated by Google
BDFL
Benevolent Dictator For Life, alias de Guido van Rossum, creador Un nombre desafortunado que todavía se usa para referirse a
de la lengua Python. bytes o bytearray en Python 3. En Python 2, el tipo str era realmente
calibre una cadena de bytes, y el término tenía sentido para distinguir str de
orden de bytes utilizado en la codificación. Aunque redundante, un Un objeto que se puede invocar con el operador de llamada (), para
BOM codificado como b'\xef\xbb \xbf' se puede encontrar en archivos devolver un resultado o para realizar alguna acción. Hay siete tipos
UTF-8. de objetos invocables en Python: funciones definidas por el usuario,
Ese objeto es el método enlazado. Se puede invocar sin pasar el PEP-8 recomienda que los nombres de las clases se escriban en
Cheese Shop
función incorporada (BIF) Nombre original del Python Package Index (PyPI), después del
Una función incluida con el intérprete de Python, codificada en el sketch de Monty Python sobre una tienda de quesos donde no hay
lenguaje de implementación subyacente (es decir, C para CPython; nada disponible. Al escribir estas líneas, el alias https://
Java para Jython, etc.). El término a menudo se refiere solo a las cheeseshop.python.org todavía funciona. Ver PyPI.
punto de código
base de datos ter. A partir de Unicode 7.0, menos del 3 % de it__ es un inicializador, ya que en realidad no construye la
todos los puntos de código se asignan a caracteres. instancia, sino que la recibe como su propio argumento. El
En la documentación de Python, el término se puede escribir término constructor describe mejor el método de clase
como una o dos palabras. Por ejemplo, en el Capítulo 2, __new__ , que Python llama antes que __init__, y es
"Funciones integradas", de la Referencia de la biblioteca de responsable de crear una instancia y devolverla. Ver
Python, se dice que la función chr toma un "punto de código" inicializador.
entero, mientras que su inversa, ord, se describe como que
problema con el diseño de un programa. Por ejemplo, el uso colecciones pero no contenedores.
excesivo de verificaciones de instancias de instancias contra
códec
coroutine
(codificador/decodificador) Un módulo con funciones para
Un generador utilizado para la programación concurrente
codificar y decodificar, generalmente de str a bytes y viceversa,
mediante la recepción de valores de un planificador o un bucle
aunque Python tiene algunos códecs que realizan
de eventos a través de coro.send (valor).
transformaciones de bytes a bytes y de str a str .
El término puede usarse para describir la función generadora
o el objeto generador obtenido llamando a la función
colección generadora. Ver generador.
Término genérico para estructuras de datos hechas de
CRUD
considerado dañino
Acrónimo de Crear, Leer, Actualizar y Eliminar, las cuatro
La carta de Edsger Dijkstra titulada “Ir a la declaración
funciones básicas en cualquier aplicación que almacene
considerada dañina” estableció una fórmula para los títulos de
registros.
los ensayos que critican alguna técnica informática. El artículo
de Wikipedia "Considerado dañino" enumera varios decorador
para __in
copia profunda
descriptor EAFP
Una clase que implementa uno o más de los métodos Acrónimo que representa la cita "Es más fácil pedir perdón que
especiales __get__, __set__ o __delete__ se convierte en un permiso", atribuido a la pionera de la informática Grace Hopper,
descriptor cuando una de sus instancias se usa como un y citado por pitonistas que se refieren a prácticas de
atributo de clase de otra clase, la clase administrada. Los programación dinámica como acceder a atributos sin probar
descriptores administran el acceso y la eliminación de atributos primero si existen y luego detectarlos. la excepción cuando ese
administrados en la clase administrada, a menudo almacenando es el caso. La cadena de documentación para la función ha
datos en las instancias administradas. sattr en realidad dice que funciona "llamando a getattr (objeto,
docstring
adjunto y el intérprete la guarda como el atributo __doc__ de Un objeto iterable que construye todos sus elementos a la vez.
ese objeto. Véase también doctest. En Python, la comprensión de una lista es ansiosa. Contraste
con perezoso.
Un módulo con funciones para analizar y ejecutar ejemplos Un enfoque de diseño de sistemas que recomienda que los
incrustados en las cadenas de documentación de los módulos errores se informen lo antes posible. Python se adhiere a este
de Python o en archivos de texto sin formato. También se principio más de cerca que la mayoría de los lenguajes
puede utilizar desde la línea de comandos como: dinámicos.
Por ejemplo, no hay un valor "indefinido": variables a las que
python -m doctest se hace referencia antes de la inicialización
module_with_tests.py
genera un error, y my_dict[k] genera una excepción si falta k
SECO
(en contraste con JavaScript). Como otro ejemplo, la asignación
No se repita: un principio de ingeniería de software que paralela a través del desempaquetado de tuplas en Python
establece que “Cada pieza de conocimiento debe tener una solo funciona si cada elemento se maneja explícitamente,
representación única, inequívoca y autorizada dentro de un mientras que Ruby se ocupa silenciosamente de las
sistema”. Apareció por primera vez en el libro The Pragmatic discrepancias en el recuento de elementos ignorando los
Programmer de Andy Hunt y Dave Thomas (Addison-Wesley, elementos no utilizados en el lado derecho del =, o asignando
1999). nil a extra . variables en el lado izquierdo.
tipificación
Lo contrario de veraz.
similar a un archivo Se usa informalmente en la función generadora, además del objeto que resulta de llamarla.
documentación oficial para referirse a objetos que
implementan el protocolo de archivo, con métodos
función generadora
como leer, escribir, cerrar, etc. Las variantes
Una función que tiene la palabra clave yield en su cuerpo.
comunes son archivos de texto que contienen
Cuando se invoca, una función de generador devuelve un
cadenas codificadas con lectura y escritura
generador.
orientadas a líneas, String Instancias de E/S que
son archivos de texto en memoria y archivos generador de
binarios que contienen bytes no codificados. Este expresión Una expresión entre paréntesis que usa la misma
último puede estar amortiguado o no amortiguado. ABC para elsintaxis
archivo estándar
de una lista por comprensión, pero devuelve un
Los tipos se definen en el módulo io desde Python 2.6. generador en lugar de una lista. Una expresión generadora
asignarse a variables, pasarse como argumento y devolverse Un grupo de funciones diseñadas para implementar la misma
como resultado de otra función). Las funciones de Python son operación de manera personalizada para diferentes tipos de
son secuencias planas. Contrasta con list, tu ple y Software (Addison Wesley, 1995), escrito por la llamada Gang
collections.deque, que son secuencias of Four (GoF): Erich Gamma, Richard Helm, Ralph Johnson y
función
informal, la palabra función se usa para describir cualquier objeto Un objeto es hashable si tiene métodos __hash__ y __eq__ ,
invocable, como métodos e incluso clases a veces. La lista con las restricciones de que el valor hash nunca debe cambiar y
oficial de funciones integradas incluye varias si a == b entonces hash(a) == hash(b) también debe ser
Verdadero. La mayoría de los tipos integrados inmutables son
Clases integradas como dict, range y str. hashable, pero una tupla solo es hashable si cada uno de sus
genex
modismo
partes. La frase fue acuñada por Kelly Johnson, una ingeniera
“Una manera de hablar que es natural para los hablantes nativos aeroespacial muy consumada que trabajó en el Área 51 real
de un idioma”, según Princeton WordNet. diseñando algunos de los aviones más avanzados del siglo XX.
tiempo de
importación El momento de la ejecución inicial de un módulo perezoso
cuando el intérprete de Python carga su código, lo evalúa de Un objeto iterable que produce elementos bajo demanda. En
arriba a abajo y lo compila en un código de bytes. Aquí es Python, los generadores son perezosos.
comprensión de
inicializador
lista Una expresión encerrada entre corchetes que usa las
Un mejor nombre para el método __init__ (en lugar de
palabras clave for y in para construir una lista procesando y
constructor). Inicializar la instancia recibida como propia es tarea
filtrando los elementos de uno o más iterables. La comprensión
de __en ella__. La construcción de instancias reales se realiza
de una lista funciona con entusiasmo. Ver ansioso.
mediante el método __new__ . Véase constructor.
vivacidad
iterable
Un sistema asincrónico, con subprocesos o distribuido exhibe la
Cualquier objeto del que la función integrada iter pueda obtener
propiedad de vivacidad cuando “eventualmente sucede algo
un iterador. Un objeto iterable funciona como fuente de elementos
bueno” (es decir, incluso si algún cálculo esperado no está
en bucles for , comprensiones y desempaquetado de tuplas. Los
ocurriendo en este momento, eventualmente se completará). Si
objetos que implementan un método __iter__ que devuelve un
un sistema se estanca, ha perdido su vitalidad.
iterador son iterables.
iterable Un sinónimo moderno y más preciso de desempaquetado Un atributo público administrado por un objeto descriptor. Aunque
de tuplas. Véase también asignación paralela. el atributo administrado se define en la clase administrada,
metaclase
Principio de Una clase cuyas instancias son clases. Por defecto, las clases
KISS El acrónimo significa "Keep It Simple, Stupid". Esto exige de Python son instancias de tipo, por ejemplo, type(int) es el tipo
buscar la solución más sencilla posible, con el menor número de
de clase,
por lo tanto, el tipo es una metaclase. Las metaclases definidas instancia administrada. En consecuencia, si se establece un
por el usuario se pueden crear por tipo de subclase. atributo del mismo nombre en la instancia administrada, sombreará
una introspección de las declaraciones de clase del modelo para Asignador relacional de objetos: una API que brinda acceso a las
determinar cómo validar los campos de registro de la base de tablas y registros de la base de datos como clases y objetos de
datos y convertir los tipos de base de datos en tipos de Python. Python, proporcionando llamadas a métodos para realizar
fuente, un parche de mono solo afecta la instancia del programa overriding descriptor
que se está ejecutando actualmente. Los parches de mono Un descriptor que implementa __set__ y, por lo tanto, intercepta
rompen la encapsulación y tienden a estar estrechamente y anula los intentos de establecer el atributo administrado en la
relacionados con los detalles de implementación de las unidades instancia administrada. También llamado descriptor de datos o
de código parcheadas, por lo que se consideran soluciones descriptor forzado. Contraste con el descriptor no predominante.
código.
asignación paralela
Una clase diseñada para ser subclasificada junto con una o más desestructuración.
clases adicionales en un Esta es una aplicación común del desempaquetado de tuplas.
Una implementación de método concreto proporcionada en un están vinculados a esas variables. En este libro, traté de usar
ABC o en una clase mixin. argumento para referirme a un parámetro real pasado a una
tiempo de ejecución.
mento
Un descriptor que no implementa __set__ y, por lo tanto, no Llamar a next(coro) en una corrutina para avanzarla a su primera
interfiere con la configuración del atributo administrado en el expresión de rendimiento para que
se vuelve listo para recibir valores en sucesivas llamadas permitiendo el acceso a elementos a través de índices enteros
2014, PyPy es 6,8 veces más rápido que CPython en promedio, copia
según los puntos de referencia publicados. PyPy se pronuncia superficial Una copia de un objeto que comparte referencias a todos
como "pie-pie" para evitar confusiones con PyPI. los objetos que son atributos del objeto original. Contraste con
copia profunda.
singleton
Pythonic Un objeto que es la única instancia existente de una clase,
Se usa para elogiar el código Python idiomático, que hace un generalmente no por accidente, sino porque la clase está diseñada
buen uso de las funciones del lenguaje para ser conciso, legible para evitar la creación de más de una instancia. También hay un
y, a menudo, también más rápido. patrón de diseño llamado Singleton, que es una receta para
También se dice de las API que permiten la codificación de una codificar tales clases.
manera que parece natural para los programadores de Python
refcount
slicing
El contador de referencia que cada objeto CPython mantiene Producir un subconjunto de una secuencia usando la notación de
internamente para determinar cuándo puede ser destruido por el corte, por ejemplo, my_sequence[2:6].
recolector de basura. El corte por lo general copia datos para producir un nuevo objeto;
referente la secuencia.
REEMPLAZAR
serpiente_caso
Read-eval-print loop, una consola interactiva, como Python La convención de escribir identificadores uniendo palabras con el
estándar o alternativas como ipython, bpython y Python Anywhere. carácter de subrayado (_), por ejemplo, ejecutar_hasta_completar.
palabras sin separadores. La biblioteca no se limita al tamaño de palabra de la CPU, str contiene puntos
estándar de Python tiene muchos ejemplos de datos Unicode multibyte) y abstracciones de muy alto nivel
de identificadores snake_case , pero también (por ejemplo, dict, deque, etc.). Los tipos pueden ser definidos
muchos ejemplos de identificadores sin por el usuario o integrados en el intérprete (un tipo "incorporado").
separación entre palabras (p. ej., getattr, Antes de la unificación de tipo/clase de cuenca hidrográfica en
classmethod, isinstance, str.endswith, etc.). Python 2.2, los tipos y las clases eran entidades diferentes, y las
Véase CamelCase. clases definidas por el usuario no podían extender los tipos
integrados. Desde entonces, los tipos incorporados y las clases
método especial
de estilo nuevo se volvieron compatibles, y una clase es una
Un método con un nombre especial como __getitem__, escrito
instancia de tipo. En Python 3 todas las clases son clases de
con guiones bajos dobles al principio y al final. Casi todos los
estilo nuevo.
métodos especiales reconocidos por Python se describen en el
capítulo "Modelo de datos" de The Python Language Reference, Ver clase y metaclase.
pero algunos que se usan solo en contextos específicos se
método sin consolidar
documentan en otras partes de la documentación. Por ejemplo,
el método __missing__ de asignaciones se menciona en “4.10. Un método de instancia al que se accede directamente en una
Mapping Types — dict" en The Python Standard Library. clase no está vinculado a una instancia; por lo tanto, se dice que
es un "método no ligado". Para tener éxito, una llamada a un
un descriptor. Véase también atributo gestionado. uniforme Bertrand Meyer, creador del Lenguaje Eiffel, escribió:
de tupla Asignación de elementos de un objeto iterable a una operador, haciendo que las llamadas a funciones y la creación
máquina (p. ej., float y bytes) , mientras que otros son extensiones
devueltas por los métodos dict .keys(), .values() y .items(), compatibilidad en el diseño de Python 3, ya que la mayoría
que proporcionan una vista dinámica de las claves y valores de las fallas no podrían solucionarse de otra manera. Muchos
dict sin duplicación de datos, lo que ocurre en Python 2 de los problemas de Kuchling se solucionaron en Python 3.
los valores también se pueden modificar. “No lo va a necesitar”, es un eslogan para evitar implementar
una funcionalidad que no es inmediatamente necesaria en
base a suposiciones sobre necesidades futuras.
subclase virtual
Consulte la documentación para abc.ABCMeta.register. Type importa esto en cualquier consola de Python desde la
versión 2.2.
verruga
Índice
Nos gustaría escuchar sus sugerencias para mejorar nuestros índices. Envíe un correo electrónico a [email protected].
725
Machine Translated by Google
A
ABC (Abstract Base Class)
ventajas de, 316 uso
apropiado de, 308, 317, 341 como
mixins, 360
726 | Índice
Machine Translated by Google
Índice | 727
Machine Translated by Google
728 | Índice
Machine Translated by Google
de, 464 manejo de excepciones, 472 para simulación conversión de base de datos, 437, 691 módulo
de eventos discretos, 489–498, 696 dbm, 594 clase decimal. Decimal, 373 patrón
Decorator, 199, 214 decoradores y cierres, 183–215
Índice | 729
Machine Translated by Google
comportamiento de,
234 modificaciones in situ con, 36
eliminación de atributo de objeto con, 614
generadores de delegación, 478 paquete
deques, 55 clases de descriptor, 625, 635
instancias de descriptor, 627 personalización
de descriptores, 659, 673 definición de +directiva ELLIPSIS, 7
término, 718 no invalidante, 640, 721 definición de término, 718
invalidación, 640, 721 (ver también Ataque DOS (Denegación de servicio), 507, 521, 555
descriptores de atributos) validación, 648 doble asterisco (**), 148 doble guión bajo (dunder), 3,
patrones de diseño, 167–182 4, 718 descargas
730 | Índice
Machine Translated by Google
tores)
manejo de errores, para descargas web, 520
función eval, 659 ejercicios de tiempo de evaluación,
662, 669
Índice | 731
Machine Translated by Google
functools.lru_cache, 200
functools.partial, 159
functools.reduce(), 289, 434
functools.singledispatch, 202, 203
functools.wraps decorador, 199
generador de funciones en, 424 creación
de futuros de, 511 definición de término, 505
en asyncio, 545 en la biblioteca
Concurrent.futures, 511 ejemplo práctico gremlins, definición de término, 107
de, 512 propósito de, 511 frente a
devoluciones de llamada, 562 rendimiento H
de construcción y, 546
función hasattr (objeto, nombre), 617
futuros.ProcessPoolExecutor, 530
algoritmo de tablas hash para, 88 definición
de hashable, 65 eficiencia de, 85, 91
igualdad y, 87 patrones de bits hash,
88, 689 colisiones hash, 89
implementación de dict con, 87
importancia de , 63 limitaciones de
Recolección de basura G , 234, tipo de clave, 90 limitaciones de
245 codificación gb2312, 104 memoria, 90 consecuencias prácticas,
expresiones generadoras (genexps) 93 ordenación de claves impredecible,
como alternativa al mapa/filtro, 142, 424 91
beneficios de, 25 mejor uso de, 419
definición de término, 719 evaluación
perezosa de, 396, 417
ABC hashable, 323
objetos hashable, 257, 719
732 | Índice
Machine Translated by Google
Argumento de clave K ,
ABC (Abstract Base Class), 319–335 42, 61 ausencia de
definición de término, 309 explícito, 359 en manejo de claves, 68, 70
otros idiomas, 345 protocolos como limitaciones en tablas hash, 90
informales, 302 asignación con búsqueda de clave flexible,
70 ordenación impredecible en tablas hash, 91
Enfoque de Python para, 308 argumentos de solo palabras clave, 148
secuencia, 310 Principio KISS (Keep It Simple, Stupid), 720
Índice | 733
Machine Translated by Google
métodos mágicos, 4, 16 de, 347 en el marco Django, 362–366 en la práctica, 356 orden
(ver también métodos especiales) de resolución de métodos y, 351 –356 orígenes de, 368
trampas de subclases, 348
atributos administrados, 627, 720
clases administradas, 626, 720
instancias administradas, 627, 720
función de mapa, 23, 142, 291
beneficios de mapeo de la sintaxis
literal para, 95
734 | Índice
Machine Translated by Google
mutabilidad, 244 asignaciones mutables, 237 de, 228, 718 destrucción de, 245
secuencias mutables, 20, 312 ansioso, 718 similar a un archivo,
N
223, 722 rastreo, 236 inalcanzable ,
234 módulo de operador, 156
modificación de nombres, 633, 721
sobrecarga de operadores, 371–397
tipos de tuplas con nombre, 30 listas operadores de asignación aumentada,
anidadas, 37 desempaquetado de
388–392 beneficios de, 371, 395
tuplas anidadas, 29
inconvenientes de, 395 para la
NFC (Forma de normalización C), 117 Función
multiplicación escalar, 380 para la
nfc_equal, 120 suma de vectores, 375–380
Normalización NFKC/NFKD, 118 declaración
operadores infijos, 382 descripción
no local, 195 palabra clave no local, 183
general de, 372 rico operadores de
descriptores no anulados, 640, 721
comparación, 384–388 operadores
coincidencia de texto normalizado, 120 operador
unarios, 372 operador or, 372 OrderedDict, 66, 75 ORM
no, 372 (mapeador de objetos relacionales), 721 módulo os, 130
procesos OS, 490 función de generador os.walk, 424
No implementado, 378
descriptores anulados, 640, 721
NotImplementedError, 378 tipos
numéricos emulando, 9 guardando,
49 torre numérica, 323
NumPy
beneficios de, 52
Índice | 735
Machine Translated by Google
visualizaciones formateadas,
paréntesis ( () ), 22, 144 módulo 253 hashability de, 257
pickle, 594 texto sin formato, representaciones de objetos, 248
definición de término, 135 prima, definición atributos de clase anulados, 267
de término, 721 decoradores de atributos privados y protegidos, 262, 308
imprimación, 543 atributos privados, 262, Ejemplo de clase Vector2d, 248
273, 308 proceso, definición de término, Atributo de clase __slots__, 264
490 pantallas de progreso, 521, 527 pitónico, definición de término, 722
propiedades ventajas de, 681 validación Biblioteca PyUCA, 126
de atributos con, 604 fábricas de
propiedades de codificación, 611,
635 cadenas de documentación y,
Paquete de cola Q , 57
610 atributos de instancia y, 608
recuperación de registros vinculados con,
598 descripción general de, 606 función de R
propiedad, 199 atributos protegidos, 262
RC4 algoritmo, 700
protocolos como interfaces informales, 302 función re.findall, 417 función
beneficios de, 312 definición de término, 308 re.finditer, 417 legibilidad, 21
tipificación de pato y, 279 naturaleza dinámica web en tiempo real, 581
de, 313 implementación de, 309 implementación recuperación de registros,
en tiempo de ejecución, 312 secuencia, 598 función de reducción,
280, 310, 402 frente a herencia, 309 142, 288, 290, 434 recuento de referencias
(refcount), 235, 245, 722 variables de referencia, 220
referencias
fuerte, 723
débil, 236, 724
objetos referentes, 236, 722
método de registro, 332, 338
PSF (Fundación de software de Python), 684 decoradores de registro, 187, 206
PUG (Grupo de usuarios de Python), 684 expresiones regulares, 129
Biblioteca py.test, 708 PyPI (Índice de paquetes REPL (Read-eval-print-loop), 722 función
de Python), 722 Lenguaje PyPy, 722 Biblioteca repr, 248 módulo reprlib, 277 solicitudes
de imágenes de Python (PIL), 102 Biblioteca de biblioteca, 509 argumento inverso, 42
estándar de Python ABCs in, 321 ventajas de operadores invertidos, 13 operadores de
métodos especiales, 6 array.array, 251 comparación enriquecidos, 372, 384–388
decoradores en, 199–205 paquete deques,
promedio móvil, computación, 192, 468
55 funciones de generador en, 424–433 colas tiempo de ejecución , 187, 312, 661 método
de implementación, 57 kit de herramientas run_in_executor, 560
GUI de Tkinter, 356, 361 reglas de prueba de
valor de verdad, 12
736 | Índice
Machine Translated by Google
Índice | 737
Machine Translated by Google
W
verrugas, definición de término, 724
referencias débiles, 236, 724
738 | Índice
Machine Translated by Google
X
Operador XOR (^), 258, 288
Índice | 739
Machine Translated by Google
Sobre el Autor
Luciano Ramalho ya era desarrollador web antes de la salida a bolsa de Netscape en 1995, y cambió de Perl
a Java a Python en 1998. Desde entonces, ha trabajado en algunos de los portales de noticias más grandes
de Brasil usando Python y enseñó desarrollo web en Python en la Los medios de comunicación, la banca y los
sectores gubernamentales brasileños. Sus credenciales como orador incluyen PyCon US (2013), OSCON
(2002, 2013, 2014) y 15 charlas a lo largo de los años en PythonÿBrasil (la PyCon brasileña) y FISL (la
conferencia FLOSS más grande del hemisferio sur). Ramalho es miembro de Python Software Foundation y
cofundador de Garoa Hacker Clube, el primer hackerspace de Brasil. Es copropietario de Python.pro.br, una
empresa de formación.
Colofón El
animal de la portada de Fluent Python es un lagarto de arena Namaqua (Pedioplanis namaquensis), una
criatura esbelta con una cola larga de color marrón rosado. Es de color negro con cuatro rayas blancas, patas
marrones con manchas blancas y vientre blanco.
Activo durante el día, es uno de los lagartos más rápidos. Habita en planicies de grava de arena con escasa
vegetación, permaneciendo inactiva en madrigueras durante el invierno, que excava en la base de los arbustos.
La lagartija de arena Namaqua se puede encontrar en toda Namibia, en sabanas áridas y regiones
semidesérticas. Se alimenta de pequeños insectos. En noviembre, las hembras ponen entre tres y cinco huevos.
Muchos de los animales de las portadas de O'Reilly están en peligro de extinción; todos ellos son importantes
para el mundo. Para obtener más información sobre cómo puede ayudar, visite animals.oreilly.com.
La imagen de la portada es de Wood's Natural History #3. Las fuentes de la portada son URW Typewriter y
Guardian Sans. La fuente del texto es Adobe Minion Pro; la fuente del encabezado es Adobe Myriad Condensed;
y la fuente del código es Ubuntu Mono de Dalton Maag.