100% encontró este documento útil (1 voto)
1K vistas

Fluent Python - Clear, Concise, and Effective Programming Español

Cargado por

banl
Derechos de autor
© © All Rights Reserved
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
100% encontró este documento útil (1 voto)
1K vistas

Fluent Python - Clear, Concise, and Effective Programming Español

Cargado por

banl
Derechos de autor
© © All Rights Reserved
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
Está en la página 1/ 767

Machine Translated by Google

Machine Translated by Google

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

ÿ Estructuras de datos: aproveche al máximo los tipos integrados y


“ tesoro
Python fluido es un
escondido lleno de
entender la dualidad de texto versus bytes en la era Unicode
programacion util
ÿ Funciones como objetos: vea las funciones de Python como objetos de primera clase
trucos para intermedio a
y comprenda cómo afecta esto a los patrones de diseño populares.

ÿ 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

ÿ Metaprogramación: comprenda cómo funcionan las propiedades, los descriptores


de atributos, los decoradores de clases y las metaclases.

Luciano Ramalho, programador de Python desde 1998, es miembro de la Python Software


Foundation, copropietario de Python.pro.br, una empresa de capacitación en Brasil.
y cofundador de Garoa Hacker Clube, el primer hackerspace de Brasil. Ha dirigido equipos de
desarrollo de software e impartido cursos de Python en los sectores de medios, banca y
gobierno de Brasil.

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.

Impreso en los Estados Unidos de América.

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].

Editores: Meghan Blanchette y Rachel Roumeliotis Indizador: Judy McConville


Montaje de producción: Melanie Yarbrough Diseño de portada: Ellie Volckhausen
Correctora: Kim Cofer Diseñador de interiores: David Futato
Correctora: Jasmine Kwityn Ilustrador: Rebecca Demarest

Agosto de 2015: Primera edición

Historial de revisión de la primera edición:

2015-07-24: Primer lanzamiento

2015-08-21: Segundo lanzamiento

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

Para Marta, com todo o meu amor.


Machine Translated by Google
Machine Translated by Google

Tabla de contenido

Prefacio. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . XV

Parte I. Prólogo

1. El modelo de datos de Python. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3


Una baraja de cartas pitónica 4
Cómo se utilizan los métodos especiales 8

Emulación de tipos numéricos 9

Representación de cadenas 11

Operadores aritméticos Valor booleano 12

de un tipo personalizado 12

Descripción general de los métodos especiales 13

Por qué len no es un método 14

Resumen del capítulo 14

Otras lecturas 15

Parte II. Estructuras de datos

2. Una matriz de secuencias. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19


Descripción general de las secuencias integradas 20

Comprensiones de listas y expresiones generadoras 21

Comprensión de listas y legibilidad 21

Listcomps Versus mapa y filtro 23


Productos cartesianos 23

Expresiones generadoras 25

Las tuplas no son solo listas inmutables 26

Tuplas como registros 26

Desempaquetado de tuplas 27

v
Machine Translated by Google

Desempaquetado de tuplas anidadas 29

Tuplas con nombre 30

Tuplas como listas inmutables 32

rebanar 33

Por qué las divisiones y el rango excluyen el último elemento 33

Rebanar objetos 34

Corte multidimensional y puntos suspensivos 35

Asignación a sectores 36

Usar + y * con Secuencias 36

Construyendo Listas de Listas 37

Asignación aumentada con secuencias A += 38

Rompecabezas de asignación list.sort y la función 40


incorporada ordenada Gestión de secuencias 42

ordenadas con bisect 44

Buscar con bisect Insertar 44

con bisect.insort Cuando una lista 47


no es la respuesta 48

arreglos 48

Vistas de memoria 51

NumPy y SciPy 52

Deques y otras colas 55

Resumen del capítulo 57

Otras lecturas 59

3. Diccionarios y Conjuntos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63

Tipos de mapeo genéricos 64

Comprensiones de dict 66

Descripción general de los métodos de mapeo comunes 66

Manejo de claves faltantes con setdefault 68

Asignaciones con búsqueda de clave flexible 70

defaultdict: otra versión de las llaves perdidas 70

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

Consecuencias prácticas de cómo funciona dict 90

Cómo funcionan los conjuntos: consecuencias prácticas 93

Resumen del capítulo 93

Otras lecturas 94

4. Texto versus Bytes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97


Problemas de carácter 98

Esenciales de byte 99

Estructuras y vistas de memoria 102


Codificadores/Decodificadores básicos 103

Comprensión de los problemas de codificación/decodificación 105

Hacer frente a UnicodeEncodeError 105

Lidiando con UnicodeDecodeError 106

SyntaxError al cargar módulos con codificación inesperada 108

Cómo descubrir la codificación de una secuencia de bytes 109


BOM: un gremlin útil 110

Manejo de archivos de texto 111

Valores predeterminados de codificación: un manicomio 114

Normalización de Unicode para comparaciones más sanas 117

Estuche plegable 119

Funciones de utilidad para coincidencia de texto normalizado 120

“Normalización” extrema: eliminando los signos diacríticos 121

Clasificación de texto Unicode 124

Clasificación con el algoritmo de clasificación Unicode 126


La base de datos Unicode 127

Modo dual str y bytes API str Versus 129

bytes en expresiones regulares str Versus bytes en 129

funciones os 130

Resumen del capítulo 132

Otras lecturas 133

Parte III. Funciones como objetos

5. Funciones de primera clase. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139

Tratar una función como un objeto Funciones 140

de orden superior 141

Reemplazos modernos para mapear, filtrar y reducir 142

Funciones anónimas 143

Los siete sabores de los objetos invocables 144

Tipos invocables definidos por el usuario 145

Introspección de funciones 146

Tabla de contenido | viii


Machine Translated by Google

De parámetros posicionales a parámetros de solo palabra 148

clave Recuperación de información sobre parámetros 150


Anotaciones de funciones Paquetes para programación 154

funcional El módulo operador Argumentos congelados con 156

functools.partial Resumen del capítulo Lecturas adicionales 156


159
161
162

6. Patrones de diseño con funciones de primera clase. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167


Estudio de caso: estrategia de refactorización 168

Estrategia clásica 168

Estrategia orientada a funciones 172

Elegir la mejor estrategia: enfoque simple 175

Búsqueda de estrategias en un módulo 176


Dominio 177

Resumen del capítulo 179

Otras lecturas 180

7. Función Decoradores y Cerramientos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183


Decoradores 101 184

Cuando Python ejecuta decoradores 185

Patrón de estrategia mejorado por decorador 187

Reglas de alcance variable 189


Cierres 192
La declaración no local 195

Implementando un decorador simple 196


Cómo funciona 198

Decoradores en la biblioteca estándar 199


Memoización con functools.lru_cache Funciones 200

genéricas con envío único 202


Decoradores apilados 205
Decoradores parametrizados 206

Un decorador de registro parametrizado 206


El decorador de reloj parametrizado 209

Resumen del capítulo 211

Otras lecturas 212

viii | Tabla de contenido


Machine Translated by Google

Parte IV. Modismos orientados a objetos

8. Referencias a objetos, mutabilidad y reciclaje. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219


Las variables no son cajas 220

Identidad, igualdad y alias Elegir entre 221

== y es La relativa inmutabilidad de 223

las tuplas Las copias son superficiales por 224

defecto Copias profundas y superficiales de 225

objetos arbitrarios Parámetros de funciones como referencias 228


Tipos mutables como parámetros predeterminados: mala idea 229

Programación defensiva con parámetros mutables Del and 230

Garbage Collection Referencias débiles El sketch de 232

WeakValueDictionary Limitaciones de las referencias débiles Trucos 234


Python juega con inmutables Resumen del capítulo Lecturas 236

adicionales 237
239
240
242
243

9. Un objeto pitónico. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247


Representaciones de objetos 248
Vector Class Redux Un 248
constructor alternativo classmethod 251
Versus staticmethod Pantallas formateadas 252

Un Hashable Vector2d 253


257

Atributos privados y "protegidos" en Python 262

Ahorro de espacio con el atributo de clase __slots__ 264


Los problemas con __slots__ 267

Anulación de atributos de clase 267

Resumen del capítulo 269

Otras lecturas 271

10. Secuencia Hacking, Hashing y Slicing. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275


Vector: un tipo de secuencia definida por el usuario 276
Vector Take #1: Protocolos compatibles con 276

Vector2d y Duck Typing Vector Take #2: Una 279

secuencia rebanable 280

Cómo funciona el corte 281

Un __getitem__ de Slice-Aware 283

Vector Take #3: Acceso a atributos dinámicos 284

Tabla de contenido | ix
Machine Translated by Google

Vector Take #4: Hashing y un == más rápido 288


Vector Take #5: Formateo 294
Resumen del capítulo 301
Otras lecturas 302

11. Interfaces: De Protocolos a ABCs. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 307


Interfaces y protocolos en la cultura Python Python 308
Digs Sequences Monkey-Patching para implementar 310
un protocolo en tiempo de ejecución Waterfowl de Alex Martelli 312
Subclasificación de un ABC ABC en la biblioteca estándar ABC en 314
collections.abc The Numbers Tower of ABC 319
321
321
323
Definición y uso de un ABC 324
Detalles de sintaxis ABC 328
Subclasificación de la tómbola ABC 329
Una subclase virtual de Tombola 332
Cómo se probaron las subclases de tómbola 335
Uso del registro en la práctica 338
Los gansos pueden comportarse como patos 338
Resumen del capítulo 340
Otras lecturas 342

12. Herencia: para bien o para mal. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347


Crear subclases de tipos integrados es 348
complicado Herencia múltiple y resolución de métodos Ordenar 351
la herencia múltiple en el mundo real Lidiar con la herencia 356
múltiple 358
1. Distinguir la herencia de interfaz de la herencia de implementación 2. Hacer que las 359
interfaces sean explícitas con ABC 3. Usar mixins para la reutilización de código 359
359
4. Hacer que los Mixins sean explícitos al 359
nombrarlos 5. Un ABC también puede ser un Mixin; Lo contrario no es 360
cierto 6. No haga subclases de más de una clase concreta 7. Proporcione 360
clases agregadas a los usuarios 8. "Favorecer la composición de 360
objetos sobre la herencia de clases". 361
Tkinter: lo bueno, lo malo y lo feo Un ejemplo 361
moderno: Mixins en Django Vistas genéricas Resumen del capítulo 362
Lecturas adicionales 366
367

x | Tabla de contenido
Machine Translated by Google

13. Sobrecarga del operador: hacerlo bien. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371


Sobrecarga del operador 101 372
Operadores unarios 372
Sobrecarga + para suma de vectores 375
Sobrecarga * para multiplicación escalar 380
Operadores de comparación enriquecidos 384
Operadores de asignación aumentada 388
Resumen del capítulo 392
Otras lecturas 393

Parte V. Flujo de control

14. Iterables, iteradores y generadores. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 401


Toma de oración #1: Una secuencia de palabras 402
Por qué las secuencias son iterables: la función iter 404
Iterables versus iteradores 405
Toma de oración #2: un iterador clásico 409
Hacer de la oración un iterador: Mala idea 411
Oración Toma n.° 3: Una función generadora 412
Cómo funciona una función generadora Oración 413
Toma n.° 4: Una implementación perezosa Oración 416
Toma n.° 5: Una expresión generadora Expresiones 417
generadoras: Cuándo usarlas Otro ejemplo: 419
Progresión aritmética Aritmética generadora Progresión con 420
itertools Funciones de generador en la biblioteca estándar 423
Nueva sintaxis en Python 3.3: rendimiento de funciones de 424
reducción iterables Una mirada más cercana a la función iter 433
Estudio de caso: Generadores en una base de datos Utilidad de 434
conversión Generadores como rutinas 436
437
439
Resumen del capítulo 439
Otras lecturas 440

15. Administradores de contexto y otros bloques. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 447


..
Haz esto, luego aquello: else Bloquea más allá if 448
Gestores de Contexto y con Bloques 450
Las utilidades contextlib 454
Usando @contextmanager 455
Resumen del capítulo 459
Otras lecturas 459

Tabla de contenido | xi
Machine Translated by Google

16. Corrutinas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 463


Cómo evolucionaron las corrutinas a partir de los generadores 464
Comportamiento básico de un generador utilizado como rutina 465

Ejemplo: rutina para calcular un promedio móvil 468

Decoradores para Coroutine Priming 469

Terminación de rutinas y manejo de excepciones 471

Devolver un valor de una rutina 475

Usando el rendimiento de 477

El significado de rendimiento de 483


Caso de uso: corrutinas para simulación de eventos discretos 489
Acerca de las simulaciones de eventos discretos 489
La simulación de la flota de taxis 490

Resumen del capítulo 498

Otras lecturas 500

17. Concurrencia con Futuros. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 505


Ejemplo: Descargas web en tres estilos Un script de 505

descarga secuencial Descarga con 507

concurrent.futures ¿Dónde están los futuros? 509


511

Bloqueo de E/S y GIL 515

Lanzamiento de procesos con concurrent.futures 515

Experimentación con Executor.map Descargas con 517

visualización de progreso y manejo de errores Manejo de errores en 520

flags2 Ejemplos Uso de futures.as_completed Subprocesos y 525

alternativas de multiprocesamiento Resumen del capítulo Lecturas 527

adicionales 530
530
531

18. Concurrencia con asyncio. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 537


Thread Versus Coroutine: una comparación 539

asyncio.Future: Nonblocking by Design Yield from 545

Futures, Tasks y Coroutines 546

Descarga con asyncio y aiohttp Ejecución Dando 548

vueltas Bloqueo de llamadas Mejora de la secuencia 552

de comandos del descargador de asyncio Uso de 554

asyncio.as_completed Uso de un ejecutor para 555

evitar el bloqueo del bucle de eventos Desde devoluciones de 560


llamada hasta futuros y rutinas 562

Hacer múltiples solicitudes para cada descarga 564

Escribir servidores asyncio 567

xi | Tabla de contenido
Machine Translated by Google

Un servidor TCP asyncio 568

Un servidor web aiohttp 573

Clientes más inteligentes para una mejor concurrencia 576

Resumen del capítulo 577

Otras lecturas 579

Parte VI. Metaprogramación

19. Atributos y propiedades dinámicas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 585


Gestión de datos con atributos dinámicos 586

Exploración de datos similares a JSON con atributos dinámicos 588


El problema del nombre de atributo no válido 591

Creación flexible de objetos con __new__ 592

Reestructuración del OSCON Feed con estantería 594

Recuperación de registros vinculados con propiedades 598

Uso de una propiedad para la validación de atributos 604


LineItem Take #1: Clase para un artículo en un pedido 604

LineItem Take #2: una propiedad de validación 605

Una mirada adecuada a las propiedades 606

Las propiedades anulan los atributos de la instancia 608

Documentación de la propiedad 610

Codificación de una fábrica de propiedades 611

Gestión de la eliminación de atributos 614

Atributos y funciones esenciales para el manejo de atributos 616

Atributos especiales que afectan el manejo de atributos 616

Funciones integradas para el manejo de atributos 616

Métodos especiales para el manejo de atributos 617

Resumen del capítulo 619

Otras lecturas 619

20. Descriptores de atributos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 625


Ejemplo de descriptor: validación de atributos 625

LineItem Take #3: Un descriptor simple 626

LineItem Take #4: Nombres de atributos de almacenamiento automático 631

LineItem Take #5: Un nuevo tipo de descriptor 637

Descriptores anulados frente a no anulados 640

Descriptor superior 642

Descriptor anulado sin __get__ 643

Descriptor no predominante 644

Sobrescribir un descriptor en la clase 645

Los métodos son descriptores 646

Tabla de contenido | XIII


Machine Translated by Google

Sugerencias de uso de descriptores 648

Cadena de documentación del descriptor y eliminación anulada 650

Resumen del capítulo 651

Otras lecturas 651

21. Metaprogramación de clases. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 655


Una fábrica de 656

clases Un decorador de clases para personalizar descriptores 659

Qué sucede cuando: tiempo de importación versus tiempo de 661


ejecución Los ejercicios de tiempo de evaluación 662
Metaclases 101 666
El ejercicio del tiempo de evaluación de la metaclase 669

Una metaclase para personalizar descriptores 673

El método especial Metaclass __prepare__ 675

Clases como objetos 677

Resumen del capítulo 678

Otras lecturas 679

Epílogo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 683

A. Guiones de soporte. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 687

Jerga de Python. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 715

Índice. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 725

xiv | Tabla de contenido


Machine Translated by Google

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

1. Mensaje al grupo de Usenet comp.lang.python, 23 de diciembre de 2002: “Acrimony in clp”

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.

Para quien es este libro


Este libro fue escrito para programadores en práctica de Python que quieren volverse competentes en
Python 3. Si conoce Python 2 pero está dispuesto a migrar a Python 3.4 o posterior, debería estar bien.
En el momento de escribir este artículo, la mayoría de los programadores profesionales de Python
utilizan Python 2, por lo que tuve especial cuidado en resaltar las características de Python 3 que
pueden ser nuevas para esa audiencia.

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.

Para quién no es este libro


Si recién está aprendiendo Python, este libro será difícil de seguir. No solo eso, si lo lee demasiado
pronto en su viaje de Python, puede darle la impresión de que cada secuencia de comandos de Python
debe aprovechar métodos especiales y trucos de metaprogramación. La abstracción prematura es tan
mala como la optimización prematura.

Cómo está organizado este libro


La audiencia principal de este libro no debería tener problemas para saltar directamente a cualquier
capítulo de este libro. Sin embargo, cada una de las seis partes forma un libro dentro del libro. Concebí
los capítulos dentro de cada parte para ser leídos en secuencia.

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.

Estos son los temas principales de cada parte del libro:


Parte I
Un solo capítulo sobre el modelo de datos de Python que explica cómo los métodos especiales
(por ejemplo, __repr__) son la clave para el comportamiento consistente de objetos de todo tipo,
en un lenguaje admirado por su consistencia. Comprender varias facetas del modelo de datos es
el tema de la mayor parte del resto del libro, pero el Capítulo 1 proporciona una descripción general
de alto nivel.

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

Hardware utilizado para tiempos


El libro tiene algunos puntos de referencia y tiempos simples. Esas pruebas se realizaron en una u otra
computadora portátil que usé para escribir el libro: una MacBook Pro de 13” de 2011 con una CPU Intel Core
i7 de 2,7 GHz, 8 GB de RAM y un disco duro giratorio, y una MacBook Air de 13” de 2014 con una CPU Intel
Core i5 de 1,4 GHz, 4 GB de RAM y un disco de estado sólido. El MacBook Air tiene una CPU más lenta y
menos RAM, pero su RAM es más rápida (1600 frente a 1333 MHz) y el SSD es mucho más rápido que el
HD. En el uso diario, no puedo decir qué máquina es más rápida.

Soapbox: mi perspectiva personal


He estado usando, enseñando y debatiendo Python desde 1998, y disfruto estudiando y comparando
lenguajes de programación, su diseño y la teoría detrás de ellos. Al final de algunos capítulos, agregué
barras laterales "Soapbox" con mi propia perspectiva sobre Python y otros lenguajes. Siéntase libre de omitir
estos si no está interesado en tales discusiones.
Su contenido es completamente opcional.

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.

Versión de Python cubierta


Probé todo el código del libro usando Python 3.4, es decir, CPython 3.4, la implementación de Python más
popular escrita en C. Solo hay una excepción: “El nuevo operador @ Infix en Python 3.5” en la página 383
muestra el operador @ , que solo es compatible con Python 3.5.

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

Las convenciones usadas en este libro

En este libro se utilizan las siguientes convenciones tipográficas:

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.

Negrita de ancho constante

Muestra comandos u otro texto que el usuario debe escribir literalmente.

Cursiva de ancho constante Muestra

el texto que se debe reemplazar con valores proporcionados por el usuario o por valores determinados por el contexto.

Este elemento significa un consejo o sugerencia.

Este elemento significa una nota general.

Este elemento indica una advertencia o precaución.

Uso de ejemplos de código


Todos los scripts y la mayoría de los fragmentos de código que aparecen en el libro están disponibles en el repositorio de
código de Fluent Python en GitHub.

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.”

Safari® Libros en línea


Safari Books Online es una biblioteca digital a pedido que ofrece
contenido experto en forma de libro y video de los principales
autores del mundo en tecnología y negocios.

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:

O'Reilly Media, Inc.


1005 Gravenstein Highway North
Sebastopol, CA 95472 800-998-9938
(en los Estados Unidos o Canadá) 707-829-0515
(internacional o local) 707-829-0104 (fax)

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

Encuéntrenos en Facebook: http:// facebook.com/ oreilly

Síganos en Twitter: http:// twitter.com/ oreillymedia Véanos en

YouTube: http:// www.youtube.com/ oreillymedia

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

La maravillosa comunidad brasileña de Python es conocedora, generosa y divertida. El grupo Python


Brasil tiene miles de personas y nuestras conferencias nacionales reúnen a cientos, pero los más
influyentes en mi camino como Pythonista fueron Leonardo Rochael, Adriano Petrich, Daniel
Vainsencher, Rodrigo RBP Pimentel, Bruno Gola, Leonardo Santagada, Jean Ferri , Rodrigo Senra, JS
Bueno, David Kwast, Luiz Irber, Osvaldo Santana, Fernando Masanori, Henrique Bastos, Gustavo
Niemayer, Pedro Werneck, Gustavo Barbieri, Lalo Martins, Danilo Bellini, and Pedro Kroger.

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.

Gracias a todos, por todo.

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 modelo de datos de Python

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

• Creación y destrucción de objetos •

Representación y formato de cadenas •

Contextos administrados (es decir, con bloques)

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

Una baraja de cartas pitónica

El siguiente es un ejemplo muy simple, pero demuestra el poder de implementar solo dos métodos
especiales, __getitem__ y __len__.

El ejemplo 1-1 es una clase para representar una baraja de cartas.

Ejemplo 1-1. Una baraja como secuencia de cartas


importar colecciones

2. Consulte “Atributos privados y “protegidos” en Python” en la página 262.

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

4 | Capítulo 1: El modelo de datos de Python


Machine Translated by Google

Carta = colecciones.namedtuple('Carta', ['rango', 'palo'])

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)

def __getitem__(self, posición): return


self._cards[posición]

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:

>>> tarjeta_cerveza = Tarjeta('7', 'diamantes') >>>


tarjeta_cerveza
Carta(rango='7', palo='diamantes')

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:

>>> mazo = mazo francés()


>>> len(mazo) 52

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:

>>> de elección de importación


aleatoria >>> elección (mazo)
Carta(rango='3', palo='corazones') >>>
elección(mazo)
Carta(rango='K', palo='picas')

Una baraja de cartas pitónica | 5


Machine Translated by Google

>>> 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é?”).

• Es más fácil beneficiarse de la rica biblioteca estándar de Python y evitar reinventarse


la rueda, como la función random.choice .

Pero se pone mejor.

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:

>>> para carta en mazo: # doctest: +ELIPSIS ...


print(carta)
Carta(rango='2', palo='picas')
Carta(rango='3', palo='picas')
Carta(rango='4', palo='picas')
...

El mazo también se puede iterar a la inversa:

>>> para carta invertida(baraja): # doctest: +ELIPSIS ... print(carta)

Carta(rango='A', palo='corazones')
Carta(rango='K', palo='corazones')
Carta(rango='Q', palo='corazones')
...

6 | Capítulo 1: El modelo de datos de Python


Machine Translated by Google

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.

La iteración es a menudo implícita. Si una colección no tiene un método __contains__ , el operador in


realiza un escaneo secuencial. Caso en cuestión: in funciona con nuestra clase FrenchDeck porque es
iterable. Échale un vistazo:

>>> Carta('Q', 'corazones') en mazo


Verdadero

>>> Carta('7', 'bestias') en mazo


Falso

¿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:

valores_del_palo = dict(picas=3, corazones=2, diamantes=1, tréboles=0)

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:

>>> for card in sorted(deck, key=spades_high): # doctest: +ELLIPSIS print(card)


...
Carta(rango='2', palo='tréboles')
Carta(rango='2', palo='diamantes')
Carta(rango='2', palo='corazones') ...
(se omiten 46 cartas)
Carta(rango='A', palo='diamantes')
Carta(rango='A', palo='corazones')
Carta(rango='A', palo='picas')
4
Aunque FrenchDeck hereda implícitamente del objeto, proviene su funcionalidad no se hereda,
del aprovechamiento del modelo de datos y la composición. Al implementar los métodos especiales
__len__ y __getitem__, nuestro FrenchDeck se comporta como una secuencia estándar de Python, lo
que le permite beneficiarse de las funciones básicas del lenguaje (por ejemplo, iteración y división).

4. En Python 2, tendría que ser explícito y escribir FrenchDeck(objeto), pero ese es el valor predeterminado en Python 3.

Una baraja de cartas pitónica | 7


Machine Translated by Google

y de la biblioteca estándar, como se muestra en los ejemplos que usan random.choice,


invertida y ordenada. Gracias a la composición, las implementaciones __len__ y
__getitem__ pueden transferir todo el trabajo a un objeto de lista , self._cards.

¿Qué hay de barajar?


Como se implementó hasta ahora, un FrenchDeck no se puede
barajar, porque es inmutable: las cartas y sus posiciones no se
pueden cambiar, excepto violando la encapsulación y manejando
el atributo _cards directamente. En el Capítulo 11, eso se solucionará
agregando un método __setitem__ de una línea.

Cómo se utilizan los métodos especiales

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.

8 | Capítulo 1: El modelo de datos de Python


Machine Translated by Google

Emulación de tipos numéricos Varios

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

El tipo complejo incorporado se puede usar para representar vectores


bidimensionales, pero nuestra clase se puede extender para representar
n vectores dimensionales. Lo haremos en el Capítulo 14.

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:

>>> v1 = Vector(2, 4) >>>


v2 = Vector(2, 1)
>>> v1 + v2
Vector(4, 5)

Observe cómo el operador + produce un resultado vectorial , que se muestra de manera amigable en la consola.

Cómo se utilizan los métodos especiales | 9


Machine Translated by Google

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:

>>> v = Vector(3, 4) >>>


abs(v)
5.0

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__.

Ejemplo 1-2. Una clase vectorial bidimensional simple


de hipot de importación matemática

clase vectorial:

def __init__(self, x=0, y=0): self.x = x


self.y = y

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

def __add__(uno mismo, otro):


x = uno mismo.x + otro.x
y = self.y + other.y return
Vector(x, y)

def __mul__(uno mismo, escalar):


devuelve Vector(self.x * escalar, self.y * escalar)

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

10 | Capítulo 1: El modelo de datos de Python


Machine Translated by Google

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 .

Hablando del operador % y el método str.format , notará que uso ambos


en este libro, al igual que la comunidad de Python en general. Estoy
cada vez más a favor del formato str., que es más poderoso , pero soy
consciente de que muchos Pythonistas prefieren el % más simple, por lo
que probablemente veremos ambos en el código fuente de Python en el
futuro previsible.

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.

“Diferencia entre __str__ y __repr__ en Python” es una pregunta de Stack


Overflow con excelentes contribuciones de los pitonistas Alex Martelli y
Martijn Pieters.

Cómo se utilizan los métodos especiales | 11


Machine Translated by Google

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.

Tal como se implementó, el ejemplo 1-2 permite multiplicar un vector por un


número, pero no un número por un vector, lo que viola la propiedad
conmutativa de la multiplicación. Arreglaremos eso con el método especial
__rmul__ en el Capítulo 13.

Valor booleano de un tipo personalizado

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.

Nuestra implementación de __bool__ es conceptualmente simple: devuelve False si la magnitud del


vector es cero, True en caso contrario. Convertimos la magnitud a un booleano usando bool(abs(self))
porque se espera que __bool__ devuelva un booleano.

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 .

Una implementación más rápida de Vector.__bool__ es esta:


def __bool__(self):
return bool(self.x o self.y)

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.

12 | Capítulo 1: El modelo de datos de Python


Machine Translated by Google

Descripción general de los métodos especiales

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.

La agrupación que se muestra en las siguientes tablas no es exactamente la misma


como en la documentación oficial.

Tabla 1-1. Nombres de métodos especiales (operadores excluidos)

Categoría nombres de métodos

Representación de cadena/bytes __repr__, __str__, __formato__, __bytes__


Conversión a número
__abs__, __bool__, __complejo__, __int__, __float__, __hash__,
__índice__

Emulando colecciones __len__, __getitem__, __setitem__, __delitem__, __contains__


Iteración
__iter__, __invertido__, __siguiente__

Emulación de llamadas __llamar__

Gestión de contexto __entrar__, __salir__

Creación y destrucción de instancias __new__ , __init__, __del__

Gestión de atributos __getattr__, __getattribute__, __setattr__, __delattr__, __dir__

Descriptores de atributos __obtener__, __establecer__, __eliminar__

Servicios de clase
__preparar__, __instanciaverificar__, __subclaseverificar__

Tabla 1-2. Nombres de métodos especiales para operadores

Categoría Nombres de métodos y operadores relacionados

Operadores numéricos unarios __neg__ -, __pos__ +, __abs__ abs()

Operadores de comparación enriquecidos __lt__ >, __le__ <=, __eq__ ==, __ne__ !=, __gt__ >, __ge__ >=

Operadores aritméticos __add__+, __sub__ -, __mul__ *, __truediv__ /, __floordiv__ //, __mod__


**
%, __divmod__ divmod() , __pow__ o poder(), __redondo__ redondo()

Operadores aritméticos inversos __radd__,__rsub__,__rmul__,__rtruediv__,__rfloordiv__,__rmod__,

__rdivmod__, __rpow__

Asignación aumentada __iadd__,__isub__,__imul__,__itruediv__,__ifloordiv__,__imod__,


operadores aritméticos __ipow__

Operadores bit a bit __invertir__ ~, __lshift__ <<, __rshift__ >>, __y__ &, __o__ |,
^
__xor__

Descripción general de los métodos especiales | 13


Machine Translated by Google

Categoría Nombres de métodos y operadores relacionados

Operadores bit a bit invertidos __rlshift__, __rrshift__, __rand__, __rxor__, __ror__


Operadores bit a bit de asignación __ilshift__, __irshift__, __iand__, __ixor__, __ior__
aumentada

Los operadores invertidos son alternativas que se utilizan cuando se


un en lugar
intercambian operandos (b *de un mientras
* b), que
son atajos lascombinan
que asignaciones aumentadas
un operador
infijo con una asignación variable (a = a * b se convierte en *= b). El Capítulo
13 explica en detalle tanto los operadores invertidos como la asignación
aumentada.

Por qué len no es un método


Le hice esta pregunta al desarrollador central Raymond Hettinger en 2013 y la clave de su respuesta
fue una cita de The Zen of Python: "la practicidad supera a la pureza". En “Cómo se usan los métodos
especiales” en la página 8, describí cómo len(x) se ejecuta muy rápido cuando x es una instancia de un
tipo integrado. No se llama a ningún método para los objetos integrados de CPython: la longitud
simplemente se lee de un campo en una estructura C. Obtener la cantidad de elementos en una
colección es una operación común y debe funcionar de manera eficiente para tipos tan básicos y
diversos como str, list, memoryview, etc.

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".

Si piensa en abs y len como operadores unarios, es posible que se sienta


más inclinado a perdonar su apariencia funcional, a diferencia de la sintaxis
de llamada de método que uno podría esperar en un lenguaje OO. De
hecho, el lenguaje ABC, un ancestro directo de Python que fue pionero en
muchas de sus características, tenía un operador # que era el equivalente
de len (usted escribiría #s). Cuando se usa como operador infijo, escrito
x#s, contaba las ocurrencias de x en s, que en Python obtienes como
s.count(x), para cualquier secuencia s.

Resumen del capítulo


Al implementar métodos especiales, sus objetos pueden comportarse como los tipos integrados, lo que
permite el estilo de codificación expresivo que la comunidad considera Pythonic.

14 | Capítulo 1: El modelo de datos de Python


Machine Translated by Google

Un requisito básico para un objeto de Python es proporcionar representaciones de cadenas utilizables


de sí mismo, una utilizada para la depuración y el registro, otra para la presentación a los usuarios
finales. Por eso existen los métodos especiales __repr__ y __str__ en el modelo de datos.

La emulación de secuencias, como se muestra en el ejemplo de FrenchDeck , es una de las


aplicaciones más utilizadas de los métodos especiales. Aprovechar al máximo los tipos de secuencias
es el tema del Capítulo 2, y la implementación de su propia secuencia se tratará en el Capítulo 2.
ter 10 cuando creamos una extensión multidimensional de la clase Vector .

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

¿Modelo de datos o modelo de objetos?

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

siempre se refieren a él como el "modelo de objeto". En Wikipedia, la primera definición de modelo de


objetos es "Las propiedades de los objetos en general en un lenguaje de programación de computadora
específico". De esto se trata el “modelo de datos de Python”. En este libro, usaré "modelo de datos"
porque la documentación favorece ese término cuando se refiere al modelo de objetos de Python, y
porque es el título del capítulo de The Python Language Reference más relevante para nuestras
discusiones.

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.

16 | Capítulo 1: El modelo de datos de Python


Machine Translated by Google

PARTE II

Estructuras de datos
Machine Translated by Google
Machine Translated by Google

CAPITULO 2

Una matriz de secuencias

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

— Geurts, Meertens y Pemberton


Manual del programador ABC

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.

Comprender la variedad de secuencias disponibles en Python nos evita reinventar la rueda, y su


interfaz común nos inspira a crear API que admitan y aprovechen adecuadamente los tipos de
secuencia existentes y futuros.

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

Descripción general de las secuencias integradas

La biblioteca estándar ofrece una amplia selección de tipos de secuencias implementadas en C: la

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.

Otra forma de agrupar tipos de secuencias es por mutabilidad:

lista de secuencias mutables, bytearray, array.array,


collections.deque y memoryview

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.

20 | Capítulo 2: Una matriz de secuencias


Machine Translated by Google

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.

Comprensiones de listas y expresiones generadoras


Una forma rápida de construir una secuencia es usar una lista por comprensión (si el objetivo es una lista) o una
expresión generadora (para todos los demás tipos de secuencias). Si no usa estas formas sintácticas a diario,
apuesto a que está perdiendo oportunidades para escribir código que sea más legible y, a menudo, más rápido al
mismo tiempo.

Si duda de mi afirmación de que estas construcciones son "más legibles", siga leyendo. Intentaré convencerte.

Por razones de brevedad, muchos programadores de Python se refieren a las listas


por comprensión como listcomps y a las expresiones generadoras como genexs.
Usaré estas palabras también.

Lista de comprensión y legibilidad Aquí hay una prueba:

¿cuál encuentra más fácil de leer, el ejemplo 2-1 o el ejemplo 2-2?

Ejemplo 2-1. Cree una lista de puntos de código Unicode a partir de una cadena

>>> símbolos = '$¢£¥€¤'


>>> códigos = [] >>> for
símbolo en símbolos:
... códigos.append(ord(símbolo))
...
>>> codigos
[36, 162, 163, 165, 8364, 164]

Ejemplo 2-2. Cree una lista de puntos de código Unicode a partir de una cadena, tome dos

>>> símbolos = '$¢£¥€¤'


>>> códigos = [ord(símbolo) for símbolo en símbolos] >>>
códigos [36, 162, 163, 165, 8364, 164]

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.

Comprensiones de listas y expresiones generadoras | 21


Machine Translated by Google

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 \ .

Los listcomps ya no filtran sus variables


En Python 2.x, las variables asignadas en las cláusulas for en listas de comprensión se
establecieron en el ámbito circundante, a veces con consecuencias trágicas. Consulte la siguiente
sesión de consola de Python 2.7:

Python 2.7.6 (predeterminado, 22 de marzo de 2014, 22:59:38)


[GCC 4.8.2] en linux2 Escriba
"ayuda", "derechos de autor", "créditos" o "licencia" para obtener más información. >>> x = 'mi
precioso' >>> dummy = [x por x en 'ABC']

>>> 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í

22 | Capítulo 2: Una matriz de secuencias


Machine Translated by Google

[65, 66, 67]


>>>

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.

Listcomps Versus mapear y filtrar Los Listcomps

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

>>> símbolos = '$¢£¥€¤' >>>


más allá_ascii = [ord(s) for s en símbolos si ord(s) > 127] >>> más allá_ascii
[162, 163, 165, 8364, 164] >>> más allá_ascii = lista(filtro(lambda c: c > 127,
map(ord, símbolos))) >>> más allá_ascii [162, 163, 165, 8364, 164]

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.

Comprensiones de listas y expresiones generadoras | 23


Machine Translated by Google

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.

Ejemplo 2-4. Producto cartesiano usando una lista por comprensión

>>> colores = ['negro', 'blanco'] >>>


tallas = ['S', 'M', 'L'] >>> camisetas =
[(color, talla) para color en colores para talla en tallas] >>> camisetas [('negro',
'S'), ('negro', 'M'), ('negro', 'L'), ('blanco', 'S'), ('blanco ', 'M'), ('blanco', 'L')] >>> para
color en colores: para tamaño en tamaños:

...
... 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')]

24 | Capítulo 2: Una matriz de secuencias


Machine Translated by Google

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:

self._cards = [Carta(rango, palo) para palo en self.palos para rango


en self.ranks]

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.

Expresiones generadoras Para

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

>>> símbolos = '$¢£¥€¤'


>>> tupla(ord(símbolo) for símbolo en símbolos) (36,
162, 163, 165, 8364, 164) >>> import array >>> array.
array('I', (ord(símbolo) para símbolo en símbolos))
array('I', [36, 162, 163, 165, 8364, 164])

Si la expresión del generador es el único argumento en una llamada de función, no es necesario


duplicar los paréntesis que la encierran.

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

Comprensiones de listas y expresiones generadoras | 25


Machine Translated by Google

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 .

Ejemplo 2-6. Producto cartesiano en una expresión generadora


>>> colores = ['negro', 'blanco'] >>>
tallas = ['S', 'M', 'L'] >>> para camiseta
en ('%s %s' % (c, s ) para c en colores para s en tallas): estampado (camiseta)
...
...
negro S
negro M
negro L
blanco S
blanco M
blanco L

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.

Ahora pasamos al otro tipo de secuencia fundamental en Python: la tupla.

Las tuplas no son solo listas inmutables


Algunos textos introductorios sobre Python presentan las tuplas como “listas inmutables”, pero eso es
venderlas en corto. Las tuplas cumplen una doble función: se pueden usar como listas inmutables y
también como registros sin nombres de campo. Este uso a veces se pasa por alto, por lo que
comenzaremos con eso.

Las tuplas como

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.

26 | Capítulo 2: Una matriz de secuencias


Machine Translated by Google

Ejemplo 2-7. Tuplas utilizadas como registros

>>> lax_coordinates = (33.9425, -118.408056) >>>


ciudad, año, población , cambio, área = ('Tokyo', 2003, 32450, 0.66, 8014) >>>
traveler_ids = [('USA', '31195855 '), ('BRA', 'CE342567'),
... ('ESP', 'XDA205856')]
>>> para pasaporte en sorted(traveler_ids):
... print('%s/%s' % pasaporte)
...
SUJETADOR/CE342567

ESP/XDA205856
EE. UU./31195855
>>> para país, _ en traveler_ids: print(país)
...
...
EE.UU

SOSTÉN

ESP

Latitud y longitud del Aeropuerto Internacional de Los Ángeles.

Datos sobre Tokio: nombre, año, población (millones), cambio de población (%),
superficie (km²).

Una lista de tuplas de la forma (código_país, número_pasaporte).

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

cada elemento de la tupla de pasaporte a un espacio en la cadena de formato en el argumento de impresión .


Esos son dos ejemplos de desempaquetado de tuplas.

Las tuplas no son solo listas inmutables| 27


Machine Translated by Google

El desempaquetado de tuplas funciona con cualquier objeto iterable. El único


requisito es que el iterable genere exactamente un elemento por variable en la tupla
receptora, a menos que use una estrella (*) para capturar elementos en exceso,
como se explica en "Uso de El
* para
término
capturar
tupla elementos
El desempaquetado
en exceso"esenampliamente
la página 29.
utilizado por Pythonistas, pero el desempaquetado iterable está ganando terreno,
como en el título de PEP 3132: Desempaquetado iterable extendido.

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:

>>> lax_coordinates = (33.9425, -118.408056) >>> latitud,


longitud = lax_coordinates # tupla descomprimiendo >>> latitud

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:

>>> divmod(20, 8) (2,


4) >>> t = (20, 8) >>>
divmod(*t) (2, 4) >>>
cociente, resto =
divmod(*t) > >>
cociente, resto (2, 4)

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.

28 | Capítulo 2: Una matriz de secuencias


Machine Translated by Google

Si escribe software internacionalizado, no es _una buena variable ficticia


porque se usa tradicionalmente como un alias para la función get
text.gettext , como se recomienda en la documentación del módulo
gettext . De lo contrario, es un buen nombre para la variable marcadora
de posición.

Otra forma de enfocarse solo en algunos de los elementos al desempaquetar una tupla es usar el *, como veremos
enseguida.

El uso * para agarrar el exceso de artículos


de parámetros de función de definición con *args para capturar argumentos arbitrarios en exceso es una característica
clásica de Python.

En Python 3, esta idea se amplió para aplicarse también a la asignación en paralelo:

>>> a, b, *resto = rango(5) >>>


a, b, resto (0, 1, [2, 3, 4]) >>>
a, b, *resto = rango(3) > >> a,
b, resto (0, 1, [2]) >>> a, b,
*resto = rango(2) >>> a, b, resto
(0, 1, [])

En el contexto de la asignación paralela, el prefijo * se puede aplicar exactamente a una variable, pero puede aparecer
en cualquier posición:

>>> a, *cuerpo, c, d = rango(5) >>>


a, cuerpo, c, d (0, [1, 2], 3, 4) >>>
*cabeza, b, c, d = rango(5) >>>
cabeza, b, c, d ([0, 1], 2, 3, 4)

Finalmente, una característica poderosa del desempaquetado de tuplas es que funciona con estructuras anidadas.

Desempaquetado de tupla anidada La

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.

Ejemplo 2-8. Desempaquetando tuplas anidadas para acceder a la longitud

á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)),

Las tuplas no son solo listas inmutables| 29


Machine Translated by Google

('Nueva York-Newark', 'EE. UU.', 20.104, (40.808611, -74.020386)), ('Sao


Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
]

print('{:15} | {:^9} | {:^9}'.format('', 'lat.', 'long.')) fmt = '{:15} | {:9.4f} |


{:9.4f}' para nombre, cc, pop, (latitud, longitud) en áreas_metro: # si
longitud <= 0: # print(fmt.format(nombre, latitud, longitud))

Cada tupla contiene un registro con cuatro campos, el último de los cuales es un par de coordenadas.

Al asignar el último campo a una tupla, desempaquetamos las coordenadas. si

longitud <= 0: limita la salida a las áreas metropolitanas del hemisferio occidental.

El resultado del Ejemplo 2-8 es:

| lat. | largo. | 19.4333 |


Nueva York-Newark -99.1333
| 40.8086Ciudad de México
| -74.0204 São
Paulo | -23.5478 | -46.6358

Antes de Python 3, era posible definir funciones con tuplas anidadas en


los parámetros formales (por ejemplo, def fn(a, (b, c), d):).
Esto ya no se admite en las definiciones de función de Python 3, por
razones prácticas explicadas en PEP 3113 — Eliminación del
desempaquetado de parámetros de tupla. Para ser claros: nada cambió
desde la perspectiva de los usuarios llamando a una función. La restricción
se aplica sólo a la definición de funciones.

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

Tuplas con nombre

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.

30 | Capítulo 2: Una matriz de secuencias


Machine Translated by Google

Recuerde cómo construimos la clase Tarjeta en el Ejemplo 1-1 en el Capítulo 1:

Carta = colecciones.namedtuple('Carta', ['rango', 'palo'])

El ejemplo 2-9 muestra cómo podríamos definir una tupla con nombre para contener información sobre una
ciudad.

Ejemplo 2-9. Definición y uso de un tipo de tupla con nombre

>>> from collections import namedtuple >>>


City = namedtuple('Ciudad', 'coordenadas de la población del país del nombre') >>>
tokio = Ciudad('Tokio', 'JP', 36.933, (35.689722, 139.691667)) >> > tokio
Ciudad(nombre='Tokio', país='JP', población=36.933, coordenadas=(35.689722,
139.691667)) >>> tokio.población 36.933 >>> tokio.coordenadas (35.689722, 139.691667) >>>
tokio [1]

'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).

Puede acceder a los campos por nombre o cargo.

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

OrderedDict([('nombre', 'Delhi NCR'), ('país', 'IN'), ('población', 21.935), ('coordenadas',


LatLong(lat=28.613889, long=77.208889))]) >>> para clave, valor en delhi._asdict().items():

imprimir (clave + ':', valor)

nombre: Delhi NCR


país: IN
población: 21.935

Las tuplas no son solo listas inmutables| 31


Machine Translated by Google

coordenadas: LatLong(lat=28.613889, long=77.208889)


>>>

_fields es una tupla con los nombres de campo de la clase.

_make() le permite instanciar una tupla con nombre a partir de un iterable; Ciudad(*del
hi_data) haría lo mismo.

_asdict() devuelve un collections.OrderedDict construido a partir de la tupla nombrada


instancia. Eso se puede usar para producir una buena visualización de los datos de la ciudad.

Ahora que hemos explorado el poder de las tuplas como registros, podemos considerar su segundo
rol como una variante inmutable del tipo de lista .

Tuplas como listas inmutables

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.__iadd__(s2) • s += s2: concatenación en el lugar

s.append(e) • Agregar un elemento después del último

claro() • Eliminar todos los elementos

s.__contiene__(e) •• e en s

s.copiar() • Copia superficial de la lista.

s.count(e) •• Contar las ocurrencias de un elemento

s.__delitem__(p) • Eliminar elemento en la posición p

s.extender (es) • Agregar elementos de iterable it

s.__getitem__(p) •• s[p]: obtiene el elemento en la posición

s.__getnewargs__() • Soporte para serialización optimizada con pickle

s.index(e) •• Encuentre la posición de la primera aparición de e

s.insertar(p, e) • Inserte el elemento e antes del elemento en la posición p

s.__iter__() •• Obtener iterador

s.__len__() •• len(s)—número de elementos

s.__mul__(n) •• s * n: concatenación repetida

32 | Capítulo 2: Una matriz de secuencias


Machine Translated by Google

tupla lista
s.__imul__(n) • s *= n: concatenación repetida en el lugar

s.__rmul__(n) •• norte
* s: concatenación repetida invertidaa

pop([p]) • Retire y devuelva el último artículo o el artículo en la posición opcional p

s.remove(e) • Eliminar la primera aparición del elemento e por valor

al revés() • Invertir el orden de los elementos en su lugar

s.__invertido__() • Obtener iterador para escanear elementos del último al primero

s.__setitem__(p, e) • s[p] = e: coloque e en la posición p, sobrescribiendo el elemento existente

s.sort([clave], [reversa]) • Ordene los elementos en su lugar con la clave de argumentos de palabras clave opcionales y revierta

a Los operadores invertidos se explican en el Capítulo 13.

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.

En esta sección, describimos el uso de estas formas avanzadas de rebanado. Su imple-


La mentación en una clase definida por el usuario se cubrirá en el Capítulo 10, de acuerdo con nuestra
filosofía de cubrir clases listas para usar en esta parte del libro, y crear nuevas
clases en la Parte IV.

Por qué las divisiones y el rango excluyen el último elemento

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:

>>> l = [10, 20, 30, 40, 50, 60]


>>> l[:2] # dividir en 2
[10, 20]
>>> l[2:]
[30, 40, 50, 60]

Rebanar | 33
Machine Translated by Google

>>> l[:3] # dividir en 3


[10, 20, 30]
>>> l[3:]
[40, 50, 60]

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

Ahora echemos un vistazo de cerca a cómo Python interpreta la notación de corte.

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.

Ejemplo 2-11. Elementos de línea de una factura de archivo plano

>>> factura = """


... 0.....6.................................40...... ..52...55........
... 1909 Pimoroni PiBrella ... 1489 $17.50 3 $52.50
Interruptor táctil de 6 mm x 20 ... 1510 Panavise $4.95 2 $9.90
Jr. - PV-201 ... 1601 Mini kit PiTFT 320x240 $28.00 1 $28.00
$34.95 1 $34.95
... """

34 | Capítulo 2: Una matriz de secuencias


Machine Translated by Google

>>> SKU = rebanada (0, 6)


>>> DESCRIPCION = rebanada(6, 40)
>>> PRECIO_UNITARIO = segmento(40, 52)
>>> CANTIDAD = rebanada (52, 55)
>>> TOTAL_ARTÍCULO = sector (55, Ninguno)
>>> artículos_línea = factura.split('\n')[2:] >>> for artículo
en artículos_línea:
... print(artículo[PRECIO_UNIDAD], artículo[DESCRIPCIÓN])
...
$17.50 Pimoroni PiBrella $4.95
Interruptor táctil de 6 mm x 20 $28.00
Panavise Jr. - PV-201 $34.95 Mini kit PiTFT
320x240

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

Cortes multidimensionales y puntos suspensivos El

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:

>>> l = lista(rango(10)) >>>


l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] >>> l[2:5]
= [20, 30] >>> l [0, 1, 20, 30, 5 , 6, 7,
8, 9] >>> del l[5:7] >>> l [0, 1, 20,
30, 5, 8, 9] >>> l[3::2] = [ 11, 22] >>>
l [0, 1, 20, 11, 5, 22, 9] >>> l[2:5] =
100

Rastreo (llamadas recientes más última):


Archivo "<stdin>", línea 1, en <módulo>
TypeError: solo puede asignar un iterable >>>
l[2:5] = [100] >>> l [0, 1, 100, 22, 9]

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.

Usar + y * con Secuencias


Los programadores de Python esperan que las secuencias admitan + y *. Por lo general, ambos
operandos de + deben ser del mismo tipo de secuencia, y ninguno de ellos se modifica, pero se crea
una nueva secuencia del mismo tipo como resultado de la concatenación.

Para concatenar varias copias de la misma secuencia, multiplíquela por un número entero. Nuevamente,
se crea una nueva secuencia:

>>> l = [1, 2, 3] >>>


l * 5 [1, 2, 3, 1, 2, 3,
1, 2, 3, 1, 2, 3, 1, 2, 3]

36 | Capítulo 2: Una matriz de secuencias


Machine Translated by Google

>>> 5 * 'abcd'
'abcdabcdabcdabcdabcd'

Tanto + como * siempre crean un nuevo objeto y nunca cambian sus operandos.

Tenga cuidado con expresiones como


* an cuando a es una secuencia que
contiene elementos mutables porque el resultado puede sorprenderlo. Por
ejemplo, intentar inicializar una lista de listas como my_list = [[]] * 3 dará
como resultado una lista con tres referencias a la misma lista interna, que
probablemente no sea lo que desea.

La siguiente sección cubre los peligros de tratar de usar * para inicializar una lista de listas.

Creación de listas de listas A

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

>>> tablero = [['_'] * 3 for i in range(3)] >>> tablero


[['_', '_', '_'], ['_', '_', ' _'], ['_', '_', '_']] >>> tablero[1]
[2] = 'X' >>> tablero

[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]

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.

Un atajo tentador pero incorrecto es hacerlo como el Ejemplo 2-13.

Ejemplo 2-13. Una lista con tres referencias a la misma lista es inútil

>>> tablero_raro = [['_'] * 3] * 3 >>>


tablero_raro [['_', '_', '_'], ['_', '_', '_'], [ '_',
'_', '_']] >>> tablero_raro[1][2] = 'O' >>> tablero_raro

[['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']]

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.

Usando + y * con Secuencias | 37


Machine Translated by Google

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)

La misma fila se anexa tres veces al tablero.

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 #

[['_', '_', '_'], ['_', '_', '_'], ['X', '_', '_']]

Cada iteración crea una nueva fila y la agrega al tablero.

Solo se cambia la fila 2, como se esperaba.

Si el problema o la solución en esta sección no le resultan claros,


relájese. El capítulo 8 se escribió para aclarar la mecánica y los
peligros de las referencias y los objetos mutables.

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.

Asignación aumentada con secuencias


Los operadores de asignación aumentada += y *= se comportan de manera muy diferente dependiendo
del primer operando. Para simplificar la discusión, nos centraremos primero en la suma aumentada (+=),
pero los conceptos también se aplican a *= y a otros operadores de asignación aumentada.

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

38 | Capítulo 2: Una matriz de secuencias


Machine Translated by Google

Si implementa __iadd__ , se llamará. En el caso de secuencias mutables (p. ej., list,


bytearray, array.array), a se cambiará de lugar (es decir, el efecto será similar a
a.extend(b)). Sin embargo, cuando a no implementa __iadd__, la expresión a += b
tiene el mismo efecto que a = a + b: la expresión a + b se evalúa primero, produciendo
un nuevo objeto, que luego se vincula a a. En otras palabras, la identidad del objeto
vinculado a un puede cambiar o no, dependiendo de la disponibilidad de __iadd__.

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 = [1, 2, 3] >>>


id(l) 4311953800

>>> l *= 2
>>> l [1, 2,
3, 1, 2, 3] >>> id(l)
4311953800

>>> t = (1, 2, 3) >>>


id(t) 4312681568

>>> t *= 2
>>> id(t)
4301348296
ID de la lista inicial

Después de la multiplicación, la lista es el mismo objeto, con nuevos elementos añadidos

ID de la tupla inicial

Después de la multiplicación, se creó una nueva tupla.

La concatenación repetida de secuencias inmutables es ineficiente, porque en lugar de agregar nuevos


elementos, el intérprete tiene que copiar toda la secuencia de destino para crear una nueva con los
nuevos elementos concatenados.3

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.

Asignación aumentada con secuencias | 39


Machine Translated by Google

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

Ejemplo 2-14. Un acertijo

>>> t = (1, 2, [30, 40]) >>>


t[2] += [50, 60]

¿Qué pasa después? Elige la mejor respuesta:

una. t se convierte en (1, 2, [30, 40, 50, 60]).

b. TypeError se genera con el mensaje 'tupla' el objeto no es compatible con el elemento


asignación.
C. Ninguno de los dos.

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

Ejemplo 2-15. El resultado inesperado: el elemento t2 se cambia y se genera una excepción

>>> t = (1, 2, [30, 40]) >>>


t[2] += [50, 60]
Rastreo (llamadas recientes más última):
Archivo "<stdin>", línea 1, en <módulo>
TypeError: el objeto 'tuple' no admite la asignación de elementos >>> t
(1, 2, [30, 40, 50, 60])

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 +=.

40 | Capítulo 2: Una matriz de secuencias


Machine Translated by Google

Figura 2-3. Estado inicial y final del rompecabezas de asignación de tuplas (diagrama generado
por el tutor de Python en línea)

Si observa el código de bytes que Python genera para la expresión s[a] += b


(Ejemplo 2-16), queda claro cómo sucede eso.

Ejemplo 2-16. Código de bytes para la expresión s[a] += b

>>> dis.dis('s[a] += b')


1 0 CARGA_NOMBRE 0
3 CARGA_NOMBRE 1 (un)
6 DUP_TOP_TWO
7 BINARY_SUBSCR
8 LOAD_NAME 11 2 (b)
INPLACE_ADD 12
ROT_THREE
13 STORE_SUBSCR
14 LOAD_CONST 17 0 (Ninguno)
RETURN_VALUE

Ponga el valor de s[a] en TOS (Top Of Stack).

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.

Saco tres lecciones de esto:

• Poner elementos mutables en tuplas no es una buena idea.

Asignación aumentada con secuencias | 41


Machine Translated by Google

• 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,

es útil para ver qué sucede debajo del capó.

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.

list.sort y la función incorporada ordenada


El método list.sort ordena una lista en su lugar, es decir, sin hacer una copia. Devuelve None para recordarnos
que cambia el objeto de destino y no crea una nueva lista. Esta es una convención importante de la API de
Python: las funciones o los métodos que cambian un objeto en su lugar deben devolver Ninguno para dejar en
claro a la persona que llama que el objeto en sí se cambió y que no se creó ningún objeto nuevo. El mismo
comportamiento se puede ver, por ejemplo, en la función random.shuffle .

La convención de devolver None para indicar cambios en el lugar tiene un


inconveniente: no puede conectar en cascada llamadas a esos métodos. Por el
contrario, los métodos que devuelven nuevos objetos (p. ej., todos los métodos str )
se pueden conectar en cascada en el estilo de interfaz fluida. Consulte la entrada
"Fluent interface" de Wikipedia en Wikipedia para obtener una descripción más detallada de este tema.

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

42 | Capítulo 2: Una matriz de secuencias


Machine Translated by Google

El parámetro de palabra clave opcional clave también se puede usar


con las funciones integradas min() y max() y con otras funciones de la
biblioteca estándar (por ejemplo, itertools.groupby() y heapq.nlargest()).

Aquí hay algunos ejemplos para aclarar el uso de estas funciones y argumentos de palabras clave6 :

>>> frutas = ['uva', 'frambuesa', 'manzana', 'plátano'] >>>


ordenado(frutas) ['manzana', 'plátano', 'uva', 'frambuesa'] >>>
frutas ['uva', 'frambuesa', 'manzana', 'banana'] >>> sorted(frutas,
inverso=Verdadero) ['frambuesa', 'uva', 'banana', 'manzana'] >>>
sorted( frutas, clave=len) ['uva', 'manzana', 'banana', 'frambuesa']
>>> sorted(frutas, clave=len, reversa=True) ['frambuesa', 'banana',
'uva' , 'manzana'] >>> frutas ['uva', 'frambuesa', 'manzana',
'plátano'] >>> frutas.clasificar() >>> frutas ['manzana', 'plátano',
'uva' , 'frambuesa']

Esto produce una nueva lista de cadenas ordenadas alfabéticamente.

Al inspeccionar la lista original, vemos que no ha cambiado.

Esto es simplemente un orden alfabético inverso.

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”.

Hasta ahora, el orden de la lista de frutas original no ha cambiado.

Esto ordena la lista en su lugar y devuelve Ninguno (que la consola omite).


Ahora las frutas están clasificadas.

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.

list.sort y la función integrada ordenada | 43


Machine Translated by Google

función bisect.insort , que puede usar para asegurarse de que sus secuencias ordenadas permanezcan
ordenadas.

Gestión de secuencias ordenadas con bisect


El módulo bisect ofrece dos funciones principales, bisect e insort, que utilizan el algoritmo de búsqueda binaria para

encontrar e insertar rápidamente elementos en cualquier secuencia ordenada.

Buscar con bisect bisect(pajar,

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.

Raymond Hettinger, un prolífico colaborador de Python, tiene una receta


de colección ordenada que aprovecha el módulo bisect pero es más fácil
de usar que estas funciones independientes.

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]

ROW_FMT = '{0:2d} @ {1:2d} {2}{0:<2d}'

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:

44 | Capítulo 2: Una matriz de secuencias


Machine Translated by Google

bisect_fn = bisect.bisect

print('DEMO:', bisect_fn.__name__)
print('pajar ->', ' '.join('%2d' % n for n en HAYSTACK)) demo(bisect_fn)

Utilice la función de bisección elegida para obtener el punto de inserción.

Construya un patrón de barras verticales proporcionales al desplazamiento.

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.

Imprimir encabezado con el nombre de la función seleccionada.

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

El comportamiento de bisect se puede ajustar de dos maneras.

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.

Gestión de secuencias ordenadas con bisect | 45


Machine Translated by Google

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

>>> def grado(puntaje, puntos de corte=[60, 70, 80, 90], grados='FDCBA'): i =


... bisect.bisect(puntos de corte, puntaje) return grados[i]
...
...
>>> [calificación(puntaje) para puntaje en [33, 99, 77, 70, 89, 90, 100]]
['F', 'A', 'C', 'C', 'B', 'A', 'A']

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.

46 | Capítulo 2: Una matriz de secuencias


Machine Translated by Google

Insertar con bisect.insort


Ordenar es caro, así que una vez que tienes una secuencia ordenada, es bueno mantenerla así.
Por eso se creó bisect.insort .

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.

Ejemplo 2-19. Insort mantiene una secuencia ordenada siempre ordenada

importar bisect
importación aleatoria

TAMAÑO = 7

semilla aleatoria (1729)

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)

Figura 2-6. Resultado del Ejemplo 2-19

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.

Gestión de secuencias ordenadas con bisect | 47


Machine Translated by Google

Cuando una lista no es la respuesta


El tipo de lista es flexible y fácil de usar, pero según los requisitos específicos, existen mejores opciones.
Por ejemplo, si necesita almacenar 10 millones de valores de punto flotante, una matriz es mucho más
eficiente, porque una matriz en realidad no contiene objetos flotantes completos , sino solo los bytes
empaquetados que representan sus valores de máquina, al igual que una matriz en el lenguaje C. Por
otro lado, si constantemente agrega y elimina elementos de los extremos de una lista como una
estructura de datos FIFO o LIFO, una deque (cola de dos extremos) funciona más rápido.

Si su código hace muchas comprobaciones de contención (por ejemplo,


elemento en mi_colección), considere usar un conjunto para mi_colección,
especialmente si contiene una gran cantidad de elementos. Los conjuntos están
optimizados para una verificación rápida de membresía. Pero no son secuencias
(su contenido está desordenado). Los cubrimos en el Capítulo 3.

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.

Ejemplo 2-20. Crear, guardar y cargar una gran variedad de flotantes

>>> from array import array


>>> from random import random
>>> floats = array('d', (random() for i in range(10**7))) >>> floats[-1]
0.07802343889111107 >>> fp = abrir('floats.bin', 'wb') >>>
floats.tofile(fp) >>> fp.close() >>> floats2 = array('d')

48 | Capítulo 2: Una matriz de secuencias


Machine Translated by Google

>>> fp = abrir('floats.bin', 'rb') >>>


floats2.fromfile(fp, 10**7) >>> fp.close()
>>> floats2[-1] 0.07802343889111107
>> > flotadores2 == flotadores

Verdadero

Importe el tipo de matriz .

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.

Inspeccione el último número en la matriz.

Guarde la matriz en un archivo binario.

Crea una matriz vacía de dobles.

Lea 10 millones de números del archivo binario.

Inspeccione el último número en la matriz.

Verifique que el contenido de las matrices coincida.

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.

Otra forma rápida y más flexible de guardar datos numéricos es el módulo


pickle para la serialización de objetos. Guardar una matriz de flotantes con
pickle.dump es casi tan rápido como con array.tofile; sin embargo , pickle
maneja casi todos los tipos integrados, incluidos números complejos ,
colecciones anidadas e incluso instancias de clases definidas por el usuario
automáticamente (si es necesario). no son demasiado complicados en su
implementación).

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.

Cuando una lista no es la respuesta | 49


Machine Translated by Google

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.__añadir__(s2) •• s + s2: concatenación


s.__iadd__(s2) •• s += s2: concatenación en el lugar
s.append(e) •• Agregar un elemento después del último

s.intercambio de bytes() • Intercambie bytes de todos los elementos en la matriz para la conversión de endianess

claro() • Eliminar todos los elementos

s.__contiene__(e) •• e en s

s.copiar() • Copia superficial de la lista.

s.__copia__() • Soporte para copiar.copiar

s.count(e) •• Contar las ocurrencias de un elemento

s.__copia profunda__() • Soporte optimizado para copy.deepcopy


s.__delitem__(p) •• Eliminar elemento en la posición p

s.extender (es) •• Agregar elementos de iterable it

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.__getitem__(p) •• s[p]: obtiene el elemento en la posición

s.index(e) •• Encuentre la posición de la primera aparición de e

s.insertar(p, e) •• Inserte el elemento e antes del elemento en la posición p

s.itemsize • Longitud en bytes de cada elemento de la matriz

s.__iter__() •• Obtener iterador

s.__len__() •• len(s)—número de elementos

s.__mul__(n) •• s * n: concatenación repetida

s.__imul__(n) •• s *= n: concatenación repetida en el lugar


s.__rmul__(n) •• norte

* s: concatenación repetida invertidaa


pop([p]) •• Eliminar y devolver el artículo en la posición p (predeterminado: último)

s.remove(e) •• Eliminar la primera aparición del elemento e por valor

al revés() •• Invertir el orden de los elementos en su lugar

s.__invertido__() • Obtener iterador para escanear elementos del último al primero

s.__setitem__(p, e) •• s[p] = e: coloque e en la posición p, sobrescribiendo el elemento existente

s.sort([clave], [reversa]) • Ordene los elementos en su lugar con la clave de argumentos de palabras clave opcionales y revierta

s.tobytes() • Devolver elementos como valores de máquina empaquetados en un objeto de bytes

en el archivo (f) • Guardar elementos como valores de máquina empaquetados en un archivo binario f

s.tolist() • Devolver elementos como objetos numéricos en una lista

50 | Capítulo 2: Una matriz de secuencias


Machine Translated by Google

matriz de lista


s.typecode Cadena de un carácter que identifica el tipo C de los elementos

a Los operadores invertidos se explican en el Capítulo 13.

A partir de Python 3.4, el tipo de matriz no tiene un método de clasificación


en el lugar como list.sort(). Si necesita ordenar una matriz, use la función
sorted para reconstruirla ordenada:

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:

Una vista de memoria es esencialmente una estructura de matriz NumPy generalizada en


Python (sin las matemáticas). Le permite compartir memoria entre estructuras de datos (cosas como
imágenes PIL, bases de datos SQLlite, arreglos NumPy, etc.) sin copiar primero. Esto es muy
importante para grandes conjuntos de datos.

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

>>> numeros = array.array('h', [-2, -1, 0, 1, 2]) >>> memv =


vistamemoria(numeros) >>> len(memv) 5

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

Cuando una lista no es la respuesta | 51


Machine Translated by Google

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

tipo 'h'). memv ve los mismos 5 elementos en la matriz.

Cree memv_oct convirtiendo los elementos de memv en el código de tipo 'B' (caracter sin firmar).

Exporte elementos de memv_oct como una lista para su inspección.

Asigne el valor 4 al desplazamiento de bytes 5.

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.

NumPy y SciPy A lo largo

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.

Ejemplo 2-22. Operaciones básicas con filas y columnas en un numpy.ndarray

>>> import numpy


>>> a = numpy.arange(12)
>>> un
matriz ([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])

52 | Capítulo 2: Una matriz de secuencias


Machine Translated by Google

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

Importe Numpy, después de la instalación (no está en la biblioteca estándar de Python).

Cree e inspeccione un numpy.ndarray con números enteros del 0 al 11.

Inspeccione las dimensiones de la matriz: esta es una matriz unidimensional de 12 elementos.

Cambie la forma de la matriz, agregue una dimensión y luego inspeccione el resultado.

Obtenga la fila en el índice 2.

Obtenga el elemento en el índice 2, 1.

Obtenga la columna en el índice 1.

Cree una nueva matriz transponiendo (intercambiando columnas con filas).

NumPy también admite operaciones de alto nivel para cargar, guardar y operar en todos
elementos de un numpy.ndarray:

>>> importar números


>>> floats = numpy.loadtxt('floats-10M-lines.txt') >>> floats[-3:]
array([ 3016362.69195522, 535281.10514262,
4566560.44373946])
>>> flotantes *= .5
>>> flotantes[-3:]
matriz ([ 1508181.34597761, 267640.55257131, 2283280.22186973])
>>> desde el momento de importar perf_counter como pc
>>> t0 = pc(); flotadores /= 3; pc() - t0
0.03690556302899495
>>> numpy.save('floats-10M', floats) >>>
floats2 = numpy.load('floats-10M.npy', 'r+') >>> floats2 *= 6

Cuando una lista no es la respuesta | 53


Machine Translated by Google

>>> float2[-3:]
memmap([ 3016362.69195522, 535281.10514262, 4566560.44373946])

Cargue 10 millones de números de coma flotante desde un archivo de texto.

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.

Guarde la matriz en un archivo binario .npy .

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.

Instalar NumPy y SciPy desde la fuente no es pan comido. La página


Installing the SciPy Stack en SciPy.org recomienda usar distribuciones
científicas especiales de Python como Anaconda, Enthought Canopy
y WinPython, entre otras. Estas son descargas grandes, pero vienen
listas para usar. Los usuarios de distribuciones populares de GNU/
Linux generalmente pueden encontrar NumPy y SciPy en los
repositorios de paquetes estándar. Por ejemplo, instalarlos en Debian
o Ubuntu es tan fácil como:
$ sudo apt-get install python-numpy python-scipy

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.

54 | Capítulo 2: Una matriz de secuencias


Machine Translated by Google

Deques y otras colas Los

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.

Ejemplo 2-23. Trabajando con un deque

>>> from collections import deque


>>> dq = deque(range(10), maxlen=10) >>>
dq deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9] ,
maxlen=10) >>> dq.girar(3) >>> dq deque([7, 8, 9, 0, 1, 2,
3, 4, 5, 6], maxlen=10) >>> dq .rotar(-4) >>> dq deque([1,
2, 3, 4, 5, 6, 7, 8, 9, 0], maxlen=10) >>> dq.appendleft(-1)
>> > dq deque([-1, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10) >>>
dq.extend([11, 22, 33]) >>> dq deque([3, 4, 5, 6, 7, 8, 9,
11, 22, 33], maxlen=10) >>> dq.extendleft([10, 20, 30, 40])
>>> dq deque ([40, 30, 20, 10, 3, 4, 5, 6, 7, 8], maxlen=10)

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.

Cuando una lista no es la respuesta | 55


Machine Translated by Google

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.appendleft(e) • Añadir un elemento a la izquierda (antes del primero)

claro() •• Eliminar todos los elementos

s.__contiene__(e) • e en s

s.copiar() • Copia superficial de la lista.

s.__copia__() • Soporte para copy.copy (copia superficial)

s.count(e) •• Contar las ocurrencias de un elemento

s.__delitem__(p) •• Eliminar elemento en la posición p

s.extender(i) •• Agregar elementos de iterable i a la derecha

s.extenderleft(i) • Agregar elementos de iterable i a la izquierda

s.__getitem__(p) •• s[p]: obtiene el elemento en la posición

s.index(e) • Encuentre la posición de la primera aparición de e

s.insertar(p, e) • Inserte el elemento e antes del elemento en la posición p

s.__iter__() •• Obtener iterador

s.__len__() •• len(s)—número de elementos

s.__mul__(n) • s * n: concatenación repetida

s.__imul__(n) • s *= n: concatenación repetida en el lugar


s.__rmul__(n) • norte

* s: concatenación repetida invertidaa


pop() •• Quitar y devolver el último artículob

s.popleft() • Quitar y devolver el primer artículo

s.remove(e) •• Eliminar la primera aparición del elemento e por valor

al revés() •• Invertir el orden de los elementos en su lugar

s.__invertido__() •• Obtener iterador para escanear elementos del último al primero

56 | Capítulo 2: Una matriz de secuencias


Machine Translated by Google

lista de que

s.girar(n) • Mover n elementos de un extremo al otro

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

a Los operadores invertidos se explican en el Capítulo 13.


b
a_list.pop(p) permite eliminar desde la posición p pero deque no admite esa opción.

Además de deque, otros paquetes de biblioteca estándar de Python implementan colas:

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

Resumen del capítulo


Dominar los tipos de secuencia de la biblioteca estándar es un requisito previo para escribir código
de Python conciso, efectivo e idiomático.

Resumen del capítulo | 57


Machine Translated by Google

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.

58 | Capítulo 2: Una matriz de secuencias


Machine Translated by Google

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

La mejor defensa de la convención de Python de excluir el último elemento en rangos y sectores


fue escrita por el propio Edsger W. Dijkstra, en un breve memorándum titulado "Por qué la
numeración debería comenzar en cero". El tema de la nota es la notación matemática, pero es
relevante para Python porque el profesor Dijkstra explica con rigor y humor por qué la secuencia
…, 2, 3, 12 siempre debe expresarse como 2 ÿ i < 13. Todas las demás convenciones razonables son
refutado, al igual que la idea de dejar que cada usuario elija una convención. El título se refiere
a la indexación basada en cero, pero el memorándum realmente trata sobre por qué es deseable
12 como
que 'ABCDE'[1:3] signifique 'BC' y no 'BCD' y por qué tiene perfecto sentido escribir 2, 3,..., rango
(2, 13). (Por cierto, el memorándum es una nota escrita a mano, pero es hermoso y totalmente
legible. Alguien debería crear una fuente Dijkstra; yo la compraría).

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.

“La elegancia engendra simplicidad”

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.

60 | Capítulo 2: Una matriz de secuencias


Machine Translated by Google

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”.

Secuencias planas versus secuencias

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:

Algunos objetos contienen referencias a otros objetos; estos se llaman contenedores.

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.

Listas de bolsas mixtas

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)

Rastreo (llamadas recientes más última):


Archivo "<stdin>", línea 1, en <módulo>
TypeError: tipos no ordenados: str() < int()

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']

Oracle, Google y la conspiración de Timbot

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.

62 | Capítulo 2: Una matriz de secuencias


Machine Translated by Google

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.

Aquí hay un breve resumen de este capítulo:

• Métodos comunes de diccionario •

Manejo especial de claves faltantes •

Variaciones de dict en la biblioteca estándar • Los tipos

set y frozenset • Cómo funcionan las tablas hash

• Implicaciones de las tablas hash (limitaciones de tipo de clave, ordenación impredecible, etc.)

63
Machine Translated by Google

Tipos de mapeo genéricos


El módulo collections.abc proporciona los ABC Mapping y MutableMapping para formalizar las
interfaces de dict y tipos similares (en Python 2.6 a 3.2, estas clases se importan desde el módulo
collections y no desde collections.abc). Consulte la Figura 3-1.

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)

Las implementaciones de asignaciones especializadas a menudo extienden dict o collections.User


Dict, en lugar de estos ABC. El principal valor de los ABC es documentar y formalizar las
interfaces mínimas para las asignaciones y servir como criterio para las pruebas de instancia en
el código que necesita admitir las asignaciones en un sentido amplio:

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

64 | Capítulo 3: Diccionarios y conjuntos


Machine Translated by Google

¿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:

>>> tt = (1, 2, (30, 40)) >>>


hash(tt)
8027212646858338501
>>> tl = (1, 2, [30, 40]) >>>
hash(tl)
Rastreo (llamadas recientes más última):
Archivo "<stdin>", línea 1, en <módulo>
TypeError: tipo no modificable: 'lista' >>>
tf = (1, 2, conjunto congelado ([30, 40])) >>>
hash (tf)
-4118419923444501110

En el momento de escribir este artículo, el glosario de Python establece:


"Todos los objetos integrados inmutables de Python se pueden modificar",
pero eso es incorrecto porque una tupla es inmutable, pero puede contener
referencias a objetos que no se pueden modificar.

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:

>>> a = dict(uno=1, dos=2, tres=3) >>> b =


{'uno': 1, 'dos': 2, 'tres': 3} >>> c = dict( zip(['uno',
'dos', 'tres'], [1, 2, 3])) >>> d = dict([('dos', 2), ('uno', 1), ( 'tres', 3)])
>>> e = dict({'tres': 3, 'uno': 1, 'dos': 2}) >>> a == b == c == d == mi

Verdadero

Tipos de asignación genéricos | sesenta y cinco


Machine Translated by Google

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

Desde Python 2.7, la sintaxis de listcomps y genexs se aplicó a la comprensión de dictados.


siones (y conjuntos de comprensiones también, que pronto visitaremos). Un dictcomp construye un dict
instancia produciendo un par clave:valor de cualquier iterable. El ejemplo 3-1 muestra el uso de
dict comprensiones para construir dos diccionarios a partir de la misma lista de tuplas.

Ejemplo 3-1. Ejemplos de comprensiones dictadas

>>> 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'}

Se puede usar una lista de pares directamente con el constructor dict .

Aquí los pares están invertidos: el país es la clave y el código es el valor.

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.

Ahora pasamos a una vista panorámica de la API para asignaciones.

Descripción general de los métodos de mapeo comunes

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 .

66 | Capítulo 3: Diccionarios y conjuntos


Machine Translated by Google

Tabla 3-1. Métodos de los tipos de mapeo dict, collections.defaultdict y colecÿ


tions.OrderedDict (métodos de objetos comunes omitidos por brevedad); argumentos opcionales
están encerrados en […]

dict predeterminadodict OrderedDict

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.fromkeys(it, [inicial]) • • • Nueva asignación de claves en iterable, con opción


valor inicial (predeterminado en Ninguno)

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.pop(k, [predeterminado]) •• • Eliminar y devolver el valor en k, o por defecto o


Ninguno si falta

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.setdefault(k, [predeterminado]) • • • Si k en d, devuelve d[k]; otra cosa establece d[k] =


predeterminado y devolverlo

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

Descripción general de los métodos comunes de mapeo | 67


Machine Translated by Google

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.

Manejo de claves faltantes con setdefault De acuerdo

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)

"""Crear una palabra de mapeo de índice -> lista de ocurrencias"""

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

1. El guión original aparece en la diapositiva 41 de la presentación "Reaprendizaje de Python" de Martelli. Su script es en


realidad una demostración de dict.setdefault, como se muestra en nuestro Ejemplo 3-4.

68 | Capítulo 3: Diccionarios y conjuntos


Machine Translated by Google

# imprimir en orden alfabético por


palabra en sorted(index, key=str.upper): print(word,
index[word])

Obtenga la lista de ocurrencias de palabra, o [] si no se encuentra.

Agregar nueva ubicación a las 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)

$ python3 index0.py ../../data/zen.txt a [(19, 48),


(20, 53)]
Aunque [(11, 1), (16, 1), (18, 1)] ambigüedad
[(14, 16)] y [(15, 23)] son [(21, 12)] son [(10,
15 )] en [(16, 38)] malo [(19, 50)] ser [(15, 14),
(16, 27), (20, 50)] latidos [(11, 23)]

Hermosa [(3, 1)] mejor


[(3, 14), (4, 13), (5, 11), (6, 12), (7, 9), (8, 11), (17, 8 ) ), (18, 25)]

...

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

"""Crear una palabra de mapeo de índice -> lista de ocurrencias"""

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.

Descripción general de los métodos comunes de mapeo | 69


Machine Translated by Google

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 ) index.setdefault(palabra,
[]).append(ubicación)

# imprimir en orden alfabético por


palabra en sorted(index, key=str.upper): print(word,
index[word])

Obtenga la lista de ocurrencias de la palabra, o configúrela en [] si no se encuentra; setdefault


devuelve el valor, por lo que se puede actualizar sin necesidad de una segunda búsqueda.

En otras palabras, el resultado final de esta línea...

my_dict.setdefault(clave, []).append(nuevo_valor)

…es lo mismo que correr…

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.

Un problema relacionado, el manejo de claves faltantes en cualquier búsqueda (y no solo al insertar), es el


tema de la siguiente sección.

Asignaciones con búsqueda de clave flexible


A veces es conveniente tener asignaciones que devuelvan algún valor inventado cuando se busca una
clave faltante. Hay dos enfoques principales para esto: uno es usar un dictado predeterminado en lugar de
un dictado simple . La otra es subclasificar dict o cualquier otro tipo de mapeo y agregar un método
__missing__ . Ambas soluciones se tratan a continuación.

defaultdict: otra versión de las claves que faltan El ejemplo

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.

70 | Capítulo 3: Diccionarios y conjuntos


Machine Translated by Google

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:

1. Llama a list() para crear una nueva lista.

2. Inserta la lista en dd usando 'nueva clave' como clave.


3. Devuelve una referencia a esa lista.

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

"""Crear una palabra de mapeo de índice -> lista de ocurrencias"""

importar
sistema
importar volver a importar colecciones

WORD_RE = re.compilar('\w+')

index = collections.defaultdict(list) with


open(sys.argv[1], encoding='utf-8') as 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 ) index[word].append(ubicación)

# imprimir en orden alfabético por


palabra en sorted(index, key=str.upper): print(word,
index[word])

Cree un dictado predeterminado con el constructor de listas como default_factory.

Si word no está inicialmente en el índice , se llama a default_factory para generar el valor


faltante, que en este caso es una lista vacía que luego se asigna a index[word] y se devuelve,
por lo que la operación .append(ubicación) siempre tiene éxito.

Si no se proporciona default_factory , se genera el KeyError habitual para las claves que faltan.

Asignaciones con búsqueda de clave flexible | 71


Machine Translated by Google

La fábrica predeterminada de un dictamen predeterminado solo se invoca para


proporcionar valores predeterminados para las llamadas __getitem__ , y no
para los otros métodos. Por ejemplo, si dd es un dictamen predeterminado y k
falta una clave, dd[k] llamará a default_factory para crear un valor
predeterminado, pero dd.get(k) seguirá devolviendo Ninguno.

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.

El método __missing__ Subyacente

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.

El método __missing__ simplemente es llamado por __getitem__ (es decir,


para el operador d[k] ). La presencia de un método __missing__ no tiene efecto
en el comportamiento de otros métodos que buscan claves, como get o
__contains__ (que implementa el operador in ). Esta es la razón por la que
default_factory de defaultdict solo funciona con __getitem__, como se indica en
la advertencia al final de la sección anterior.

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 = StrKeyDict0([('2', 'dos'), ('4', 'cuatro')]) >>> d['2'] 'dos'

72 | Capítulo 3: Diccionarios y conjuntos


Machine Translated by Google

>>> d[4]
'cuatro'
>>> d[1]
Rastreo (última llamada más reciente):
...
Error de clave: '1'

Pruebas para la recuperación de elementos usando la notación `d.get(key)` ::

>>> d.get('2') 'dos'


>>> d.get(4)
'cuatro'

>>> d.get(1, 'N/A')


'N / A'

Pruebas para el operador `in` ::

>>> 2 en re

Verdadero >>> 1 en d
Falso

El ejemplo 3-7 implementa una clase StrKeyDict0 que pasa las pruebas anteriores.

Una mejor manera de crear un tipo de mapeo definido por el usuario es


crear una subclase collections.UserDict en lugar de dict (como haremos
en el Ejemplo 3-8). Aquí subclasificamos dict solo para mostrar que
__miss ing__ es compatible con el método incorporado dict.__getitem__ .

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

def __missing__(self, key): if


isinstance(key, str): raise
KeyError(key)
volver self[str(clave)]

def get(self, key, default=Ninguno): try:


return self[key] excepto KeyError:
return default

def __contiene__(uno mismo, clave):


tecla de retorno en self.keys() o str(key) en self.keys()

Asignaciones con búsqueda de clave flexible | 73


Machine Translated by Google

StrKeyDict0 hereda de dict.

Compruebe si la clave ya es una cadena. Si es así y falta, genera KeyError.

Construya str desde la clave y búsquelo.

El método get delega a __getitem__ usando la notación self[key] ; que da la oportunidad a nuestros
__desaparecidos__ de actuar.

Si se generó un KeyError , __missing__ ya falló, por lo que devolvemos el valor predeterminado.

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

Una búsqueda como k en my_dict.keys() es eficiente en Python 3 incluso


para asignaciones muy grandes porque dict.keys() devuelve una vista, que
es similar a un conjunto, y las comprobaciones de contención en los
conjuntos son tan rápidas como en los diccionarios. Los detalles están
documentados en la sección de objetos de vista "Diccionario" de la
documentación. En Python 2, dict.keys() devuelve una lista, por lo que
nuestra solución también funciona ahí, pero no es eficiente para diccionarios
grandes, porque k en my_list debe escanear la lista.

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.

74 | Capítulo 3: Diccionarios y conjuntos


Machine Translated by Google

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:

>>> ct = colecciones.Contador('abracadabra') >>>


ct Contador({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1} ) >>>
ct.update('aaaaazzz') >>> ct

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

def __missing__(self, key): if


isinstance(key, str): raise
KeyError(key)
volver self[str(clave)]

def __contiene__(uno mismo, clave):


devuelve str(clave) en self.data

def __setitem__(self, key, item):


self.data[str(key)] = item

StrKeyDict extiende UserDict.


__missing__ es exactamente como en el Ejemplo 3-7.

__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.

76 | Capítulo 3: Diccionarios y conjuntos


Machine Translated by Google

__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).

Después de escribir StrKeyDict, descubrí que Antoine Pitrou fue el


autor de PEP 455: agregar un diccionario de transformación de
claves a las colecciones y un parche para mejorar el módulo de
colecciones con TransformDict. El parche se adjunta al problema
18986 y puede aterrizar en Python 3.5. Para experimentar con
TransformDict, lo extraje en un módulo independiente (03-dict-set/
transformdict.py en el repositorio de código de Fluent Python ).
TransformDict es más general que StrKeyDict y se complica por el
requisito de conservar las claves tal como se insertaron originalmente.

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

>>> from tipos importar MappingProxyType >>>


d = {1: 'A'} >>> d_proxy = MappingProxyType(d)
>>> d_proxy mapeoproxy({1: 'A'}) >>> d_proxy[1]

'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'
>>>

Los elementos en d se pueden ver a través de d_proxy.

No se pueden realizar cambios a través de d_proxy.

d_proxy es dinámico: se refleja cualquier cambio en d .

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.

78 | Capítulo 3: Diccionarios y conjuntos


Machine Translated by Google

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:

>>> l = ['spam', 'spam', 'huevos', 'spam'] >>> conjunto(l)


{'huevos', 'spam'} >>> lista(conjunto(l)) [' huevos', 'spam']

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

encontrado = len(agujas y pajar)

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

encontrado = len(conjunto(agujas) & conjunto(pajar))

# 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.

En Python 3, la representación de cadenas estándar de conjuntos siempre usa la notación {...} ,


excepto para el conjunto vacío:

>>> s = {1}
>>> tipo(s)
<clase 'conjunto'>
>>> s
{1}
>>> s.pop() 1

80 | Capítulo 3: Diccionarios y conjuntos


Machine Translated by Google

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

>>> de des import des


>>> des('{1}') 1
0 LOAD_CONST 0 (1)
3 CONSTRUIR_SET 1
6 RETURN_VALUE
>>> dis('establecer([1])')
1 0 LOAD_NOMBRE 0
3 LOAD_CONST (conjunto) 0 (1)
6 CONSTRUIR_LISTA 1
9 LLAMADA_FUNCIÓN 1 (1 posicional, 0 par de palabras clave)
12 RETURN_VALUE

Desensamblar el código de bytes para la expresión literal {1}.

El bytecode especial BUILD_SET hace casi todo el trabajo.

Código de bytes para el conjunto ([1]).

Tres operaciones en lugar de BUILD_SET: LOAD_NAME, BUILD_LIST y


LLAMADA_FUNCIÓN.

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:

>>> conjunto congelado(rango(10))


conjunto congelado({0, 1, 2, 3, 4, 5, 6, 7, 8, 9})

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

>>> from unicodedata importar nombre


>>> {chr(i) for i in range(32, 256) if 'SIGN' in name(chr(i),'')} {'§', '=', ' ¢', '#', '¤', '<',
'¥', 'µ', '×', '$', '¶', '£', '©', '°', '+' , '÷', '±', '>', '¬', '®', '%'}

Función de importación de nombres desde UnicodeData para obtener nombres de personajes.

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)

82 | Capítulo 3: Diccionarios y conjuntos


Machine Translated by Google

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ÿZ s&z Intersección de s y z


s.__y__(z)
zys s.__rand__(z) Invertido y operador

s.intersection(it, …) Intersección de s y todos los conjuntos construidos a partir de iterables it, etc.

s&=z s.__iand__(z) s actualizado con la intersección de s y z

s.intersection_up s actualizado con la intersección de s y todos los conjuntos construidos a partir de

fecha(es, …) lo itera , etc.

SÿZ Unión de s y z
s|z s.__o__(z)

z|s s.__ror__(z) Invertido |

s.union(it, …) Unión de s y todos los conjuntos construidos a partir de iterables , etc.

s |= z s.__ior__(z) s actualizado con unión de s y z

s.update(it, …) s actualizado con la unión de s y todos los conjuntos creados a partir de iterables

eso, etc

S\Z s-z s.__sub__(z) Complemento relativo o diferencia entre s y z

z-s s.__rsub__(z) Invertido - operador

s.diference(it, …) Diferencia entre s y todos los conjuntos construidos a partir de iterables ,


etc.

s-=z s.__isub__(z) s actualizado con diferencia entre s y z

s.difference_up s actualizado con diferencia entre s y todos los conjuntos construidos

fecha(es, …) de iterables , etc.

s.metric_differ Complemento de s & set(it)

ence (eso)

SÿZ s ^ z s.__xor__(z) Diferencia simétrica (el complemento de la intersección


s y z)

z ^ s s.__rxor__(z) Operador ^ invertido

s.metric_differ actualizado con diferencia simétrica de arena y todos los conjuntos construidos

ence_update(eso, …) de iterables , etc.

s^=z s.__ixor__(z) s actualizado con diferencia simétrica de s y z

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.

Tabla 3-3. Establecer operadores de comparación y métodos que devuelvan un bool

Símbolo matemático Operador Python Método Descripción

s.es disjunto(z) s y z son disjuntos (no tienen elementos en común)

mi ÿ S e en s s.__contains__(e) El elemento e es miembro de s

SÿZ s <= z s.__le__(z) s es un subconjunto del conjunto z

s.issubset(it) s es un subconjunto del conjunto construido a partir del iterable it

SÿZ s<z s.__lt__(z) s es un subconjunto propio del conjunto z

SÿZ s >= z s.__ge__(z) s es un superconjunto del conjunto z

s.issuperset(it) s es un superconjunto del conjunto construido a partir del iterable it

SÿZ s>z s.__gt__(z) s es un superconjunto propio del conjunto z

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.

Tabla 3-4. Métodos de configuración adicionales

conjunto congeladoconjunto

• Agregar elemento e a s
añadir (e)

claro() Eliminar todos los elementos de s

••
s.copiar() copia superficial de s

para descartar(e) • Eliminar el elemento e de s si está presente

s.__iter__() • • Obtener iterador sobre s

__len__() • • lente)

pop() Eliminar y devolver un elemento de s, generando KeyError si s está vacío

eliminar(e) • Eliminar el elemento e de s, generando KeyError si e no está en s

Esto completa nuestra descripción general de las características de los conjuntos.

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

84 | Capítulo 3: Diccionarios y conjuntos


Machine Translated by Google

comportamiento aparentemente impredecible exhibido a veces por dict, set y sus hermanos.

dictar y establecer Bajo el capó


Comprender cómo se implementan los diccionarios y conjuntos de Python mediante tablas hash es útil para
entender sus fortalezas y limitaciones.

Aquí hay algunas preguntas que esta sección responderá:

• ¿Qué tan eficientes son Python dict y set? • ¿Por qué

están desordenados? • ¿Por qué no podemos usar

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.

Un experimento de rendimiento Por

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

Ejemplo 3-14. Busca agujas en un pajar y cuenta las encontradas

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.

dictar y establecer Bajo el capó | 85


Machine Translated by Google

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)

len del factor pajar Factor de tiempo de dictado

1,000 1x 0.000202s 1.00x

10,000 10x 0.000140s 0.69x

100,000 100x 0.000228s 1.13x

1,000,000 1,000x 0.000290s 1.44x

10,000,000 10,000x 0.000337s 1.67x

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

encontrado = len(agujas y 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

1,000 1x 0.000202s 1.00x 0.000143s 1.00x 0.000087s 1.00x 0.010556s 1.00x

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

86 | Capítulo 3: Diccionarios y conjuntos


Machine Translated by Google

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.

Tablas hash en diccionarios


Esta es una vista de alto nivel de cómo Python usa una tabla hash para implementar un dict. Se omiten
muchos detalles (el código CPython tiene algunos trucos de optimización5), pero la descripción general es
precisa.

Para simplificar la presentación subsiguiente, nos centraremos primero en las


partes internas de dict y luego transferiremos los conceptos a conjuntos.

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.

dictar y establecer Bajo el capó | 87


Machine Translated by Google

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)

Compilación de Python de 32 bits


1 000000000000000000000000000000001
!= 0
1.0 000000000000000000000000000000001
------------------------------------------------
1.0 000000000000000000000000000000001
! !!! ! !! ! ! ! ! !! !!! != 16
1.0001 00101110101101010000101011011101
------------------------------------------------
1.0001 00101110101101010000101011011101
!!! !!!! !!!!! !!!!! !! ! != 20
1.0002 01011101011010100001010110111001
------------------------------------------------
1.0002 01011101011010100001010110111001
! !! !! ! !!!!! !=
!!! 17
! ! !
1.0003 00001100000111110010000010010110
------------------------------------------------

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.

A partir de Python 3.3, se agrega un valor salt aleatorio al


hashes de objetos str, bytes y datetime . El valor de la sal es
constante dentro de un proceso de Python, pero varía entre los intérpretes
carreras. La sal aleatoria es una medida de seguridad para evitar un DOS
ataque. Los detalles están en una nota en la documentación para el __hash__
método especial.

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.

El algoritmo de la tabla hash

Para obtener el valor en my_dict[search_key], Python llama a hash(search_key) para obtener


el valor hash de search_key y utiliza los bits menos significativos de ese número como un
offset para buscar un depósito en la tabla hash (el número de bits utilizados depende del
tamaño actual de la tabla). Si el depósito encontrado está vacío, se genera KeyError . De lo contrario,

88 | Capítulo 3: Diccionarios y conjuntos


Machine Translated by Google

el depósito encontrado tiene un elemento, un par clave_encontrada:valor_encontrado , y luego Python


verifica si clave_búsqueda == clave_encontrada. Si coinciden, ese era el elemento buscado: se devuelve
el valor_encontrado .

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.

dictar y establecer Bajo el capó | 89


Machine Translated by Google

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.

Consecuencias prácticas de cómo funciona dict En las

siguientes subsecciones, analizaremos las limitaciones y los beneficios que la implementación de la tabla hash
subyacente aporta al uso de dict .

Las claves deben ser objetos hash

Un objeto es hashable si se cumplen todos estos requisitos:

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.

2. Admite la igualdad a través de un método eq() .

3. Si a == b es Verdadero entonces hash(a) == hash(b) también debe ser Verdadero.

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 implementa una clase con un método __eq__ personalizado , también debe


implementar un __hash__ adecuado, porque siempre debe asegurarse de que
si a == b es Verdadero , entonces hash(a) == hash(b) también es Verdadero.
De lo contrario, está rompiendo un invariante del algoritmo de la tabla hash,
con la grave consecuencia de que los dictados y los conjuntos no tratarán de
forma fiable con sus objetos. Si un __eq__ personalizado depende del estado
mutable, entonces __hash__ debe generar TypeError con un mensaje como
un tipo que no se puede modificar: 'MyClass'.

los dictados tienen una sobrecarga de

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.

90 | Capítulo 3: Diccionarios y conjuntos


Machine Translated by Google

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.

El orden de las claves depende del orden

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

# códigos de marcación de los 10 países más poblados


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'),
]

d1 = dict(DIAL_CODES)
print('d1:', d1.keys()) d2 =
dict(ordenado(DIAL_CODES))

dictar y establecer Bajo el capó | 91


Machine Translated by Google

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

ordenadas por nombre de país.

Los diccionarios se comparan igual, porque contienen los mismos pares clave:valor .

El ejemplo 3-18 muestra la salida.

Ejemplo 3-18. La salida de dialcodes.py muestra tres ordenaciones de teclas distintas

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

Agregar elementos a un dictado puede cambiar el orden de las

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.

En Python 3, los métodos .keys(), .items() y .values() devuelven


vistas de diccionario, que se comportan más como conjuntos que
las listas devueltas por estos métodos en Python 2. Estas vistas
también son dinámicas. : no replican el contenido del dict y
reflejan inmediatamente cualquier cambio en el dict.

Ahora podemos aplicar lo que sabemos sobre las tablas hash a los conjuntos.

92 | Capítulo 3: Diccionarios y conjuntos


Machine Translated by Google

Cómo funcionan los conjuntos: consecuencias prácticas Los

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:

• Los elementos del conjunto deben ser objetos

hashable. • Los conjuntos tienen una sobrecarga de

memoria significativa. • La prueba de membresía es

muy eficiente. • El orden de los elementos depende del orden

de inserción. • Agregar elementos a un conjunto puede cambiar el orden de otros elementos.

Resumen del capítulo


Los diccionarios son una piedra angular de Python. Más allá del dictado básico , la biblioteca estándar ofrece
asignaciones especializadas útiles y listas para usar, como dictado predeterminado, OrderedDict , ChainMap y
Counter, todas definidas en el módulo de colecciones . El mismo módulo también proporciona la clase UserDict
fácil de ampliar.

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.

Resumen del capítulo | 93


Machine Translated by Google

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.

Capítulo 1 de Python Cookbook, tercera edición (O'Reilly) de David Beazley y Brian K.


Jones tiene 20 recetas útiles y perspicaces con estructuras de datos, la mayoría usando dict de manera
inteligente.

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

94 | Capítulo 3: Diccionarios y conjuntos


Machine Translated by Google

(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

Texto frente a bytes

Los humanos usan texto. Las computadoras hablan bytes.1

— Esther Nam y Travis Fischer


Codificación de caracteres y Unicode en Python

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.

En este capítulo, vamos a visitar los siguientes temas:

• Caracteres, puntos de código y representaciones de bytes •

Características únicas de secuencias binarias: bytes, bytearray y memoryview • Códecs para

juegos de caracteres heredados y Unicode completos • Evitar y tratar errores de codificación •

Mejores prácticas al manejar archivos de texto • La trampa de codificación predeterminada y

problemas de E/S estándar • Comparaciones seguras de texto Unicode con normalización

1. Diapositiva 12 de PyCon 2014 charla "Codificación de caracteres y Unicode en Python" (diapositivas, video).

97
Machine Translated by Google

• Funciones de utilidad para normalización, plegado de mayúsculas y minúsculas y eliminación diacrítica de

fuerza bruta • Ordenación adecuada de texto Unicode con la configuración regional y la biblioteca PyUCA •

Metadatos de caracteres en la base de datos Unicode

• API de modo dual que manejan str y bytes

Comencemos con los caracteres, los puntos de código y los bytes.

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.

La conversión de puntos de código a bytes es codificación; la conversión de bytes a puntos de código es


decodificación. Vea el Ejemplo 4-1.

Ejemplo 4-1. Codificación y decodificación

>>> s = 'café'
>>> len(s) # 4
>>> b =
s.encode('utf8') # >>> b
b'caf\xc3\xa9' # >>> len(b) # 5

98 | Capítulo 4: Texto versus Bytes


Machine Translated by Google

>>> b.decode('utf8') # 'café'

El str 'café' tiene cuatro caracteres Unicode.

Codifique str a bytes usando la codificación UTF-8. los

bytes literales comienzan con un prefijo b . bytes b tiene

cinco bytes (el punto de código para “é” está codificado como dos bytes en UTF-8).

Decodifica bytes a str usando la codificación UTF-8.

Si necesita una ayuda de memoria para ayudar a distinguir .decode()


de .encode(), convénzase de que las secuencias de bytes pueden ser
volcados de memoria de máquina crípticos mientras que los objetos Unicode
str son texto "humano". Por lo tanto, tiene sentido que decodifiquemos bytes
a str para obtener texto legible por humanos, y codifiquemos str a bytes
para almacenamiento o transmisión.

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

>>> café = bytes('café', codificación='utf_8') >>>


café b'caf\xc3\xa9' >>> café[0] 99 >>> café[:1] b'c'
> >> cafe_arr = bytearray(cafe) >>> cafe_arr

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.

Cada elemento es un número entero en el rango (256).

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.

Una porción de bytearray también es un bytearray.

El hecho de que my_bytes[0] recupere un int pero my_bytes[:1]


devuelva un objeto de bytes de longitud 1 no debería sorprender.
El único tipo de secuencia donde s[0] == s[:1] es el tipo str .
Aunque práctico, este comportamiento de str es excepcional. Para
cualquier otra secuencia, s[i] devuelve un elemento y s[i:i+1]
devuelve una secuencia del mismo tipo con el elemento s[1] dentro.

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

secuencias binarias, solo usando bytes y no argumentos str .

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.

100 | Capítulo 4: Texto versus Bytes


Machine Translated by Google

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:

>>> bytes.fromhex('31 4B CE A9')


b'1K\xce\xa9'

Las otras formas de construir instancias de bytes o bytearray son llamando a sus constructores con:

• Una cadena y un argumento de palabra clave de

codificación . • Un iterable que proporciona elementos con valores de 0

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

>>> import array


>>> numeros = array.array('h', [-2, -1, 0, 1, 2]) >>> octetos
= bytes(numeros) >>> octetos

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

contiene una copia de los bytes que componen los números.

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.

Esenciales de bytes | 101


Machine Translated by Google

Estructuras y vistas de memoria El

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

>>> import struct


>>> fmt = '<3s3sHH' # >>>
with open('filter.gif', 'rb') as fp: img =
... memoryview(fp.read()) #
...
>>> cabecera = img[:10] #
>>> bytes(cabecera) #
b'GIF89a+\x02\xe6\x00' >>>
struct.unpack(fmt, cabecera) # (b'GIF',
b'89a ', 555, 230) >>> del encabezado #
>>> del img

formato de estructura : < little-endian; 3s3s dos secuencias de 3 bytes; HH dos enteros de 16 bits.

Cree una vista de memoria a partir del contenido del archivo en

la memoria... ...luego otra vista de memoria cortando la primera; aquí no se copian bytes.

Convertir a bytes solo para visualización; Aquí se copian 10 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 .

2. Pillow es la horquilla más activa de PIL.

102 | Capítulo 4: Texto versus Bytes


Machine Translated by Google

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

>>> para el códec en ['latin_1', 'utf_8', 'utf_16']:


... print(códec, 'El Niño'.encode(códec), sep='\t')
...
latin_1 b'El Ni\xf1o'
utf_8 b'El Ni\xc3\xb1o'
utf_16 b'\xff\xfeE\x00l\x00 \x00N\x00i\x00\xf1\x00o\x00'

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.

Codificadores/Decodificadores básicos | 103


Machine Translated by Google

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.

104 | Capítulo 4: Texto versus Bytes


Machine Translated by Google

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".

UTF-16 reemplazó la codificación Unicode 1.0 original de 16 bits


(UCS-2) allá por 1996. UCS-2 aún se implementa en muchos sistemas,
pero solo admite puntos de código hasta U+FFFF. A partir de Unicode
6.3, más del 50% de los puntos de código asignados están por encima
de U +10000, incluidas las pictografías emoji cada vez más populares.

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.

Comprensión de los problemas de codificación/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.

Lo primero que debe tener en cuenta cuando recibe un error Unicode


es el tipo exacto de excepción. ¿Es un UnicodeEncodeError, un
UnicodeDeco deError o algún otro error (por ejemplo, SyntaxError) que
menciona un problema de codificación? Para resolver el problema,
primero hay que entenderlo.

Lidiando con UnicodeEncodeError La mayoría de

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 %.

Comprensión de los problemas de codificación/descodificación | 105


Machine Translated by Google

Ejemplo 4-6. Codificación a bytes: éxito y manejo de errores


>>> ciudad = 'São Paulo'
>>> ciudad.codificar('utf_8')
b'S\xc3\xa3o Paulo' >>>
ciudad.codificar('utf_16')
b'\xff\xfeS\x00\xe3\x00o \x00 \x00P\x00a\x00u\x00l\x00o\x00' >>>
ciudad.codificar('iso8859_1') b'S\xe3o Paulo' >>> ciudad.codificar('cp437')

Rastreo (llamadas recientes más última):


Archivo "<stdin>", línea 1, en <módulo>
Archivo "/.../lib/python3.4/encodings/cp437.py", línea 12, en codificar devuelve
codecs.charmap_encode(input,errors,encoding_map)
UnicodeEncodeError: el códec 'charmap' no puede codificar el carácter '\xe3' en la
posición 1: el carácter se asigna a <indefinido> >>> ciudad.encode('cp437',
errores='ignorar') b'So Paulo' >>> ciudad.encode('cp437', errores='reemplazar') b'S?
o Paulo' >>> ciudad.encode('cp437', errores='xmlcharrefreplace') b'S&#227;o Paulo'

El 'utf_?' las codificaciones manejan cualquier


str. 'iso8859_1' también funciona para la calle 'São Paulo'.

'cp437' no puede codificar la 'ã' ("a" con tilde). El controlador de errores predeterminado,
"estricto", genera UnicodeEncodeError.

El controlador error='ignore' omite silenciosamente los caracteres que no se pueden codificar;


esto suele ser una muy mala idea.

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

codificables con una entidad XML.

El manejo de errores de códecs es extensible. Puede registrar


cadenas adicionales para el argumento de errores pasando un nombre
y una función de manejo de errores a la función codecs.register_error .
Consulte la documentación de codecs.register_error .

Hacer frente a UnicodeDecodeError No todos

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.

106 | Capítulo 4: Texto versus Bytes


Machine Translated by Google

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.

Los caracteres ilegibles se conocen como gremlins o mojibake (ÿÿÿ


ÿ —"texto transformado" en japonés).

El ejemplo 4-7 ilustra cómo el uso del códec incorrecto puede producir gremlins o un Unico
decodeError.

Ejemplo 4-7. Decodificación de str a bytes: éxito y manejo de errores


>>> octetos = b'Montr\xe9al'
>>> octetos.decode('cp1252')
'Montreal'
>>> octetos.decode('iso8859_7')
'Montreal'
>>> octetos.decode('koi8_r')
'Montreal'
>>> octetos.decode('utf_8')
Rastreo (llamadas recientes más última):
Archivo "<stdin>", línea 1, en <módulo>
UnicodeDecodeError: el códec 'utf-8' no puede decodificar el byte 0xe9 en la posición
5: byte de continuación no válido >>> octets.decode('utf_8', errores='reemplazar')

'Montreal'

Estos bytes son los caracteres de “Montréal” codificados como latin1; '\xe9' es el byte para
“é”.

La decodificación con 'cp1252' (Windows 1252) funciona porque es un superconjunto


adecuado de latin1.

ISO-8859-7 está diseñado para griego, por lo que el byte '\xe9' se malinterpreta y no se emite
ningún error.

KOI8-R es para ruso. Ahora '\xe9' representa la letra cirílica “ÿ”.


El códec 'utf_8' detecta que los octetos no son UTF-8 válidos y genera Unicode DecodeError.

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.

Comprensión de los problemas de codificación/descodificación | 107


Machine Translated by Google

Error de sintaxis al cargar módulos con codificación inesperada UTF-8 es la codificación

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.

Ejemplo 4-8. ola.py: “¡Hola, mundo!” en portugues

# codificación: cp1252

print('¡Olá, Mundo!')

Ahora que el código fuente de Python 3 ya no está limitado a ASCII y


tiene por defecto la excelente codificación UTF-8, la mejor "solución" para
el código fuente en codificaciones heredadas como ' cp1252' es
convertirlas ya a UTF-8, y no molestarse con el comentarios de
codificación . Si su editor no es compatible con UTF-8, es hora de cambiar.

Nombres que no son ASCII en el código fuente: ¿debería usarlos?

Python 3 permite identificadores que no son ASCII en el código fuente:

>>> acción = 'PBR' # acción = acción # ÿ =


10**-6 épsilon >>> ÿ =

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.

108 | Capítulo 4: Texto versus Bytes


Machine Translated by Google

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.

Cómo descubrir la codificación de una secuencia de bytes ¿Cómo

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:

$ chardetect 04-text-byte.asciidoc 04-text-


byte.asciidoc: utf-8 con confianza 0.99

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.

Comprensión de los problemas de codificación/descodificación | 109


Machine Translated by Google

BOM: un gremlin útil


En el Ejemplo 4-5, es posible que haya notado un par de bytes adicionales al comienzo de una secuencia
codificada en UTF-16. Aquí están de nuevo:

>>> u16 = 'El Niño'.encode('utf_16') >>>


u16 b'\xff\xfeE\x00l\x00
\x00N\x00i\x00\xf1\x00o\x00'

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]

En una CPU big-endian, la codificación se invertiría; 'E' se codificaría como 0 y 69.

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:

>>> u16le = 'El Niño'.encode('utf_16le') >>>


list(u16le) [69, 0, 108, 0, 32, 0, 78, 0, 105, 0,
241, 0, 111, 0] >>> u16be = 'El Niño'.encode('utf_16be') >>>
lista(u16be) [0, 69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111]

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.

110 | Capítulo 4: Texto versus Bytes


Machine Translated by Google

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'.

Ahora pasamos al manejo de archivos de texto en Python 3.

Manejo de archivos de texto

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.

Manejo de archivos de texto | 111


Machine Translated by Google

Por lo tanto, usar archivos de texto es simple. Pero si confía en las codificaciones predeterminadas, será
mordido.

Considere la sesión de consola del ejemplo 4-9. ¿Puedes detectar el error?

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)

>>> open('café.txt', 'w', codificación='utf_8').write('café') 4

>>> open('cafe.txt').read() 'cafe'

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.

El código que tiene que ejecutarse en múltiples máquinas o en múltiples


ocasiones nunca debe depender de los valores predeterminados de
codificación. Pase siempre un argumento encoding= explícito al abrir
archivos de texto, porque el valor predeterminado puede cambiar de una
máquina a otra, o de un día a otro.

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

>>> fp = open('cafe.txt', 'w', codificación='utf_8') >>> fp


<_io.TextIOWrapper name='cafe.txt' mode='w'
codificación='utf_8'> > >> fp.write('cafe') 4 >>> fp.close() >>> import os
>>> os.stat('cafe.txt').st_size 5 >>> fp2 = open('cafe. TXT')

112 | Capítulo 4: Texto versus Bytes


Machine Translated by Google

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

<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='utf_8'>


>>> fp3.leer()
'café'
>>> fp4 = open('cafe.txt', 'rb') >>> fp4

<_io.BufferedReader name='cafe.txt'> >>>


fp4.read() b'caf\xc3\xa9'

De forma predeterminada, open funciona en modo de texto y devuelve un objeto TextIOWrapper .

El método de escritura en un TextIOWrapper devuelve el número de Unicode


caracteres escritos.

os.stat informa que el archivo contiene 5 bytes; UTF-8 codifica 'é' como 2 bytes, 0xc3
y 0xa9.

Abrir un archivo de texto sin codificación explícita devuelve un TextIOWrapper con el


codificación establecida en un valor predeterminado de la configuración regional.

Un objeto TextIOWrapper tiene un atributo de codificación que puede inspeccionar: cp1252


en este caso.

En la codificación de Windows cp1252 , el byte 0xc3 es una “Ô (A con tilde) y 0xa9
es el signo de derechos de autor.

Abriendo el mismo archivo con la codificación correcta.

El resultado esperado: los mismos cuatro caracteres Unicode para 'café'.

La bandera 'rb' abre un archivo para leer en modo binario.

El objeto devuelto es un BufferedReader y no un TextIOWrapper.

Lectura que devuelve bytes, como se esperaba.

No abra archivos de texto en modo binario a menos que necesite analizar


el contenido del archivo para determinar la codificación; incluso entonces, debe
estar usando Chardet en lugar de reinventar la rueda (ver “Cómo
Descubra la codificación de una secuencia de bytes” en la página 109). Ordina-
El código ry solo debe usar el modo binario para abrir archivos binarios, como
imágenes de trama

Manejo de archivos de texto | 113


Machine Translated by Google

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.

Valores predeterminados de codificación: un

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.

Ejemplo 4-11. Exploración de los valores predeterminados de codificación

sistema de importación , configuración regional

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

"""

mi_archivo = abrir('ficticio', 'w')

para expresión en expresiones.split(): valor =


eval(expresión) print(expresión.rjust(30), '-
>', repr(valor))

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'

En Windows, sin embargo, el resultado es el Ejemplo 4-12.

114 | Capítulo 4: Texto versus Bytes


Machine Translated by Google

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'

chcp muestra la página de códigos activa para la consola: 850.

Ejecutando default_encodings.py con salida a la consola.

locale.getpreferredencoding() es la configuración más importante.

Los archivos de texto usan locale.getpreferredencoding() de forma predeterminada.

La salida va a la consola, por lo que sys.stdout.isatty() es True.

Por lo tanto, sys.stdout.encoding es lo mismo que la codificación de la consola.

Si la salida se redirige a un archivo, así:

Z:\>python default_encodings.py > codificaciones.log

El valor de sys.stdout.isatty() se convierte en False, y sys.stdout.encoding se establece mediante


locale.getpreferredencoding(), 'cp1252' en esa máquina.

Tenga en cuenta que hay cuatro codificaciones diferentes en el Ejemplo 4-12:

• 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).

• La codificación de sys.stdout/stdin/stderr viene dada por la variable de entorno PYTHONIOENCODING ,


si está presente; de lo contrario, se hereda de la consola o se define mediante locale.getpreferredencoding()
si la salida/entrada se redirige a/desde un archivo .

Manejo de archivos de texto | 115


Machine Translated by Google

• 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".)

En GNU/Linux y OSX, todas estas codificaciones están configuradas en UTF-8 de forma


predeterminada y lo han sido durante varios años, por lo que la E/S maneja todos los
caracteres Unicode. En Windows, no solo se usan diferentes codificaciones en el mismo
sistema, sino que generalmente son páginas de códigos como 'cp850' o 'cp1252' que
admiten solo ASCII con 127 caracteres adicionales que no son los mismos de una
codificación a otra.
Por lo tanto, es mucho más probable que los usuarios de Windows enfrenten errores de
codificación a menos que tengan mucho cuidado.

Para resumir, la configuración de codificación más importante es la que devuelve locale.get


preferencoding(): es la predeterminada para abrir archivos de texto y para sys.stdout/stdin/stderr
cuando se redirigen a archivos. Sin embargo, la documentación dice (en parte):

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”.

7. La función sys.setdefaultencoding de Python 2 se usó incorrectamente y ya no está documentada en Python 3.


Estaba destinado a ser utilizado por los desarrolladores principales cuando la codificación interna predeterminada de
Python aún no estaba decidida. En el mismo subproceso comp.python.devel, Marc-André Lemburg afirma que el código
de usuario nunca debe llamar a la codificación sys.setdefaulten y que los únicos valores admitidos por CPython son
'ascii' en Python 2 y 'utf-8' en Python 3.

116 | Capítulo 4: Texto versus Bytes


Machine Translated by Google

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.

Normalización de Unicode para comparaciones más sanas

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 solución es utilizar la normalización Unicode, proporcionada por la función


unicodedata.normal ize . El primer argumento de esa función es una de cuatro cadenas:
'NFC', 'NFD', 'NFKC' y 'NFKD'. Comencemos con los dos primeros.

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:

>>> from unicodedata import normalize >>>


s1 = 'café' # "e" compuesta con acento agudo >>> s2 =
'cafe\u0301' # "e" descompuesta y acento agudo >>> len(s1), len
(s2) (4, 5) >>> len(normalizar('NFC', s1)), len(normalizar('NFC', s2))
(4, 4) >>> len(normalizar('NFC', s1)), len(normalizar('NFD', s2)) (5, 5)
>>> normalizar('NFC', s1) == normalizar('NFC', s2)

Verdadero

Normalización de Unicode para comparaciones más sanas | 117


Machine Translated by Google

>>> normalizar('NFD', s1) == normalizar('NFD', s2)


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:

>>> de la importación de datos Unicode normalizar, nombre


>>> ohm = '\u2126' >>> nombre (ohm)

'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.

118 | Capítulo 4: Texto versus Bytes


Machine Translated by Google

Así es como funciona el NFKC en la práctica:

>>> from unicodedata import normalize, name >>> half = '½'


>>> normalize('NFKC', half) '1ÿ2' >>> four_squared = '4²' >>>
normalize('NFKC', four_squared) '42' >>> micro = 'µ' >>> micro_kc
= normalize('NFKC', micro) >>> micro, micro_kc ('µ', 'ÿ') >>>
ord(micro), ord (micro_kc) (181, 956) >>> nombre(micro),
nombre(micro_kc)

('MICRO SIGNO', 'LETRA MU MINÚSCULA GRIEGA')

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'.

La normalización de NFKC y NFKD debe aplicarse con cuidado y solo en


casos especiales, por ejemplo, búsqueda e indexación, y no para
almacenamiento permanente, ya que estas transformaciones provocan la pérdida de datos.

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”:

>>> micro = 'µ' >>>


nombre(micro)
'MICRO SEÑAL'
>>> micro_cf = micro.casefold() >>>
nombre(micro_cf)

Normalización de Unicode para comparaciones más sanas | 119


Machine Translated by Google

'LETRA GRIEGA MINÚSCULA


MU' >>> micro, micro_cf ('µ', 'ÿ')
>>> eszett = 'ß' >>> nombre(eszett)

'LETRA S SOSTIDA MINÚSCULA


LATINA' >>> eszett_cf = eszett.casefold() >>>
eszett, eszett_cf ('ß', '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,

con suerte, funcione para la mayoría de los usuarios.

En las próximas dos secciones, pondremos en práctica nuestro conocimiento de normalización para desarrollar
funciones de utilidad.

Funciones de utilidad para coincidencia de texto normalizado Como

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.

Ejemplo 4-13. normeq.py: comparación de cadenas Unicode normalizadas


"""

Funciones de utilidad para la comparación de cadenas Unicode normalizadas.

Usando la forma normal C, distingue entre mayúsculas y minúsculas:

>>> s1 = 'café' >>>


s2 = 'café\u0301' >>> s1 == s2

Falso
>>> nfc_equal(s1, s2)
Verdadero

>>> nfc_equal('A', 'a')


Falso

Uso de la Forma C normal con plegado de cajas:

>>> s3 = 'Plaza' >>> s4


= 'Plaza'
>>> s3 == s4

120 | Capítulo 4: Texto versus Bytes


Machine Translated by Google

Falso
>>> nfc_equal(s3, s4)
Falso
>>> doblar_igual(s3, s4)

Verdadero >>> fold_equal(s1, s2)


Verdadero

>>> doblar_igual('A', 'a')


Verdadero

"""

desde unicodedata importar normalizar

def nfc_equal(str1, str2):


volver normalizar ('NFC', str1) == normalizar ('NFC', str2)

def fold_equal(str1, str2): return


(normalize('NFC', str1).casefold() == normalize('NFC',
str2).casefold())

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.

“Normalización” extrema: eliminación de signos diacríticos La salsa

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.

Normalización de Unicode para comparaciones más sanas | 121


Machine Translated by Google

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)

Descomponer todos los caracteres en caracteres básicos y marcas de combinación.

Filtre todas las marcas de combinación.

Recomponer todos los caracteres.

El ejemplo 4-15 muestra un par de usos de shave_marks.

Ejemplo 4-15. Dos ejemplos usando shave_marks del Ejemplo 4-14

>>> 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'

Solo se reemplazaron las letras “è”, “ç” e “í”.

Tanto "ÿ" como "é" fueron reemplazados.

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:

continuar # ignorar el diacrítico en la base latina char

122 | Capítulo 4: Texto versus Bytes


Machine Translated by Google

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)

Descomponer todos los caracteres en caracteres básicos y marcas de combinación.

Omita las marcas de combinación cuando el carácter base sea latino.

De lo contrario, mantenga el carácter actual.


Detecta un nuevo carácter base y determina si es latino.

Recomponer todos los caracteres.

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)

single_map = str.maketrans("""‚ƒ„†ˆ‹''“”•–—˜›""", """'f"*^<''""---


~>""")

multi_map = str.maketrans({ '€':


'<euro>',
'…': '...',
'Œ': 'OE',
'™': '(TM)',
'œ': 'oe',
'‰': '<por mil>',
'‡': '**',
})

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)

Cree una tabla de mapeo para el reemplazo de char a char.

Cree una tabla de mapeo para el reemplazo de char a string.

Normalización de Unicode para comparaciones más sanas | 123


Machine Translated by Google

Combinar tablas de mapeo.

dewinize no afecta el texto ASCII o latin1 , solo las adiciones de Microsoft a latin1 en cp1252.

Aplique dewinize y elimine las marcas diacríticas.

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.

El ejemplo 4-18 muestra el uso de asciize .

Ejemplo 4-18. Dos ejemplos usando asciize del Ejemplo 4-17

>>> 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".'

dewinize reemplaza las comillas, las viñetas y ™ (símbolo de marca


registrada). asciize aplica dewinize, elimina signos diacríticos y reemplaza la 'ß'.

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.

Esto concluye nuestra discusión sobre la normalización del texto Unicode.

El siguiente asunto de Unicode a resolver es... la clasificación.

Clasificación de texto Unicode

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.

Considere clasificar una lista de frutas cultivadas en Brasil:

124 | Capítulo 4: Texto versus Bytes


Machine Translated by Google

>>> frutas = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola'] >>> sorted(frutas)


['acerola', 'atemoia', 'açaí', 'caju', 'caja']

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 lista de frutas clasificadas debe ser:

['açaí', 'acerola', 'atemoia', 'cajá', '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 .

Ejemplo 4-19. Uso de la función locale.strxfrm como clave de clasificación

>>> import locale


>>> locale.setlocale(locale.LC_COLLATE, 'pt_BR.UTF-8')
'pt_BR.UTF-8' >>> frutas = ['caju', 'atemoia', 'cajá', ' açaí', 'acerola']
>>> sorted_fruits = sorted(fruits, key=locale.strxfrm) >>> sorted_fruits
['açaí', 'acerola', 'atemoia', 'cajá', 'caju']

Por lo tanto, debe llamar a setlocale(LC_COLLATE, «your_locale») antes de usar locale.strxfrm como clave al
ordenar.

Sin embargo, hay algunas advertencias:

• 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:

excepción de configuración regional no admitida .

• 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.

Clasificación de texto Unicode | 125


Machine Translated by Google

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.

Ordenar con el Algoritmo de intercalación Unicode James Tauber,

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.

Ejemplo 4-20. Usando el método pyuca.Collator.sort_key

>>> import pyuca


>>> coll = pyuca.Collator() >>>
frutas = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola'] >>> frutas_clasificadas =
clasificadas(frutas, key=coll.sort_key) >>> sorted_fruits ['açaí', 'acerola',
'atemoia', 'cajá', 'caju']

Esto es amigable y simplemente funciona. Lo probé en GNU/Linux, OSX y Windows. Solo se admite Python
3.X en este momento.

PyUCA no tiene en cuenta la configuración regional. Si necesita personalizar la clasificación, puede


proporcionar la ruta a una tabla de clasificación personalizada para el constructor Collator() . Fuera de

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.

126 | Capítulo 4: Texto versus Bytes


Machine Translated by Google

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.

La base de datos Unicode


El estándar Unicode proporciona una base de datos completa, en forma de numerosos
archivos de texto estructurados, que incluye no solo los puntos de código de asignación de
tablas a los nombres de los personajes, sino también metadatos sobre los caracteres
individuales y cómo se relacionan. Por ejemplo, la base de datos Unicode registra si un
carácter es imprimible, es una letra, es un dígito decimal o es algún otro símbolo numérico.
Así es como funcionan los métodos str isidentifier, isprintable, isdecimal e isnumeric .
str.casefold también usa información de una tabla Unicode.

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

Punto de código en formato U+0000 .

Carácter centralizado en una cadena de longitud 6.

Muestre re_dig si el carácter coincide con la expresión regular r'\d' .

Mostrar isdig si char.isdigit() es verdadero.

La base de datos Unicode | 127


Machine Translated by Google

Mostrar isnum si char.isnumeric() es verdadero.

Valor numérico con formato de ancho 5 y 2 decimales.


Nombre de carácter Unicode.

Ejecutar el Ejemplo 4-21 le da el resultado de la Figura 4-3.

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 sexta columna de la figura 4-3 es el resultado de llamar a unicodedata.numeric(char) en el


carácter. Muestra que Unicode conoce el valor numérico de los símbolos que representan números.
Entonces, si desea crear una aplicación de hoja de cálculo que admita dígitos tamiles o números
romanos, ¡hágalo!

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.

128 | Capítulo 4: Texto versus Bytes


Machine Translated by Google

API de str y bytes de modo dual


La biblioteca estándar tiene funciones que aceptan argumentos str o bytes y se comportan
diferente según el tipo. Algunos ejemplos están en los módulos re y os .

str versus bytes en expresiones regulares


Si crea una expresión regular con bytes, los patrones como \d y \w solo coinciden
caracteres ASCII; por el contrario, si estos patrones se dan como str, coinciden con Unicode
dígitos o letras más allá de ASCII. El ejemplo 4-22 y la figura 4-4 comparan cómo las letras, ASCII
los dígitos, los superíndices y los dígitos tamiles coinciden con los patrones str y bytes .

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+')

text_str = ("Ramanujan vio \u0be7\u0bed\u0be8\u0bef" como


"
1729 = 1³ + 12³ = 9³ + 10³).

bytes_texto = cadena_texto.encode('utf_8')

imprimir('Texto', repr(cadena_texto), sep='\n ')


imprimir('Números')
print(' str :', re_numbers_str.findall(text_str)) print(' bytes:',
re_numbers_bytes.findall(text_bytes)) print('Palabras')

print(' str :', re_words_str.findall(text_str)) print(' bytes:',


re_words_bytes.findall(text_bytes))

Las dos primeras expresiones regulares son del tipo str .

Los dos últimos son del tipo bytes .

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 str r'\d+' coincide con los dígitos Tamil y ASCII.

El patrón de bytes rb'\d+' coincide solo con los bytes ASCII para dígitos.

API de str y bytes de modo dual | 129


Machine Translated by Google

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.

Figura 4-4. Captura de pantalla de la ejecución de ramanujan.py del Ejemplo 4-22

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.

Otro módulo importante de modo dual es os.

str frente a bytes en funciones os El kernel de

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

130 | Capítulo 4: Texto versus Bytes


Machine Translated by Google

La característica le permite manejar cualquier archivo o nombre de ruta, sin importar cuántos gremlins pueda
encontrar. Vea el Ejemplo 4-23.

Ejemplo 4-23. listdir con str y bytes argumentos y resultados

>>> os.listdir('.') # ['abc.txt',


'digitos-de-ÿ.txt'] >>> os.listdir(b'.') #
[b'abc.txt', b'dígitos-de-\xcf\x80.txt']

El segundo nombre de archivo es "dígitos-de-ÿ.txt" (con la letra griega pi).

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.

fsdecode (nombre de archivo)


Decodifica el nombre del archivo (puede ser str o bytes) a str usando el códec nombrado por sys.get
filesystemencoding() si el nombre del archivo es de tipo bytes; de lo contrario, devuelve el nombre del
archivo str 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 .

Uso de escape sustituto para lidiar con Gremlins


Un truco para lidiar con bytes inesperados o codificaciones desconocidas es el controlador de errores de códec de
escape sustituto descrito en PEP 383 — Bytes no decodificables en las interfaces de caracteres del sistema
presentado en Python 3.1.

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.

Ejemplo 4-24. Uso del manejo de errores de Surrogatescape

>>> os.listdir('.')
['abc.txt', 'dígitos-de-ÿ.txt']

API de str y bytes de modo dual | 131


Machine Translated by Google

>>> 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'

Liste el directorio con un nombre de archivo que no sea ASCII.

Supongamos que no conocemos la codificación y obtengamos los nombres de archivo

como bytes. pi_names_bytes es el nombre de archivo con el carácter pi.

Descifrarlo a str usando el códec 'ascii' con 'surrogateescape'.

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!

Resumen del capítulo


Comenzamos el capítulo descartando la noción de que 1 carácter == 1 byte. A medida que el mundo adopta
Unicode (el 80 % de los sitios web ya usan UTF-8), debemos mantener el concepto de cadenas de texto
separadas de las secuencias binarias que las representan en los archivos, y Python 3 impone esta
separación.

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.

Luego consideramos la teoría y la práctica de la detección de codificación en ausencia de metadatos: en


teoría, no se puede hacer, pero en la práctica, el paquete Chardet lo logra bastante bien para una serie de
codificaciones populares. Luego se presentaron marcas de orden de bytes

132 | Capítulo 4: Texto versus Bytes


Machine Translated by Google

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

Lectura adicional | 133


Machine Translated by Google

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.

134 | Capítulo 4: Texto versus Bytes


Machine Translated by Google

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

¿Qué es el "texto sin formato"?

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

Lectura adicional | 135


Machine Translated by Google

ÿ 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.

¿Cómo se representan str en RAM?

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.

136 | Capítulo 4: Texto versus Bytes


Machine Translated by Google

PARTE III

Funciones como objetos


Machine Translated by Google
Machine Translated by Google

CAPÍTULO 5

Funciones de primera clase

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

clase" como una entidad de programa que puede ser:

• Creado en tiempo de ejecución

• Asignado a una variable o elemento en una estructura de datos • Pasado como

argumento a una función • Devuelto como resultado de una función

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.

El término "funciones de primera clase" se usa ampliamente como abreviatura


de "funciones como objetos de primera clase". No es perfecto porque parece
implicar una “élite” entre las funciones. En Python, todas las funciones son de
primera clase.

1. “Orígenes de las características funcionales de Python ”, del blog The History of Python de Guido.

139
Machine Translated by Google

Tratar una función como un objeto


La sesión de consola del Ejemplo 5-1 muestra que las funciones de Python son objetos. Aquí creamos una
función, la llamamos, leemos su atributo __doc__ y verificamos que el objeto de función en sí sea una instancia
de la clase de función .

Ejemplo 5-1. Cree y pruebe una función, luego lea su __doc__ y verifique su tipo

>>> def factorial(n):


... '''devuelve n!'''
... devuelve 1 si n < 2 sino n * factorial(n-1)
...
>>> factorial(42)
1405006117752879898543142606244511569936384000000000
>>> factorial.__doc__ 'devuelve n!' >>> tipo(factorial) <clase
'función'>

Esta es una sesión de consola, por lo que estamos creando una función en "tiempo de

ejecución". __doc__ es uno de varios atributos de los objetos de función.


factorial es una instancia de la clase de función .

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.

140 | Capítulo 5: Funciones de primera clase


Machine Translated by Google

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.

Funciones de orden superior


Una función que toma una función como argumento o devuelve una función como resultado es una función
de orden superior. Un ejemplo es el mapa, que se muestra en el Ejemplo 5-2. Otra es la función incorporada
sorted: un argumento clave opcional le permite proporcionar una función que se aplicará a cada elemento
para ordenar, como se ve en “list.sort y la función incorporada sorted” en la página 42.

Por ejemplo, para ordenar una lista de palabras por longitud, simplemente pase la función len como clave,
como en el Ejemplo 5-3.

Ejemplo 5-3. Ordenar una lista de palabras por longitud

>>> 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.

Ejemplo 5-4. Ordenar una lista de palabras por su ortografía inversa

>>> def reverse(word):


... return word[::-1] >>>
reverse('testing') 'gnitset' >>>
sorted(frutas, clave=reverse)
['banana', 'apple', ' higo', 'frambuesa',
'fresa', 'cereza']
>>>

En el paradigma de la programación funcional, algunas de las funciones de orden superior


más conocidas son mapear, filtrar, reducir y aplicar. La función de aplicación quedó obsoleta
en Python 2.3 y se eliminó en Python 3 porque ya no es necesaria. Si necesita llamar a un

Funciones de orden superior | 141


Machine Translated by Google

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.

Reemplazos modernos para mapear, filtrar y reducir


Los lenguajes funcionales comúnmente ofrecen el mapa, filtran y reducen funciones de orden superior.
ciones (a veces con diferentes nombres). Las funciones de mapa y filtro todavía están integradas en Python 3, pero
desde la introducción de listas de comprensión y generación de ejemplos.
presiones, no son tan importantes. Un listcomp o un genex hace el trabajo de mapa y
filtro combinado, pero es más legible. Considere el Ejemplo 5-5.

Ejemplo 5-5. Listas de factoriales producidos con mapa y filtro en comparación con alternativas
codificado como listas de comprensión

>>> lista(mapa(hecho, rango(6))) [1, 1,


2, 6, 24, 120]
>>> [hecho(n) para n en rango(6)] [1, 1,
2, 6, 24, 120]
>>> lista(mapa(factorial, filtro(lambda n: n % 2, rango(6)))) [1, 6, 120]

>>> [factorial(n) para n en rango(6) si n % 2] [1, 6, 120]

>>>

¡Construye una lista de factoriales desde 0! a las 5!.

Misma operación, con una lista de comprensión.

Lista de factoriales de números impares hasta el 5!, usando tanto mapa como filtro.

La comprensión de listas hace el mismo trabajo, reemplaza el mapa y el filtro, y hace


lambda innecesaria.

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

La función de reducción se degradó de una función integrada en Python 2 al módulo functools


en Python 3. Su caso de uso más común, la suma, es mejor atendido por la suma integrada disponible desde que
se lanzó Python 2.3 en 2003. Esta es una gran victoria en términos de legibilidad
y rendimiento (consulte el Ejemplo 5-6).

Ejemplo 5-6. Suma de enteros hasta 99 realizada con reduce y sum

>>> from functools import reduce >>>


from operator import add >>> reduce(add,
range(100))

142 | Capítulo 5: Funciones de primera clase


Machine Translated by Google

4950
>>> suma(rango(100))
4950
>>>

A partir de Python 3.0, reduce no está integrado.

Importe agregar para evitar crear una función solo para sumar dos números.

Sumar números enteros hasta 99.

Misma tarea usando sum; No se necesita importar o agregar funciones.

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.

Otros integrados reductores son all y any: all(iterable)

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

Funciones anónimas | 143


Machine Translated by Google

['plátano', 'manzana', 'higo', 'frambuesa', 'fresa', 'cereza']


>>>

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.

Receta de refactorización lambda de Lundh

Si encuentra un fragmento de código difícil de entender debido a una lambda, Fredrik Lundh
sugiere este procedimiento de refactorización:

1. Escribe un comentario explicando qué diablos hace lambda .

2. Estudie el comentario por un momento y piense en un nombre que capture la esencia de


el comentario.

3. Convierta la declaración lambda en una declaración de definición , usando ese nombre.

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.

Los siete sabores de los objetos invocables


El operador de llamada (es decir, ()) se puede aplicar a otros objetos más allá de las funciones definidas por el
usuario. Para determinar si un objeto es invocable, utilice la función integrada callable() .
La documentación del modelo de datos de Python enumera siete tipos a los que se

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 implementados en C, como dict.get.

Métodos

Funciones definidas en el cuerpo de una clase.

144 | Capítulo 5: Funciones de primera clase


Machine Translated by Google

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.

Dada la variedad de tipos invocables existentes en Python, la forma


más segura de determinar si un objeto es invocable es usar el calla
ble() incorporado:
>>> abs, str, 13
(<función incorporada abs>, <clase 'str'>, 13) >>>
[llamable(obj) for obj in (abs, str, 13)]
[Verdadero, Verdadero, Falso]

Ahora pasamos a crear instancias de clase que funcionan como objetos invocables.

Tipos invocables definidos por el usuario

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 __init__(uno mismo, elementos):


self._items = list(items)
random.shuffle(self._items)

Tipos invocables definidos por el usuario | 145


Machine Translated by Google

def pick(self): try:


return
self._items.pop() excepto
IndexError: raise
LookupError('seleccionar de BingoCage vacío')

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

aleatoria funcione porque self._items es una lista.


El método principal.

Genera una excepción con un mensaje personalizado si self._items está vacío.

Acceso directo a bingo.pick(): bingo().

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 = BingoCage(rango(3)) >>>


bingo.pick() 1

>>> 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__ ',

146 | Capítulo 5: Funciones de primera clase


Machine Translated by Google

'__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__',


'__init__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__ ', '__ne__', '__nuevo__',
'__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__']

>>>

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

Ejemplo 5-9. Listado de atributos de funciones que no existen en instancias simples

>>> clase C: pasar # >>>


obj = C() # >>> def func():
pasar # >>> sorted(set(dir(func))
- set(dir(obj))) # [ '__annotations__', '__call__', '__closure__',
'__code__', '__defaults__', '__get__', '__globals__', '__kwdefaults__', '__name__', '__qualname__']

>>>

Cree una clase simple definida por el usuario.

Haz una instancia de ello.

Crear una función desnuda.

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.

La tabla 5-1 muestra un resumen de los atributos enumerados en el ejemplo 5-9.

Introspección de funciones | 147


Machine Translated by Google

Tabla 5-1. Atributos de funciones definidas por el usuario

Nombre Escribe Descripción

Anotaciones de parámetros y retorno


__anotaciones__ dictado

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

__código__ código Metadatos de función y cuerpo de función compilados en código de bytes

__predeterminados__ tuple Valores por defecto para los parámetros formales

__obtener__ method-wrapper Implementación del protocolo de descriptor de solo lectura (consulte el Capítulo 20)

__globales__ dictar Variables globales del módulo donde se define la función

__kwdefaults__ dictado Valores predeterminados para los parámetros formales de solo palabra clave

calle El nombre de la función


__nombre__

__qualname__ calle El nombre calificado de la función, por ejemplo, Random.choice (ver PEP-3155)

Discutiremos las funciones __default__ , __code__ y __annotations__ , utilizadas por


IDE y marcos para extraer información sobre firmas de funciones, en secciones posteriores.
Pero para apreciar completamente estos atributos, haremos un desvío para explorar el poderoso
sintaxis Python ofrece declarar parámetros de funciones y pasarles argumentos.

De parámetros posicionales a parámetros solo de palabra clave

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

def etiqueta(nombre, *contenido, cls=Ninguno, **atributos):


"""Generar una o más etiquetas HTML"""
si cls no es Ninguno:
atributos['clase'] = cls
si atributos:
attr_str = ''.join(' %s="%s"' % (atributo, valor)
para atributo, valor
en ordenados (attrs.items()))
más:
''
attr_str = si el
contenido:
devuelve '\n'.join('<%s%s>%s</%s>' %
(nombre, attr_str, c, nombre) para c en el contenido)
más:
devuelve '<%s%s />' % (nombre, attr_str)

148 | Capítulo 5: Funciones de primera clase


Machine Translated by Google

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 .

El parámetro cls solo se puede pasar como un argumento de palabra clave.

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í:

>>> def f(a, *, b):


... volver a, b
...
>>> f(1, b=2)
(1, 2)

De parámetros posicionales a parámetros de solo palabra clave | 149


Machine Translated by Google

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.

Recuperación de información sobre parámetros


Una aplicación interesante de la introspección de funciones se puede encontrar en el micro-marco Bobo
HTTP. Para verlo en acción, considere una variación de la aplicación "Hola mundo" del tutorial de Bobo en
el Ejemplo 5-12.

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

$ curl -i http://localhost:8080/ HTTP/1.0


403 Fecha prohibida: jueves, 21 de
agosto de 2014 21:39:44 GMT Servidor:
WSGIServer/0.2 CPython/3.4.1 Tipo de
contenido: text/html; charset=UTF-8 Contenido-
Longitud: 103

<html>
<head><title>Parámetro faltante</title></head>
<body>Persona variable de formulario faltante</body> </
html>

150 | Capítulo 5: Funciones de primera clase


Machine Translated by Google

Sin embargo, si obtiene http://localhost:8080/?person=Jim, la respuesta será la cadena '¡Hola, Jim!'. Vea el
Ejemplo 5-14.

Ejemplo 5-14. Se requiere pasar el parámetro de persona para una respuesta OK

$ curl -i http://localhost:8080/?person=Jim HTTP/1.0 200 OK


Fecha: jueves, 21 de agosto de 2014 21:42:32 GMT Servidor:
WSGIServer/0.2 CPython/3.4.1 Tipo de contenido: texto/ html;
charset=UTF-8 Contenido-Longitud: 10

¡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

clip de definición (texto, max_len = 80):


"""Retorna el texto recortado en el último espacio antes o después de max_len
"""

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

El ejemplo 5-16 muestra los valores de __defaults__, __code__.co_varnames y __code__.co_argcount para


la función de recorte enumerada en el ejemplo 5-15.

Ejemplo 5-16. Extraer información sobre los argumentos de la función

>>> desde clip importar clip >>>


clip.__predeterminado__

Recuperación de información sobre parámetros | 151


Machine Translated by Google

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

Afortunadamente, hay una mejor manera: el módulo de inspección .

Eche un vistazo al ejemplo 5-17.

Ejemplo 5-17. Extrayendo la firma de la función


>>> desde clip importar clip
>>> desde inspeccionar importar firma
>>> sig = firma(clip) >>> sig # doctest: +
ELIPSIS <inspeccionar. Objeto de firma
en 0x...> >>> str(sig) '(texto, max_len=80)'
>>> para nombre, parámetro en
sig.parameters.items ():

... print(param.tipo, ':', nombre, '=', param.predeterminado)


...
POSITIONAL_OR_KEYWORD: texto = <clase 'inspeccionar._empty'>
POSITIONAL_OR_KEYWORD: max_len = 80

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

152 | Capítulo 5: Funciones de primera clase


Machine Translated by Google

VAR_POSITIONAL
Una tupla de parámetros posicionales.

VAR_KEYWORD

Un dict de parámetros de palabras clave.

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

Un objeto inspect.Signature tiene un método de vinculación que toma cualquier número de


argumentos y los vincula a los parámetros de la firma, aplicando las reglas habituales para hacer
coincidir los argumentos reales con los parámetros formales. Esto puede ser utilizado por un marco
para validar argumentos antes de la invocación de la función real. El ejemplo 5-18 muestra cómo.

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 )

Rastreo (llamadas recientes más última):


...
TypeError: parámetro 'nombre' sin valor predeterminado

Obtenga la firma de la función de etiqueta en el Ejemplo 5-10.

Pase un dict de argumentos a .bind().

Recuperación de información sobre parámetros | 153


Machine Translated by Google

Se genera un objeto inspect.BoundArguments .

Iterar sobre los elementos enbound_args.arguments, que es un OrderedDict, para mostrar los
nombres y valores de los argumentos.

Elimine el nombre de argumento obligatorio de my_tag.

Llamar a sig.bind(**my_tag) genera un TypeError quejándose del parámetro de nombre faltante .

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.

Ejemplo 5-19. Función de clip anotado

def clip(texto:str, max_len:'int > 0'=80) -> str:


"""Retorna el texto recortado en el último espacio antes o después de max_len
"""
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()

La declaración de función anotada.

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

154 | Capítulo 5: Funciones de primera clase


Machine Translated by Google

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.

No se realiza ningún procesamiento con las anotaciones. Simplemente se almacenan en el atributo


__anotaciones__ de la función, un dictado:

>>> from clip_annot import clip >>>


clip.__annotations__ {'text': <class
'str'>, 'max_len': 'int > 0', 'return': <class 'str'>}

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 .

Ejemplo 5-20. Extraer anotaciones de la firma de la función


>>> from clip_annot import clip >>>
from inspect import signature >>> sig
= signature(clip) >>> sig.return_annotation
<class 'str'> >>> for param in
sig.parameters.values():

... nota = repr(parámetro.anotación).ljust(13)


... print(nota, ':', param.nombre, '=', param.predeterminado)
<clase 'str'> : texto = <clase 'inspect._empty'> 'int > 0' :
max_len = 80

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.

Anotaciones de funciones | 155


Machine Translated by Google

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.

Paquetes para Programación Funcional


Aunque Guido deja en claro que Python no pretende ser un lenguaje de programación funcional,
se puede usar un estilo de codificación funcional en buena medida, gracias al soporte de paquetes
como operator y functools, que cubrimos en las próximas dos secciones.

El módulo del operador A

menudo, en la programación funcional, es conveniente utilizar un operador aritmético como


función. Por ejemplo, suponga que desea multiplicar una secuencia de números para calcular
factoriales sin usar la recursividad. Para realizar la suma, puede usar la suma, pero no existe una
función equivalente para la multiplicación. Podría usar reduce, como vimos en “Reemplazos
modernos para mapear, filtrar y reducir” en la página 142, pero esto requiere una función para
multiplicar dos elementos de la secuencia. El ejemplo 5-21 muestra cómo resolver esto usando
lambda.

Ejemplo 5-21. Factorial implementado con reduce y una función anónima.


de functools importar reducir

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.

Ejemplo 5-22. Factorial implementado con reduce y operator.mul


desde functools importar reducir
desde operador importar mul

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.

156 | Capítulo 5: Funciones de primera clase


Machine Translated by Google

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:

>>> cc_name = itemgetter(1, 0) >>>


para ciudad en metro_data:
... print(cc_name(ciudad))
...
('JP', 'Tokio')
('IN', 'Delhi NCR')
('MX', 'Ciudad de México')
('EE. UU.', 'Nueva York-Newark')
('BR', 'Sao Paulo')
>>>

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)

>>> desde colecciones import namedtuple


>>> LatLong = tupla nombrada('LatLong', 'lat long') #
>>> Metropolis = namedtuple('Metropolis', 'name cc pop coord') #

Paquetes para Programación Funcional | 157


Machine Translated by Google

>>> áreas_metro = [Metrópolis(nombre, cc, pop, LatLong(lat, long)) #


... for name, cc, pop, (lat, long) in metro_data] >>>
metro_areas[0]
Metropolis(name='Tokyo', cc='JP', pop=36.933, coord=LatLong(lat=35.689722,
long=139.691667)) >>> áreas_metro[0].coord.lat # 35.689722

>>> from operator import attrgetter >>>


name_lat = attrgetter('name', 'coord.lat') #
>>>
>>> for city in sorted(metro_areas, key=attrgetter('coord.lat')): #
... imprimir(nombre_lat(ciudad)) #
...
('Sao Paulo', -23.547778)
('Ciudad de México', 19.433333)
('Delhi NCR', 28.613889)
('Tokio', 35.689722)
('Nueva York-Newark', 40.808611)

Use namedtuple para definir LatLong.

Defina también Metrópolis.

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.

Acceda al elemento metro_areas[0] para obtener su latitud.

Defina un attrgetter para recuperar el nombre y el atributo anidado coord.lat .

Use attrgetter nuevamente para ordenar la lista de ciudades por latitud.

Utilice el attrgetter definido en para mostrar solo el nombre de la ciudad y la latitud.

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

>>> [nombre para nombre en dir(operador) si no nombre.comienza con('_')]


['abs', 'agregar', 'y_', 'attrgetter', 'concat', 'contains', 'countOf ', 'delitem', 'eq',
'floordiv', 'ge', 'getitem', 'gt', 'iadd', 'iand', 'iconcat', 'ifloordiv', 'ilshift', 'imod', 'imul',
'index', 'indexOf', 'inv', 'invert', 'ior', 'ipow', 'irshift', 'is_', 'is_not', 'isub', 'itemgetter',
'itruediv ', 'ixor', 'le', 'length_hint', 'lshift', 'lt', 'methodcaller', 'mod', 'mul', 'ne', 'neg',
'not_', 'or_', 'pos', 'pow', 'rshift', 'setitem', 'sub', 'truediv', 'verdad', 'xor']

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

158 | Capítulo 5: Funciones de primera clase


Machine Translated by Google

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

>>> from operator import methodcaller >>> s = 'Ha


llegado el momento' >>> upcase = methodcaller('upper')
>>> upcase(s)

'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.

Congelación de argumentos con functools.partial El módulo

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

>>> from operator importar mul >>>


from functools importar parcial >>> triple =
parcial(mul, 3)

Paquetes para Programación Funcional | 159


Machine Translated by Google

>>> triple(7) 21

>>> lista(mapa(triple, rango(1, 10))) [3, 6, 9,


12, 15, 18, 21, 24, 27]

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

>>> import unicodedata, functools >>>


nfc = functools.partial(unicodedata.normalize, 'NFC') >>> s1 = 'café'

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

>>> desde la etiqueta de


importación del etiquetador >>>
etiqueta <etiqueta de función en
0x10206d1e0> >>> desde las
herramientas de función importar parcial >>> imagen = parcial
(etiqueta, 'img', cls = 'foto-marco') >>> imagen
( src='wumpus.jpeg') '<img class="pic-frame" src="wumpus.jpeg" /
>' >>> picture functools.partial(<etiqueta de función en
0x10206d1e0>, 'img', cls=' pic-frame') >>> picture.func <etiqueta de función en
0x10206d1e0> >>> picture.args ('img',)

160 | Capítulo 5: Funciones de primera clase


Machine Translated by Google

>>> imagen.palabras
clave {'cls': 'pic-frame'}

Importe la etiqueta del Ejemplo 5-10 y muestre su ID.

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

se esperaba. parcial() devuelve un objeto functools.partial.2 Un objeto functools.partial

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.

Resumen del capítulo


El objetivo de este capítulo fue explorar la naturaleza de primera clase de las funciones en Python.
Las ideas principales son que puede asignar funciones a las variables, pasarlas a otras funciones,
almacenarlas en estructuras de datos y acceder a los atributos de las funciones, lo que permite que
los marcos y las herramientas actúen sobre esa información. Las funciones de orden superior, un
elemento básico de la programación funcional, son comunes en Python, incluso si el uso de map,
filter y reduce no es tan frecuente como lo era, gracias a las listas de comprensión (y construcciones
similares como generador de expresiones). ) y la apariencia de funciones integradas reductoras
como sum, all y any. Los incorporados sorted, min, max y functools.partial son ejemplos de funciones
de orden superior comúnmente utilizadas en el lenguaje.

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.

Resumen del capítulo | 161


Machine Translated by Google

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.

Una excelente introducción a la programación funcional en Python es el CÓMO de programación funcional


en Python de AM Kuchling . Sin embargo, el enfoque principal de ese texto está en el uso de iteradores y
generadores, que son el tema del Capítulo 14.

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.

162 | Capítulo 5: Funciones de primera clase


Machine Translated by Google

La pregunta de StackOverflow "Python: ¿Por qué es necesario functools.partial?" tiene una


respuesta muy informativa (y divertida) de Alex Martelli, autor del clásico Python en pocas palabras.

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.

¿Python es un lenguaje funcional?

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”.

Shriram Krishnamurthi, profesor de Ciencias de la Computación en la Universidad de Brown, inicia su


"Enseñanza de lenguajes de programación en una era post-linneana" con esto:

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?

En ese documento, Python se menciona por su nombre en este pasaje:

Lectura adicional | 163


Machine Translated by Google

¿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.

Irónicamente, robar la sintaxis de comprensión de listas de otro lenguaje funcional, Haskell,


disminuyó significativamente la necesidad de mapas y filtros, y también de lambda.

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.

El problema con las funciones anónimas Más

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.

164 | Capítulo 5: Funciones de primera clase


Machine Translated by Google

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.

Lectura adicional | 165


Machine Translated by Google
Machine Translated by Google

CAPÍTULO 6

Patrones de diseño con funciones de primera clase

La conformidad con los patrones no es una medida de bondad.1

—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.

Los autores de Design Patterns reconocen en su Introducción que el lenguaje de implementación


determina qué patrones son relevantes:

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.

Estudio de caso: estrategia de refactorización

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

El patrón de estrategia se resume así en Patrones de diseño:

Defina una familia de algoritmos, encapsule cada uno y hágalos intercambiables.


La estrategia permite que el algoritmo varíe independientemente de los clientes que lo utilicen.

168 | Capítulo 6: Patrones de diseño con funciones de primera clase


Machine Translated by Google

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.

Considere una tienda en línea con estas reglas de descuento:

• 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

pedidos con al menos 10 artículos distintos obtienen un 7% de descuento global.

Para abreviar, supongamos que solo se puede aplicar un descuento a un pedido.

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.

Ejemplo 6-1. Clase de pedido de implementación con estrategias de descuento conectables


from abc import ABC, abstractmethod
from collections import namedtuple

Cliente = namedtuple('Cliente', 'fidelidad de nombre')

elemento de línea de clase :

def __init__(auto, producto, cantidad, precio):


self.product = producto
self.quantity = cantidad
self.price = precio

Estudio de caso: estrategia de refactorización | 169


Machine Translated by Google

def total(self): return


self.price * self.quantity

orden de clase : # el contexto

def __init__(self, cliente, carrito, promoción=Ninguno):


self.cliente = cliente self.cart =
lista(carrito) self.promotion =
promoción

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
auto.promocion es Ninguno: descuento
= 0 otro: descuento =
auto.promocion.descuento(auto)
return auto.total() - descuento

def __repr__(self): fmt =


'<Total del pedido: {:.2f} vencido: {:.2f}>' return
fmt.format(self.total(), self.due())

promoción de clase (ABC): # la estrategia: una clase base abstracta

@abstractmethod def
discount(self, order):
"""Descuento de devolución como cantidad positiva en dólares"""

class FidelityPromo(Promoción): # primera estrategia concreta


"""5% de descuento para clientes con 1000 o más puntos de fidelidad"""

def descuento(auto, pedido):


return order.total() * .05 if order.customer.fidelity >= 1000 else 0

class BulkItemPromo(Promoción): # segunda estrategia concreta


"""10% de descuento por cada LineItem con 20 o más unidades"""

def descuento(auto, pedido): descuento


=0
para el artículo en order.cart:
si artículo.cantidad >= 20:
descuento += artículo.total() * .1 devolución
de descuento

170 | Capítulo 6: Patrones de diseño con funciones de primera clase


Machine Translated by Google

class LargeOrderPromo(Promoción): # tercera estrategia concreta


"""7% de descuento para pedidos con 10 o más artículos distintos"""

def descuento(auto, pedido):


distintos_elementos = {elemento.producto para elemento en order.cart}
si len(elementos_distintos) >= 10:
devolver pedido.total() * .07
volver 0

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.

En Python 3.4, la forma más sencilla de declarar un ABC es subclasificar


abc.ABC, como hice en el Ejemplo 6-1. De Python 3.0 a 3.3, debe
use la palabra clave metaclass= en la declaración de clase (por ejemplo, clase
Promoción(metaclase=ABCMeta):).

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

>>> joe = Cliente('John Doe', 0) >>> ann


= Cliente('Ann Smith', 1100)
>>> carro = [LineItem('banana', 4, .5),
... LineItem('manzana', 10, 1.5),
... LineItem('sandía', 5, 5.0)]
>>> Pedido(joe, carro, FidelityPromo())
<Total del pedido: 42,00 vencimiento: 42,00>
>>> Pedido(ann, carro, FidelityPromo())
<Total del pedido: 42,00 vencimiento: 39,90>
>>> banana_cart = [LineItem('banana', 30, .5),
... LineItem('manzana', 10, 1.5)]
>>> Orden(joe, banana_cart, BulkItemPromo())
<Total del pedido: 30,00 vencimiento: 28,50>
>>> long_order = [LineItem(str(item_code), 1, 1.0) for item_code
... in range(10)]
>>> Orden(joe, long_order, LargeOrderPromo())
<Total del pedido: 10.00 vencimiento: 9.30>
>>> Orden(joe, carro, LargeOrderPromo())
<Total del pedido: 42,00 vencimiento: 42,00>

Dos clientes: Joe tiene 0 puntos de fidelidad, Ann tiene 1100.

Un carrito de compras con tres artículos de línea.

La promoción FidelityPromo no ofrece ningún descuento a joe.

Estudio de caso: estrategia de refactorización | 171


Machine Translated by Google

ann obtiene un 5% de descuento porque tiene al menos 1000 puntos.


El banana_cart tiene 30 unidades del producto "banana" y 10 manzanas.

Gracias a BulkItemPromo, joe obtiene un descuento de $1.50 en las bananas.


long_order tiene 10 artículos diferentes a $1.00 cada uno.

joe obtiene un 7 % de descuento en todo el pedido gracias a LargerOrderPromo.

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 orientada a funciones Cada

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

desde colecciones importar namedtuple

Cliente = namedtuple('Cliente', 'fidelidad de nombre')

elemento de línea de clase :

def __init__(auto, producto, cantidad, precio):


self.product = producto
self.quantity = cantidad
self.price = precio

def total(self):
return self.price * self.quantity

orden de clase : # el contexto

def __init__(self, cliente, carrito, promoción=Ninguno):


self.cliente = cliente self.cart
= lista(carrito) self.promotion
= promoción

def total(self): if
not hasattr(self, '__total'): self.__total
= sum(item.total() for item in self.cart) return self.__total

def debido(auto):

172 | Capítulo 6: Patrones de diseño con funciones de primera clase


Machine Translated by Google

si self.promotion es None:
descuento = 0 else:

descuento = self.promotion(self)
volver self.total() - descuento

def __repr__(self): fmt


= '<Total del pedido: {:.2f} vencido: {:.2f}>' return
fmt.format(self.total(), self.due())

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:

si artículo.cantidad >= 20:


descuento += artículo.total() * .1
devolución de descuento

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

Para calcular un descuento, simplemente llame a la función self.promotion() .

Ninguna clase abstracta.

Cada estrategia es una función.

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

>>> joe = Cliente('John Doe', 0) >>> ann


= Cliente('Ann Smith', 1100) >>> carro =
[LineItem('banana', 4, .5), LineItem('manzana ',
... 10, 1.5), LineItem('sandía', 5, 5.0)]
...
>>> Pedido(joe, carro, fidelity_promo)
<Total del pedido: 42,00 vencimiento:
42,00> >>> Pedido(ann, cart, fidelity_promo)
<Total del pedido: 42,00 vencimiento: 39,90>

Estudio de caso: estrategia de refactorización | 173


Machine Translated by Google

>>> banana_cart = [LineItem('banana', 30, .5),


... LineItem('manzana', 10, 1.5)]
>>> Pedido(joe, banana_cart, bulk_item_promo)
<Total del pedido: 30,00 debido:
28,50> >>> long_order = [LineItem(str(item_code), 1, 1.0) for
... item_code in range(10)]
>>> Pedido(joe, pedido_largo, pedido_grande_promo)
<Total del pedido: 10.00 vencimiento: 9.30>
>>> Pedido(joe, carro, gran_pedido_promo)
<Total del pedido: 42,00 vencimiento: 42,00>

Los mismos accesorios de prueba que el Ejemplo 6-1.

Para aplicar una estrategia de descuento a un pedido, simplemente pase la función de promoción como
argumento.

Aquí y en la siguiente prueba se utiliza una función de promoción diferente.

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

3. Consulte la página 323 de Patrones de diseño.

4. ídem, pág. 196

174 | Capítulo 6: Patrones de diseño con funciones de primera clase


Machine Translated by Google

que implementan este requisito utilizando una variedad de enfoques que aprovechan funciones y módulos
como objetos.

Elección de la mejor estrategia: enfoque simple Dados los mismos

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

>>> Pedido(joe, long_order, best_promo)


<Total del pedido: 10.00 vencimiento: 9.30>
>>> Pedido (joe, banana_cart, best_promo)
<Total del pedido: 30,00 vencimiento: 28,50>
>>> Pedido(ann, cart, best_promo)
<Total del pedido: 42,00 vencimiento: 39,90>

best_promo seleccionó la promoción de pedido más grande para el cliente joe.

Aquí Joe obtuvo el descuento de bulk_item_promo por pedir muchas bananas.

Al realizar el pago con un carrito simple, best_promo le dio al cliente leal a Ann el descuento por
fidelity_promo.

La implementación de best_promo es muy sencilla. Vea el Ejemplo 6-6.

Ejemplo 6-6. best_promo encuentra el descuento máximo iterando sobre una lista de funciones

promociones = [fidelity_promo, bulk_item_promo, large_order_promo]

def mejor_promo(pedido):
"""Seleccione el mejor descuento disponible
"""

return max(promo(order) for promo in promos)

promos: lista de las estrategias implementadas como funciones.

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

Estudio de caso: estrategia de refactorización | 175


Machine Translated by Google

recuerde agregarlo a la lista de promociones , de lo contrario, la nueva promoción funcionará cuando se pase explícitamente

como un argumento a Order, pero no será considerada por best_promotion.

Siga leyendo para conocer un par de soluciones a este problema.

Encontrar estrategias en un módulo Los módulos

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.

promos = [globals()[name] for name in globals() if


name.endswith('_promo') and name !=
'best_promo']

def mejor_promo(pedido):
"""Seleccione el mejor descuento disponible
"""
return max(promo(order) for promo in promos)

Itere sobre cada nombre en el diccionario devuelto por globals().

Seleccione solo nombres que terminen con el sufijo _promo .

Filtre best_promo en sí mismo, para evitar una repetición infinita.

No hay cambios dentro de best_promo.

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

176 | Capítulo 6: Patrones de diseño con funciones de primera clase


Machine Translated by Google

Ejemplo 6-8. La lista de promociones se construye mediante la introspección de un nuevo módulo de promociones.

promociones = [función para nombre, función


en inspeccionar.obtenermiembros(promociones, inspeccionar.función)]

def mejor_promo(pedido):
"""Seleccione el mejor descuento disponible
"""

return max(promo(order) for promo in promos)

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 ,

best_promo fallaría al intentar aplicarla a un pedido.

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.

178 | Capítulo 6: Patrones de diseño con funciones de primera clase


Machine Translated by Google

Ejemplo 6-9. Cada instancia de MacroCommand tiene una lista interna de comandos
macrocomando de clase :
"""Un comando que ejecuta una lista de comandos"""

def __init__(auto, comandos):


self.comandos = lista(comandos) #

def __call__(self): for


comando en self.comandos: # comando()

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__.

Resumen del capítulo


Como señaló Peter Norvig un par de años después de que apareciera el libro clásico Design Patterns , "16 de
23 patrones tienen una implementación cualitativamente más simple en Lisp o Dylan que en C++ para al menos
algunos usos de cada patrón" (diapositiva 9 de Norvig's "Design Patterns en Lenguajes Dinámicos” presentación).
Python comparte algunas de las características dinámicas de los lenguajes Lisp y Dylan, en particular funciones
de primera clase, nuestro enfoque en esta parte del libro.

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

Resumen del capítulo | 179


Machine Translated by Google

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.

180 | Capítulo 6: Patrones de diseño con funciones de primera clase


Machine Translated by Google

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.

Lectura adicional | 181


Machine Translated by Google

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.

182 | Capítulo 6: Patrones de diseño con funciones de primera clase


Machine Translated by Google

CAPÍTULO 7

Decoradores de Funciones y Cerramientos

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.

— PEP 318 — Decoradores para Funciones y Métodos

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:

• Cómo evalúa Python la sintaxis del decorador •

Cómo decide Python si una variable es local • Por qué existen

los cierres y cómo funcionan

1. Ese es el libro Design Patterns de 1995 de la llamada Gang of Four.

183
Machine Translated by Google

• ¿Qué problema se resuelve con métodos no locales ?

Con esta base, podemos abordar otros temas de decoración:

• Implementar un decorador de buen comportamiento

• Decoradores interesantes en la biblioteca estándar •

Implementar un decorador parametrizado

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.

En otras palabras, asumiendo un decorador existente llamado decorar, este código:

@decorate
def target():
print('objetivo en ejecución()')

Tiene el mismo efecto que escribir esto:

def objetivo():
print('objetivo en ejecución()')

objetivo = decorar (objetivo)

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

2. Python también admite decoradores de clase. Se tratan en el Capítulo 21.

184 | Capítulo 7: Decoradores de funciones y cierres


Machine Translated by Google

... imprimir ('objetivo en ejecución ()')


...
>>> objetivo()
ejecutando interior()
>>> destino
<función deco.<locales>.inner en 0x10063b598>

deco devuelve su objeto de función interna .

el objetivo está decorado con deco.

La invocación del objetivo decorado en realidad se ejecuta internamente.

La inspección revela que el objetivo ahora es una referencia al interior.

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.

Cuando Python ejecuta decoradores


Una característica clave de los decoradores es que se ejecutan justo después de definir la función decorada.
Eso suele ser en el momento de la importación (es decir, cuando Python carga un módulo). Considerar
registro.py en el Ejemplo 7-2.

Ejemplo 7-2. El módulo registration.py

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

Cuando Python ejecuta decoradores | 185


Machine Translated by Google

print('ejecutando main()')
print('registro ->', registro) f1() f2() f3()

si __nombre__=='__principal__':
principal()

el registro contendrá referencias a las funciones decoradas por @register.

registro toma una función como argumento.


Muestre qué función se está decorando, para demostración.
Incluir función en el registro.
Return func: debemos devolver una función; aquí devolvemos lo mismo recibido como
argumento. f1 y f2 están decorados por @register. f3 no está decorado.

main muestra el registro, luego llama a f1(), f2() y f3(). main() solo

se invoca si registration.py se ejecuta como un script.

El resultado de ejecutar registration.py como un script se ve así:

$ python3 registration.py ejecutando


el registro (<función f1 en 0x100631bf8>) ejecutando el registro
(<función f2 en 0x100631c80>) ejecutando main() registro ->
[<función f1 en 0x100631bf8>, <función f2 en 0x100631c80>]
ejecutando f1( ) ejecutando f2() ejecutando f3()

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 .

Si se importa registration.py (y no se ejecuta como un script), el resultado es este:

>>> registro de importación


registro en ejecución (<función f1 en 0x10063b1e0>) registro en
ejecución (<función f2 en 0x10063b268>)

En este momento, si observa el registro, esto es lo que obtiene:

186 | Capítulo 7: Decoradores de funciones y cierres


Machine Translated by Google

>>> registro.registro [<función


f1 en 0x10063b1e0>, <función f2 en 0x10063b268>]

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:

• La función decoradora se define en el mismo módulo que las funciones decoradas.


Un decorador real generalmente se define en un módulo y se aplica a funciones en otros módulos.

• El decorador de registros devuelve la misma función pasada como argumento. En la práctica, la


mayoría de los decoradores definen una función interna y la devuelven.

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.

Patrón de estrategia mejorado por decorador


Un decorador de registro es una buena mejora para el descuento promocional de comercio electrónico del
“Estudio de caso: estrategia de refactorización” en la página 168.

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.

Ejemplo 7-3. La lista de promociones la completa el decorador de promociones.

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"""

Patrón de estrategia mejorado por decorador | 187


Machine Translated by Google

return order.total() * .05 if order.customer.fidelity >= 1000 else 0

@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:

si artículo.cantidad >= 20:


descuento += artículo.total() * .1 devolución
de descuento

@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:

devolver pedido.total() * .07


volver 0

def mejor_promo(pedido):
"""Seleccione el mejor descuento disponible
"""

return max(promo(order) for promo in promos)

La lista de promociones comienza

vacía. El decorador de promociones devuelve promo_func sin cambios, después de agregarlo a la lista
de promociones .

Cualquier función decorada por @promotion se agregará a las promos.

No se necesitan cambios en best_promos, porque se basa en 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 ).

• El decorador @promotion resalta el propósito de la función decorada y también facilita la desactivación


temporal de una promoción: simplemente comente la decÿ
orador.

• 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ÿ

188 | Capítulo 7: Decoradores de funciones y cierres


Machine Translated by Google

Por supuesto, debemos dar un paso atrás y observar de cerca cómo funcionan los ámbitos variables
en Python.

Reglas de alcance variable


En el ejemplo 7-4, definimos y probamos una función que lee dos variables: una variable local a,
definida como parámetro de función, y una variable b que no está definida en ninguna parte de la
función.

Ejemplo 7-4. Función que lee una variable local y una global

>>> def f1(a):


...
... imprimir(a) imprimir(b)
...
>>> f1(3)
3
Rastreo (llamadas recientes más última):
Archivo "<stdin>", línea 1, en <módulo>
Archivo "<stdin>", línea 3, en f1
NameError: el nombre global 'b' no está definido

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

Ahora, veamos un ejemplo que puede sorprenderte.

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.

Ejemplo 7-5. La variable b es local, porque se le asigna un valor en el cuerpo de la funció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

Reglas de alcance variable | 189


Machine Translated by Google

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 :

>>> def f3(a):


... global b
...
...
... imprimir(a) imprimir(b) b = 9
...
>>> f3(3)
3
6

>>> 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.

190 | Capítulo 7: Decoradores de funciones y cierres


Machine Translated by Google

Comparación de códigos de bytes

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.

Ejemplo 7-6. Desmontaje de la función f1 del ejemplo 7-4


>>> de des import des
>>> des(f1)
2 0 CARGAR_GLOBAL 0 (imprimir) 0
3 CARGAR_RÁPIDO (a) 1 (1
6 FUNCIÓN_LLAMADA posicional, 0 par de palabras clave)
9 POP_ARRIBA

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

Cargue la impresión de nombre global.

Cargar nombre local a.

Cargar nombre global b.

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.

Ejemplo 7-7. Desmontaje de la función f2 del Ejemplo 7-5


>>> des(f2)
2 0 CARGAR_GLOBAL 0 (imprimir)
3 CARGAR_RÁPIDO 0 (un)
6 FUNCIÓN_LLAMADA 1 (1 posicional, 0 par de palabras clave)
9 POP_ARRIBA

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

Reglas de alcance variable | 191


Machine Translated by Google

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.

Este es un concepto difícil de entender, y se aborda mejor a través de un ejemplo.

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

¿De dónde viene avg y dónde guarda el historial de valores anteriores?

Para empezar, el Ejemplo 7-8 es una implementación basada en clases.

Ejemplo 7-8. promedio_oo.py: una clase para calcular un promedio móvil

clase Promediador():

def __init__(self):
self.serie = []

def __call__(self, nuevo_valor):

192 | Capítulo 7: Decoradores de funciones y cierres


Machine Translated by Google

self.series.append(new_value) total =
sum(self.series) return total/len(self.series)

La clase Averager crea instancias a las que se puede llamar:

>>> promedio = Promediador()


>>> promedio(10) 10.0

>>> 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.

Ejemplo 7-10. Ejemplo de prueba 7-9


>>> promedio = hacer_promedio() >>>
promedio(10)
10.0
>>> promedio(11)
10.5 >>>
promedio(12)
11.0

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

Es obvio dónde el promedio de la clase Promediador mantiene el historial: el atributo de instancia


self.series . Pero, ¿dónde encuentra la función avg en el segundo ejemplo la serie?

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.

Ejemplo 7-11. Inspeccionar la función creada por make_averager en el Ejemplo 7-9

>>> 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.

Ejemplo 7-12. Continuando con el Ejemplo 7-10

>>> avg.__code__.co_freevars
('series',) >>> avg.__closure__
(<cell at 0x107a44f78: list object
at 0x107a91a48>,) >>> avg.__closure__[0].cell_contents [10,
11, 12 ]

194 | Capítulo 7: Decoradores de funciones y cierres


Machine Translated by Google

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

Si prueba el Ejemplo 7-13, esto es lo que obtiene:

>>> promedio = hacer_promedio()


>>> promedio(10)
Rastreo (llamadas recientes más última):
...
UnboundLocalError: variable local 'recuento' referenciada antes de la asignación
>>>

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.

La Declaración no local | 195


Machine Translated by Google

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 make_averager(): cuenta


= 0 total = 0

def promediador(nuevo_valor):
recuento no local , recuento
total += 1 total += new_value
return total / count

promediador de retorno

Pasar sin no local en Python 2 La falta


de no local en Python 2 requiere soluciones alternativas, una de las
cuales se describe en el tercer fragmento de código de PEP 3104:
Acceso a nombres en ámbitos externos, que introdujo no local.
Esencialmente, la idea es almacenar las variables que las funciones
internas necesitan cambiar (por ejemplo, contar, totalizar) como
elementos o atributos de algún objeto mutable, como un dict o una
instancia simple, y vincular ese objeto a una variable libre. .

Ahora que hemos cubierto los cierres de Python, podemos implementar decoradores de manera efectiva
con funciones anidadas.

Implementando un decorador simple


El ejemplo 7-15 es un decorador que registra cada invocación de la función decorada e imprime el tiempo
transcurrido, los argumentos pasados y el resultado de la llamada.

Ejemplo 7-15. Un decorador simple para generar el tiempo de ejecución de las funciones.

tiempo de importación

196 | Capítulo 7: Decoradores de funciones y cierres


Machine Translated by Google

def clock(func): def


clocked(*args): # t0 =
time.perf_counter() resultado
= func(*args) # transcurrido =
time.perf_counter() - t0 nombre =
func.__name__ arg_str = ', '.join (repr(arg)
for arg in args) print('[%0.8fs] %s(%s) -> %r' % (transcurrido,
nombre, arg_str, resultado)) return result return clocked #

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.

Devuelve la función interna para reemplazar la función decorada.

El ejemplo 7-16 demuestra el uso del decorador de reloj .

Ejemplo 7-16. Usando el decorador de relojes

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

El resultado de ejecutar el Ejemplo 7-16 se ve así:

$ 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

Implementación de un decorador simple | 197


Machine Translated by Google

[0.00026107s] factorial(6) -> 720


6! = 720

Cómo funciona
Recuerda que este código:

@clock
def factorial(n):
devuelve 1 si n < 2 sino n*factorial(n-1)

En realidad hace esto:

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:

>>> import clockdeco_demo


>>> clockdeco_demo.factorial.__name__
'clocked'
>>>

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:

1. Registra el tiempo inicial t0.

2. Llama al factorial original, guardando el resultado.

3. Calcula el tiempo transcurrido.

4. Formatea e imprime los datos recopilados.

5. Devuelve el resultado guardado en el paso 2.

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.

198 | Capítulo 7: Decoradores de funciones y cierres


Machine Translated by Google

En Design Patterns de Gamma et al., la breve descripción del patrón


Decorator comienza con: "Adjunte responsabilidades adicionales a un
objeto dinámicamente". Los decoradores de funciones se ajustan a esa descripción.
Pero a nivel de implementación, los decoradores de Python tienen poca
semejanza con el decorador clásico descrito en el Design Patternswork
original . “Soapbox” en la página 213 tiene más información sobre este tema.

El decorador de reloj implementado en el ejemplo 7-15 tiene algunas deficiencias: no admite


argumentos de palabras clave y enmascara el __name__ y el __doc__ de la función decorada.
El ejemplo 7-17 usa el decorador functools.wraps para copiar los atributos relevantes de func
a clocked. Además, en esta nueva versión, los argumentos de palabras clave se manejan
correctamente.

Ejemplo 7-17. Un decorador de reloj mejorado


#relojdeco2.py

funciones de

importación de tiempo de importación

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.

Decoradores en la biblioteca estándar


Python tiene tres funciones integradas que están diseñadas para decorar métodos: propiedad,
método de clase y método estático. Discutiremos la propiedad en “Uso de una propiedad para

Decoradores en la Biblioteca Estándar | 199


Machine Translated by Google

Validación de atributos” en la página 604 y los demás en “classmethod Versus staticmethod” en la


página 252.

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.

Memoización con functools.lru_cache


Un decorador muy práctico es functools.lru_cache. Implementa memoización: una técnica de
optimización que funciona guardando los resultados de invocaciones anteriores de una función
costosa, evitando repetir cálculos en argumentos utilizados anteriormente. Las letras LRU significan
Menos usados recientemente, lo que significa que el crecimiento de la memoria caché se limita al
descartar las entradas que no se han leído durante un tiempo.

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

de clockdeco importar reloj

@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

200 | Capítulo 7: Decoradores de funciones y cierres


Machine Translated by Google

[0.00009704s] fibonacci(3) -> 2


[0.00000000s] fibonacci(0) -> 0
[0.00000000s] fibonacci(1) -> 1
[0.00002694s] fibonacci(2) -> 1
[0.00000095s] fibonacci( 1) -> 1
[0.00000095s] fibonacci(0) -> 0
[0.00000095s] fibonacci(1) -> 1
[0.00005102s] fibonacci(2) -> 1
[0.00008917s] fibonacci(3) -> 2
[ 0.00015593s] fibonacci(4) -> 3
[0.00029993s] fibonacci(5) -> 5
[0.00052810s] fibonacci(6) -> 8
8

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.

Ejemplo 7-19. Implementación más rápida mediante almacenamiento en caché

herramientas de importación

de clockdeco importar reloj

@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.

Este es un ejemplo de decoradores apilados: @lru_cache() se aplica a la función devuelta por


@clock.

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

Decoradores en la Biblioteca Estándar | 201


Machine Translated by Google

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.

Ahora consideremos el intrigante decorador functools.singledispatch .

Funciones genéricas con envío único Imaginemos que

estamos creando una herramienta para depurar aplicaciones web. Queremos poder generar pantallas HTML para
diferentes tipos de objetos de Python.

Podríamos empezar con una función como esta:

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>.

• int: muestra el número en decimal y hexadecimal.

• lista: genera una lista HTML, dando formato a cada elemento según su tipo.

El comportamiento que queremos se muestra en el Ejemplo 7-20.

Ejemplo 7-20. htmlize genera HTML adaptado a diferentes tipos de objetos

>>> htmlize({1, 2, 3})


'<pre>{1, 2, 3}</pre>'

202 | Capítulo 7: Decoradores de funciones y cierres


Machine Translated by Google

>>> htmlize(abs)
'<pre>&lt;función integrada abs&gt;</pre>' >>>
htmlize('Heimlich & Co.\n- un juego') '<p>Heimlich
&amp; 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.

Se muestra un int en decimal y hexadecimal, dentro de <pre></pre>.

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.

functools.singledispatch se agregó en Python 3.4, pero el


paquete sin gledispatch disponible en PyPI es un backport
compatible con Python 2.6 a 3.3.

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.

Decoradores en la Biblioteca Estándar | 203


Machine Translated by Google

Ejemplo 7-21. singledispatch crea un htmlize.register personalizado para agrupar varias funciones en una
función genérica

desde functools importar singledispatch


desde colecciones importar abc importar
números importar html

@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> '

@singledispatch marca la función base que maneja el tipo de objeto .

Cada función especializada está decorada con @«base_function».registrar(«tipo»).

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.

204 | Capítulo 7: Decoradores de funciones y cierres


Machine Translated by Google

El uso de ABC para la verificación de tipos permite que su código


admita clases existentes o futuras que son subclases reales o
virtuales de esos ABC. El uso de ABC y el concepto de una subclase
virtual son temas del Capítulo 11.

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.

@singledispatch no está diseñado para llevar la sobrecarga de


métodos de estilo Java a Python. Una sola clase con muchas
variaciones sobrecargadas de un método es mejor que una sola
función con una larga extensión de bloques if/elif/elif/elif . Pero
ambas soluciones son defectuosas porque concentran demasiada
responsabilidad en una sola unidad de código: la clase o la función.
La ventaja de @sin gledispath es admitir la extensión modular: cada
módulo puede registrar una función especializada para cada tipo que admite.

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

En otras palabras, esto:

@d1
@d2 def f(): imprimir('f')

Es lo mismo que:

Decoradores apilados | 205


Machine Translated by Google

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

Un decorador de registro parametrizado Para facilitar la

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.

206 | Capítulo 7: Decoradores de funciones y cierres


Machine Translated by Google

Ejemplo 7-23. Para aceptar parámetros, el nuevo decorador de registros debe llamarse como un
función
registro = conjunto ()

def registro(activo=Verdadero): def


decorar(función): print('
registrar en ejecución(activo=%s)->decorar(%s)'
% (activo, función))
si está activo:
registro.add(función)
más:
registro.descartar(función)

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.

registro toma un argumento de palabra clave opcional.


La función interna decorar es el decorador real; observe cómo toma una función
como argumento.

Registrar func solo si el argumento activo (recuperado del cierre) es True.


Si no está activo y funciona en el registro, elimínelo.
Como decorar es un decorador, debe devolver una función.

registrarse es nuestra fábrica de decoradores, por lo que devuelve decorar.

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.

Decoradores parametrizados | 207


Machine Translated by Google

El código del Ejemplo 7-23 está en un módulo registration_param.py . Si lo importamos, esto es lo


que obtenemos:

>>> import registration_param


running register(active=False)->decorate(<función f1 en 0x10063c1e0>) running
register(active=True)->decorate(<función f2 en 0x10063c268>) >>>
registration_param.registry [<función f2 en 0x10063c268>]

Observe cómo solo aparece la función f2 en el registro; f1 no aparece porque


active=False se pasó a la fábrica de decoración de registros , por lo que la
decoración que se aplicó a f1 no la agregó al registro.
Si, en lugar de usar la sintaxis @ , usáramos register como una función regular, la sintaxis necesaria
para decorar una función f sería register()(f) para agregar f al registro, o register(active=False)(f)
para no agregarlo (o eliminarlo). Consulte el Ejemplo 7-24 para ver una demostración de cómo
agregar y eliminar funciones del registro.

Ejemplo 7-24. Usando el módulo registration_param listado en el Ejemplo 7-23


>>> from registration_param import *
running register(active=False)->decorate(<función f1 en 0x10073c1e0>) running
register(active=True)->decorate(<función f2 en 0x10073c268>) >>> registro # {<
función f2 en 0x10073c268>} >>> register()(f3) # ejecutando register(active=True)-
>decorate(<función f3 en 0x10073c158>) <función f3 en 0x10073c158> >>> registro
# {<función f3 en 0x10073c158>, <función f2 en 0x10073c268>} >>>
register(active=False)(f2) # registro en ejecución(active=False)->decorate(<función f2
en 0x10073c268>) <función f2 en 0x10073c268> >>> registro # {<función f3 en
0x10073c158>}

Cuando se importa el módulo, f2 está en el registro.

La expresión register() devuelve decorar, que luego se aplica a f3.

La línea anterior agregó f3 al registro.


Esta llamada elimina f2 del registro.

Confirme que solo queda f3 en el registro.

El funcionamiento de los decoradores parametrizados es bastante complicado, y el que acabamos


de discutir es más simple que la mayoría. Los decoradores parametrizados generalmente reemplazan
la función decorada y su construcción requiere otro nivel de anidamiento. Recorrer tales pirámides
de funciones es nuestra próxima aventura.

208 | Capítulo 7: Decoradores de funciones y cierres


Machine Translated by Google

El decorador de reloj parametrizado


En esta sección, revisaremos el decorador de relojes y agregaremos una característica: los usuarios pueden pasar un formato
cadena para controlar la salida de la función decorada. Vea el Ejemplo 7-25.

Para simplificar, el ejemplo 7-25 se basa en el reloj inicial implementado


mención del Ejemplo 7-15, y no la mejorada de
Ejemplo 7-17 que usa @functools.wraps, agregando otro
capa de funciones.

Ejemplo 7-25. Módulo clockdeco_param.py: el decorador de reloj parametrizado


tiempo de importación

DEFAULT_FMT = '[{transcurrido:0.8f}s] {nombre}({argumentos}) -> {resultado}'

def reloj(fmt=DEFAULT_FMT): def


decorar(func): def
clocked(*_args): t0 =
hora.hora()
_resultado = func(*_args)
transcurrido = tiempo.tiempo() - t0
nombre = func.__nombre__
args = ', '.join(repr(arg) for arg in _args) result = repr(_result)
print(fmt.format(**locals())) return _result return clocked
return decorar

si __nombre__ == '__principal__':

@clock()
def snooze(segundos):
time.sleep(segundos)

para i en el rango (3):


posponer (.123)

clock es nuestra fábrica de decoradores parametrizados.


decorar es el decorador real.

clocked envuelve la función decorada.

_result es el resultado real de la función decorada.

_args contiene los argumentos reales de clocked , mientras que args se usa para mostrar.

result es la representación str de _result, para mostrar.

Decoradores parametrizados | 209


Machine Translated by Google

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.

decorar regresa cronometrado.

el reloj vuelve a decorar.

En esta autocomprobación, clock() se llama sin argumentos, por lo que el decorador aplicado
utilizará el formato predeterminado str.

Si ejecuta el Ejemplo 7-25 desde el shell, esto es lo que obtiene:

$ 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.

Ejemplo 7-26. clockdeco_param_demo1.py

importar
tiempo desde clockdeco_param importar reloj

@clock('{name}: {elapsed}s') def


snooze(segundos):
time.sleep(segundos)

para i en el rango (3):


posponer (.123)

Salida del Ejemplo 7-26:

$ python3 clockdeco_param_demo1.py
posponer: 0.12414693832397461s
posponer: 0.1241159439086914s posponer:
0.12412118911743164s

Ejemplo 7-27. clockdeco_param_demo2.py

importar
tiempo desde clockdeco_param importar reloj

@clock('{nombre}({args}) dt={elapsed:0.3f}s') def


snooze(segundos): time.sleep(segundos)

para i en el rango (3):


posponer (.123)

210 | Capítulo 7: Decoradores de funciones y cierres


Machine Translated by Google

Salida del Ejemplo 7-27:

$ 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.

Graham Dumpleton y Lennart Regebro, uno de los revisores técnicos de


este libro, argumentan que los decoradores se codifican mejor como clases
que implementan __call__, y no como funciones como los ejemplos de este
capítulo. Estoy de acuerdo en que el enfoque es mejor para los decoradores
no triviales, pero para explicar la idea básica de esta característica del
lenguaje, las funciones son más fáciles de entender.

Resumen del capítulo


Cubrimos mucho terreno en este capítulo, pero traté de hacer el viaje lo más suave posible, incluso si el
terreno es accidentado. Después de todo, entramos en el reino de la metaprogramación.

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.

También visitamos dos impresionantes decoradores de funciones provistos en el módulo functools de la


biblioteca estándar: @lru_cache() y @singledispatch.

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.

Resumen del capítulo | 211


Machine Translated by Google

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.

212 | Capítulo 7: Decoradores de funciones y cierres


Machine Translated by Google

explicando la justificación y las opciones de diseño para la implementación de cierres en


Python.

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":

Lectura adicional | 213


Machine Translated by Google

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.

Decoradores Python y el patrón de diseño Decorator

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 nivel de implementación, los decoradores de Python no se parecen al patrón de diseño clásico de


Decorator, pero se puede hacer una analogía.

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)

En Python, la función de decorador desempeña el papel de una subclase de decorador concreta , y la


función interna que devuelve es una instancia de decorador. La función devuelta envuelve la función que
se va a decorar, que es análoga al componente en el patrón de diseño.
La función devuelta es transparente porque se ajusta a la interfaz del componente al aceptar los mismos
argumentos. Reenvía las llamadas al componente y puede realizar acciones adicionales antes o después.
Tomando prestado de la cita anterior, podemos adaptar la última oración para decir que "La transparencia
le permite anidar decoradores recursivamente, lo que permite un número ilimitado de comportamientos
agregados". Eso es lo que permite que los decoradores apilados funcionen.

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,

214 | Capítulo 7: Decoradores de funciones y cierres


Machine Translated by Google

en general, el patrón Decorator se implementa mejor con clases para representar el


Decorator y los componentes que envolverá.

Lectura adicional | 215


Machine Translated by Google
Machine Translated by Google

PARTE IV

Modismos orientados a objetos


Machine Translated by Google
Machine Translated by Google

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,

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 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.

Las variables no son cajas


En 1997, tomé un curso de verano sobre Java en el MIT. La profesora, Lynn Andrea Stein, una educadora
en informática galardonada que actualmente enseña en la Facultad de ingeniería de Olin, señaló que la
metáfora habitual de "variables como cajas" en realidad dificulta la comprensión de las variables de referencia
en los lenguajes orientados a objetos. Las variables de Python son como variables de referencia en Java, por
lo que es mejor pensar en ellas como etiquetas adjuntas a objetos.

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

>>> a = [1, 2, 3] >>>


b = a >>>
a.append(4) >>> b
[1, 2, 3, 4]

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.

220 | Capítulo 8: Referencias a objetos, mutabilidad y reciclaje


Machine Translated by Google

Ejemplo 8-2. Las variables se asignan a los objetos solo después de que se crean los objetos

>>> clase Gizmo:


... def __init__(uno mismo):
... print('Gizmo id: %d' % id(self))
...
>>> x = Gizmo()
ID del aparato:
4301489152 >>> y = Gizmo() * 10
Identificación del dispositivo: 4301489432

Rastreo (llamadas recientes más última):


Archivo "<stdin>", línea 1, en <módulo>
TypeError: tipos de operandos no admitidos para *: 'Gizmo' e 'int'
>>>
>>> directorio()
['Gizmo', '__incorporados__', '__doc__', '__cargador__', '__nombre__',
'__paquete__', '__spec__', 'x']

La salida Gizmo id: ... es un efecto secundario de crear una instancia de Gizmo .

Multiplicar una instancia de Gizmo generará una excepción.

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.

Para comprender una asignación en Python, siempre lea


primero el lado derecho: ahí es donde se crea o recupera el objeto. Af-
Después de eso, la variable de la izquierda está vinculada al objeto, como una etiqueta.
pegado a él. Olvídate de las cajas.

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.

Identidad, Igualdad y Alias


Lewis Carroll es el seudónimo del profesor Charles Lutwidge Dodgson. El Sr. Carroll no es
sólo igual al Prof. Dodgson: son uno y lo mismo. El ejemplo 8-3 expresa esta idea
en Python.

Ejemplo 8-3. charles y lewis se refieren al mismo objeto

>>> charles = {'nombre': 'Charles L. Dodgson', 'nacido': 1832}


>>> luis = charles >>>
luis es charles
Verdadero

>>> id(charles), id(lewis)

Identidad, Igualdad y Alias | 221


Machine Translated by Google

(4300473992, 4300473992)
>>> luis ['saldo'] = 950 >>>
carlos
{'nombre': 'Charles L. Dodgson', 'saldo': 950, 'nacido': 1832}

lewis es un alias de charles.

El operador is y la función id lo confirman.

Agregar un artículo a lewis es lo mismo que agregar un artículo a charles.

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

El ejemplo 8-4 implementa y prueba el objeto alex representado en la figura 8-2.

Ejemplo 8-4. alex y charles se comparan iguales, pero alex no es charles

>>> alex = {'nombre': 'Charles L. Dodgson', 'nacido': 1832, 'saldo': 950} >>> alex ==
charles

Cierto >>> alex no es charles


Verdadero

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

222 | Capítulo 8: Referencias a objetos, mutabilidad y reciclaje


Machine Translated by Google

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.

En Referencia del lenguaje Python, “3.1. Objetos, valores y tipos” establece:

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 ==.

Elegir entre == y es El operador == compara

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

Y la forma correcta de escribir su negación es:

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.

Identidad, Igualdad y Alias | 223


Machine Translated by Google

Para concluir esta discusión de identidad versus igualdad, veremos que la famosa tupla inmutable no es tan rígida
como cabría esperar.

La inmutabilidad relativa de las tuplas Las tuplas,

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

>>> t1 = (1, 2, [30, 40]) >>> t2


= (1, 2, [30, 40])
>>> t1 == t2

Verdadero >>>
id(t1[-1]) 4302515784
>>> t1[-1].agregar(99)
>>> t1
(1, 2, [30, 40, 99]) >>>
id(t1[-1]) 4302515784

>>> t1 == t2
Falso

t1 es inmutable, pero t1[-1] es mutable.

Construya una tupla t2 cuyos elementos sean iguales a los de t1.

Aunque son objetos distintos, t1 y t2 son iguales, como se esperaba.

Inspeccione la identidad de la lista en t1[-1].

Modifique la lista t1[-1] en su lugar.

La identidad de t1[-1] no ha cambiado, solo su valor. t1 y t2 ahora son


diferentes.

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.

224 | Capítulo 8: Referencias a objetos, mutabilidad y reciclaje


Machine Translated by Google

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.

Las copias son poco profundas por defecto

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:

>>> l1 = [3, [55, 44], (7, 8, 9)] >>> l2 =


lista(l1) >>> l2 [3, [55, 44], (7, 8, 9 )]
>>> l2 == l1

Verdadero >>> l2 es l1
Falso

list(l1) crea una copia de l1.


Las copias son iguales.

Pero se refieren a dos objetos diferentes.

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.

Si tiene una computadora conectada a la mano, le recomiendo ver la


animación interactiva del Ejemplo 8-6 en el Tutor de Python en línea.
Mientras escribo esto, el enlace directo a un ejemplo preparado en
pythontutor.com no funciona de manera confiable, pero la herramienta
es increíble, por lo que vale la pena tomarse el tiempo para copiar y pegar el código.

las copias son poco profundas por defecto | 225


Machine Translated by Google

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

l1 = [3, [66, 55, 44], (7, 8, 9)]


l2 = lista(l1) #
l1.agregar(100) #
l1[1].remove(55) #
imprimir('l1:', l1)
imprimir('l2:', l2)
l2[1] += [33, 22] # l2[2]
+= (10, 11) # imprimir('l1:',
l1)
imprimir('l2:', l2)

l2 es una copia superficial de l1. Este estado se representa en la Figura 8-3.

Agregar 100 a l1 no tiene efecto en l2.

Aquí eliminamos 55 de la lista interna l1[1]. Esto afecta a l2 porque l2[1] es


vinculado a la misma lista que l1[1].

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.

Ejemplo 8-7. Resultado del Ejemplo 8-6

l1: [3, [66, 44], (7, 8, 9), 100]


l2: [3, [66, 44], (7, 8, 9)]
l1: [3, [66, 44, 33, 22], (7, 8, 9), 100]
l2: [3, [66, 44, 33, 22], (7, 8, 9, 10, 11)]

226 | Capítulo 8: Referencias a objetos, mutabilidad y reciclaje


Machine Translated by Google

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.

las copias son poco profundas por defecto | 227


Machine Translated by Google

Copias profundas y superficiales de objetos arbitrarios Trabajar

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.

Ejemplo 8-8. El autobús recoge y deja a los pasajeros


autobús de clase :

def __init__(self, pasajeros=Ninguno): si


pasajeros es Ninguno:
self.pasajeros = [] else:
self.pasajeros = lista(pasajeros)

def pick(self, nombre):


self.pasajeros.append(nombre)

def drop(self, nombre):


self.pasajeros.remove(nombre)

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.

Ejemplo 8-9. Efectos de usar copia versus copia profunda

>>> import copy


>>> bus1 = Bus(['Alice', 'Bill', 'Claire', 'David']) >>> bus2 =
copy.copy(bus1) >>> bus3 = copy.deepcopy( autobus1) >>>
id(autobus1), id(autobus2), id(autobus3) (4301498296,
4301499416, 4301499752) >>> autobus1.drop('Bill') >>>
autobus2.pasajeros ['Alice', 'Claire ', 'David'] >>>
id(autobús1.pasajeros), id(autobús2.pasajeros),
id(autobús3.pasajeros) (4302658568, 4302658568, 4302657800)
>>> autobús3.pasajeros ['Alice', 'Bill' , 'Claire', 'David']

Usando copy y deepcopy, creamos tres instancias de Bus distintas.

Después de que bus1 deja caer a 'Bill', también falta en bus2.

228 | Capítulo 8: Referencias a objetos, mutabilidad y reciclaje


Machine Translated by Google

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,

por lo que su atributo de pasajeros se refiere a otra lista.

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

>>> a = [10, 20]


>>> b = [a, 30] >>>
a.añadir(b)
>>> un
[10, 20, [[...], 30]] >>>
from copy import deepcopy >>>
c = deepcopy(a)
>>> do

[10, 20, [[...], 30]]

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.

Parámetros de función como referencias


El único modo de pasar parámetros en Python es llamar compartiendo. Ese es el mismo modo que se usa
en la mayoría de los lenguajes OO, incluidos Ruby, SmallTalk y Java (esto se aplica a los tipos de
referencia de Java; los tipos primitivos usan llamada por valor). Llamar compartiendo significa que cada
parámetro formal de la función obtiene una copia de cada referencia en los argumentos. En otras palabras,
los parámetros dentro de la función se convierten en alias de los argumentos reales.

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.

Parámetros de funciones como referencias | 229


Machine Translated by Google

Ejemplo 8-11. Una función puede cambiar cualquier objeto mutable que reciba

>>> def f(a, b): a


... += b
... devolver un
...
>>> x = 1
>>> y = 2
>>> f(x, y) 3

>>> 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.

Tipos mutables como parámetros predeterminados: mala idea Los

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"""

def __init__(self, pasajeros=[]):

230 | Capítulo 8: Referencias a objetos, mutabilidad y reciclaje


Machine Translated by Google

self.pasajeros = pasajeros

def pick(self, nombre):


self.pasajeros.append(nombre)

def drop(self, nombre):


self.pasajeros.remove(nombre)

Cuando no se pasa el argumento de los pasajeros , este parámetro se vincula al


objeto de lista predeterminado, que inicialmente está vacío.

Esta asignación convierte a self.passengers en un alias para pasajeros, que en sí mismo es

un alias para la lista predeterminada, cuando no se proporciona ningún argumento de pasajeros .

Cuando los métodos .remove() y .append() se usan con self.passengers

en realidad estamos mutando la lista predeterminada, que es un atributo de la función


objeto.

El ejemplo 8-13 muestra el misterioso comportamiento de HauntedBus.

Ejemplo 8-13. Autobuses perseguidos por pasajeros fantasmas

>>> bus1 = HauntedBus(['Alice', 'Bill'])


>>> bus1.pasajeros
['Alicia', 'Bill']
>>> autobus1.pick('Charlie')
>>> bus1.drop('Alicia')
>>> bus1.pasajeros
['Bill', 'Charlie']
>>> autobus2 = Autobus
Embrujado() >>> autobus2.pick('Carrie')
>>> bus2.pasajeros
['Carrie']
>>> autobus3 = Autobus
Embrujado() >>>
autobus3.pasajeros ['Carrie']
>>> autobus3.pick('Dave')
>>> bus2.pasajeros
['Carrie', 'Dave']
>>> autobús2.pasajeros es autobús3.pasajeros
Verdadero
>>> bus1.pasajeros
['Bill', 'Charlie']

Hasta ahora todo bien: sin sorpresas con bus1.

bus2 comienza vacío, por lo que la lista vacía predeterminada se asigna a self.passengers.

bus3 también comienza vacío, nuevamente se asigna la lista predeterminada.

¡El valor predeterminado ya no está vacío!

Ahora Dave, elegido por bus3, aparece en bus2.

Parámetros de funciones como referencias | 231


Machine Translated by Google

El problema: bus2.passengers y bus3.passengers se refieren a la misma lista.

Pero bus1.passengers es una lista distinta.

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__ :

>>> dir(HauntedBus.__init__) # doctest: +ELIPSIS


['__anotaciones__', '__llamada__', ..., '__predeterminados__', ...]
>>> Autobús
Embrujado.__init__.__predeterminado__ (['Carrie', 'Dave'],)

Finalmente, podemos verificar que bus2.passengers es un alias ligado al primer elemento del atributo
HauntedBus.__init__.__defaults__ :

>>> HauntedBus.__init__.__defaults__[0] is bus2.passengers True

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.

Programación defensiva con parámetros mutables Cuando está

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

232 | Capítulo 8: Referencias a objetos, mutabilidad y reciclaje


Machine Translated by Google

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']

basketball_team tiene cinco nombres de estudiantes.

Un TwilightBus está cargado con el equipo.

El autobús deja caer a un estudiante, luego a otro.

¡Los pasajeros caídos desaparecieron del equipo de baloncesto!

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.

El ejemplo 8-15 es la implementación de TwilightBus y una explicación del problema.

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"""

def __init__(self, pasajeros=Ninguno): si pasajeros


es Ninguno: self.pasajeros = [] else:
self.pasajeros = pasajeros

def pick(self, nombre):


self.pasajeros.append(nombre)

def drop(self, nombre):


self.pasajeros.remove(nombre)

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.

Parámetros de funciones como referencias | 233


Machine Translated by Google

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

def __init__(self, pasajeros=Ninguno): si


pasajeros es Ninguno:
self.pasajeros = [] else:
self.pasajeros = lista(pasajeros)

Haga una copia de la lista de pasajeros o conviértala en una lista si no lo es.

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

A menos que un método tenga la intención explícita de mutar un objeto


recibido como argumento, debe pensar dos veces antes de crear un alias
para el objeto del argumento simplemente asignándolo a una variable de
instancia en su clase. En caso de duda, haga una copia. Sus clientes a
menudo estarán más felices.

del y recolección de basura


Los objetos nunca se destruyen explícitamente; sin embargo, cuando se vuelven inalcanzables, pueden ser
recolectados como basura.

— Capítulo "Modelo de datos" de The Python Language Reference

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.

234 | Capítulo 8: Referencias a objetos, mutabilidad y reciclaje


Machine Translated by Google

Hay un método especial __del__ , pero no provoca la eliminación de la


instancia, y su código no debería llamarlo. __del__ es invocado por el
intérprete de Python cuando la instancia está a punto de ser destruida
para darle la oportunidad de liberar recursos externos. Rara vez
necesitará implementar __del__ en su propio código, sin embargo,
algunos principiantes de Python dedican tiempo a codificarlo sin una
buena razón. El uso adecuado de __del__ es bastante complicado.
Consulte la documentación del método especial __del__ en el capítulo
"Modelo de datos" de The Python Language Reference.

En CPython, el algoritmo principal para la recolección de basura es el conteo de referencias. Esencialmente,


cada objeto lleva la cuenta de cuántas referencias apuntan a él. Tan pronto como ese refcount llega a cero,
el objeto se destruye inmediatamente: CPython llama al método __del__ en el objeto (si está definido) y
luego libera la memoria asignada al objeto. En CPython 2.0, se agregó un algoritmo de recolección de basura
generacional para detectar grupos de objetos involucrados en ciclos de referencia, que pueden ser
inalcanzables incluso con referencias pendientes a ellos, cuando todas las referencias mutuas están
contenidas dentro del grupo.
Otras implementaciones de Python tienen recolectores de basura más sofisticados que no dependen del
conteo de referencias, lo que significa que el método __del__ no se puede llamar inmediatamente cuando
no hay más referencias al objeto. Ver “PyPy, Garbage Collection, and a Deadlock” por A. Jesse Jiryu Davis
para una discusión sobre el uso inapropiado y apropiado de __del__.

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

>>> import ref débil


>>> s1 = {1, 2, 3} >>>
s2 = s1 >>> def bye():

... print('Lo que el viento se llevó...')


...
>>> ender = ref.débil.finalizar(s1, bye) >>>
ender.alive True >>> del s1 >>> ender.alive

Verdadero

>>> s2 = 'correo no deseado'


Lo que el viento se llevó... >>>
ender.alive Falso

s1 y s2 son alias que se refieren al mismo conjunto, {1, 2, 3}.

del y Recolección de Basura | 235


Machine Translated by Google

Esta función no debe ser un método vinculado del objeto a punto de ser destruido o contener una referencia a
él.

Registre la devolución de llamada bye en el objeto referido por s1.

El atributo .alive es True antes de llamar al objeto de finalización .

Como se discutió, del no elimina un objeto, solo 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 .

El ejemplo 8-17 es una sesión de consola, y la consola de Python vincula


automáticamente la variable
_ Ninguno.
al resultado
Esto interfirió
de las con
expresiones
mi intención
quede
no son
demostración, pero también resalta un asunto práctico: cuando tratamos
de microgestionar la memoria, a menudo nos sorprenden las asignaciones
ocultas e implícitas que crean nuevas referencias a nuestros objetos. La
variable de la consola es un ejemplo. Los objetos
_ fuente
de rastreo
comúnson
de otra
referencias inesperadas.

236 | Capítulo 8: Referencias a objetos, mutabilidad y reciclaje


Machine Translated by Google

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

>>> importar referencia débil


>>> un_conjunto = {0, 1}
>>> wref = ref.débil.ref(un_conjunto) >>> wref

<ref débil en 0x100637598; para 'establecer' en 0x100636748>


>>> wref() {0,
1}
>>> un_conjunto = {2, 3, 4} >>>
wref() {0, 1}

>>> wref() es Ninguno Falso

>>> wref() es Ninguno


Verdadero

El objeto de referencia débil wref se crea y se inspecciona en la siguiente línea.

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.

Llamar a wref() aún devuelve {0, 1}.

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

referencias a {0, 1}.

Debido a que el objeto {0, 1} ya no está, esta última llamada a wref() devuelve Ninguno.

La documentación del módulo débilref señala que la clase débilref.ref es


en realidad una interfaz de bajo nivel destinada a usos avanzados, y que la mayoría de los programas son
mejor servido por el uso de las colecciones de ref . débiles y finalizar. En otras palabras, conÿ

sider usando WeakKeyDictionary, WeakValueDictionary, WeakSet y finalizar


(que usan referencias débiles internamente) en lugar de crear y manejar sus propias referencias débiles
instancias ref.ref a mano. Acabamos de hacer eso en el ejemplo 8-17 con la esperanza de mostrar
un solo ref.débil en acción podría eliminar parte del misterio que los rodea. Pero
en la práctica, la mayoría de las veces los programas de Python usan las colecciones de referencias débiles .

La siguiente subsección analiza brevemente las colecciones de referencias débiles .

El sketch del diccionario WeakValue


La clase WeakValueDictionary implementa un mapeo mutable donde los valores son
débiles referencias a objetos. Cuando un objeto referido es basura recolectada en otra parte del

Referencias débiles | 237


Machine Translated by Google

programa, la clave correspondiente se elimina automáticamente de WeakValueDiction ary. Esto se usa


comúnmente para el almacenamiento en caché.

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.

Ejemplo 8-18. El queso tiene un atributo amable y una representación estándar.


queso de clase :

def __init__(self, tipo): self.tipo


= tipo

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.

Ejemplo 8-19. Cliente: “¿De hecho, tiene algún queso aquí?”

>>> importar ref.


débil >>> existencias = ref. débil.Diccionario de
valores débiles () >>> catálogo = [Queso (' Leicester rojo '), Queso
... ('Tilsit'), Queso ('Brie'), Queso ('Parmesano')]
...
>>> para queso en catálogo:
... caldo[queso.tipo] = queso
...
>>> ordenado(stock.keys())
['Brie', 'Parmesan', 'Red Leicester', 'Tilsit'] >>> del
catálogo >>> sorted(stock.keys())

['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.

238 | Capítulo 8: Referencias a objetos, mutabilidad y reciclaje


Machine Translated by Google

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 variable temporal puede hacer que un objeto dure más de lo


esperado manteniendo una referencia a él. Esto no suele ser un
problema con las variables locales: se destruyen cuando la función
regresa. Pero en el ejemplo 8-19, la variable del bucle for queso es
una variable global y nunca desaparecerá a menos que se elimine explícitamente.

Una contraparte del WeakValueDictionary es el WeakKeyDictionary en el que las claves son referencias débiles. La
documentación deweakref.WeakKeyDictionary sugiere posibles usos:

[A WeakKeyDictionary] se puede usar para asociar datos adicionales con un objeto


propiedad de otras partes de una aplicación sin agregar atributos a esos objetos. Esto
puede ser especialmente útil con objetos que anulan accesos a atributos.

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.

Limitaciones de las referencias débiles

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_lista = MiLista (rango(10))

Referencias débiles | 239


Machine Translated by Google

# a_list puede ser el objetivo de una referencia débil


wref_to_a_list = debilref.ref(a_list)

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

Trucos que Python juega con los inmutables

Puede omitir esta sección con seguridad. Trata algunos detalles de


implementación de Python que no son realmente importantes para los usuarios de Python.
Son atajos y optimizaciones realizadas por los desarrolladores centrales de
CPython, que no deberían molestarle al usar el lenguaje y que pueden no
aplicarse a otras implementaciones de Python o incluso a versiones futuras de
CPython. Sin embargo, mientras experimenta con alias y copias, puede
tropezar con estos trucos, por lo que sentí que valía la pena mencionarlos.

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

>>> t1 = (1, 2, 3) >>>


t2 = tupla(t1) >>> t2
es t1

Verdadero >>>
t3 = t1[:] >>> t3 es t1
Verdadero

t1 y t2 están vinculados al mismo objeto.

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.

240 | Capítulo 8: Referencias a objetos, mutabilidad y reciclaje


Machine Translated by Google

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

Ejemplo 8-21. Los literales de cadena pueden crear objetos compartidos

>>> t1 = (1, 2, 3) >>>


t3 = (1, 2, 3) # >>> t3 es
t1 #
Falso
>>> s1 = 'ABC'
>>> s2 = 'ABC' #
>>> s2 es s1 #
Verdadero

Creando una nueva tupla desde cero. t1 y t3

son iguales, pero no el mismo objeto.

Creando una segunda str desde cero.

Sorpresa: ¡ a y b se refieren a la misma str!

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.

¡ Nunca dependa de str o int interning! Siempre use == y no es


para compararlos por igualdad. Interning es una función para uso
interno del intérprete de Python.

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.

trucos que Python juega con los inmutables | 241


Machine Translated by Google

Resumen del capítulo


Cada objeto de Python tiene una identidad, un tipo y un valor. Solo el valor de un objeto cambia con el tiempo.7

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:

• La asignación simple no crea copias. • La asignación

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.

242 | Capítulo 8: Referencias a objetos, mutabilidad y reciclaje


Machine Translated by Google

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.

Puede encontrar más información sobre el recolector de basura generacional de CPython en la


documentación del módulo gc, que comienza con la oración "Este módulo proporciona una interfaz para el
recolector de basura opcional". El calificador "opcional" aquí puede ser sorprendente, pero el capítulo
"Modelo de datos" también establece:

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.

Lectura adicional | 243


Machine Translated by Google

Plataforma improvisada

Igualdad de trato a todos los objetos

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.

244 | Capítulo 8: Referencias a objetos, mutabilidad y reciclaje


Machine Translated by Google

Destrucción de objetos y recolección de basura

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?

La recolección de basura en CPython se realiza principalmente mediante el conteo de referencias,


que es fácil de implementar, pero es propenso a perder memoria cuando hay ciclos de referencia,
por lo que con la versión 2.0 (octubre de 2000) se implementó un recolector de basura generacional
y puede eliminar de objetos inalcanzables mantenidos vivos por ciclos de referencia.

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:

abierto('prueba.txt', 'wt', codificación='utf-8').escribir('1, 2, 3')

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:

con open('test.txt', 'wt', encoding='utf-8') como fp: fp.write('1, 2,


3')

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.

Paso de parámetros: llamada compartida

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".

Lectura adicional | 245


Machine Translated by Google

La cita completa de Alicia y la canción de los caballeros

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.

'O de lo contrario no lo hace, ya sabes. El nombre de la canción se llama “HADDOCKS' EYES”.'

'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

se llama la CANCIÓN'?' Alicia se corrigió a sí misma.

'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

momento estaba completamente desconcertada.

—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

246 | Capítulo 8: Referencias a objetos, mutabilidad y reciclaje


Machine Translated by Google

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.

En capítulos anteriores, presentamos la estructura y el comportamiento de muchos objetos incorporados.


Ahora construiremos clases definidas por el usuario que se comportan como objetos Python reales.

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.

En este capítulo veremos cómo:

• 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

minilenguaje de formato utilizado por format() incorporado y str.for


método mat() .

• Proporcionar acceso de solo lectura a los atributos.

• Hacer que un objeto se pueda modificar para usarlo en conjuntos y como

claves de dictado . • Ahorre memoria con el uso de __slots__.

1. De la Guía de estilo de pegado.

247
Machine Translated by Google

Haremos todo eso mientras desarrollamos un tipo de vector euclidiano bidimensional simple.

La evolución del ejemplo se detendrá para discutir dos temas conceptuales:

• Cómo y cuándo usar los decoradores @classmethod y @staticmethod .

• Atributos privados y protegidos en Python: uso, convenciones y limitaciones.

Comencemos con los métodos de representación de objetos.

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.

Si viene de Python 2, recuerde que en Python 3 __repr__,


__str__ y __format__ siempre deben devolver cadenas Unicode
(tipo str). Se supone que solo __bytes__ devuelve una secuencia
de bytes ( bytes de tipo).

Redux de clase vectorial

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 .

Ejemplo 9-1. Las instancias de Vector2d tienen varias representaciones


>>> v1 = Vector2d(3, 4)
>>> imprimir(v1.x, v1.y)

248 | Capítulo 9: Un objeto pitónico


Machine Translated by Google

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

>>> bool(v1), bool(Vector2d(0, 0))


(Verdadero, Falso)

Se puede acceder a los componentes de un Vector2d directamente como atributos (sin llamadas a
métodos getter).

Un Vector2d se puede descomprimir en una tupla de variables.

El repr de un Vector2d emula el código fuente para construir la instancia.

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 admite la comparación con ==; esto es útil para la prueba.


print llama a str, que para Vector2d produce una visualización de pares
ordenados. bytes usa el método __bytes__ para producir una
representación binaria. abs usa el método __abs__ para devolver la
magnitud del Vector2d. bool usa el método __bool__ para devolver False para un
Vector2d de magnitud cero o True en caso contrario.

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

desde matriz importar matriz


importar matemáticas

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.

Redux de clase vectorial | 249


Machine Translated by Google

clase Vector2d:
código de tipo = 'd'

def __init__(self, x, y):


self.x = float(x) self.y =
float(y)

def __iter__(uno mismo):


return (i por i en (self.x, self.y))

def __repr__(uno mismo):


class_name = tipo(self).__name__
devuelve '{}({!r}, {!r})'.format(nombre_clase, *self)

def __str__(uno mismo):


return str(tupla(auto))

def __bytes__(uno mismo):


return (bytes([ord(self.typecode)]) +
bytes(matriz(self.typecode, self)))

def __eq__(uno mismo, otro):


return tupla(uno mismo) == tupla(otro)

def __abs__(uno mismo):


return matemáticas.hipot(self.x, self.y)

def __bool__(uno mismo):


return bool(abs(uno mismo))

typecode es un atributo de clase que usaremos al convertir instancias de Vector2d a/


de bytes.

Convertir x e y para flotar en __init__ detecta errores temprano, lo cual es útil


en caso de que se llame a Vector2d con argumentos inadecuados.

__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.

250 | Capítulo 9: Un objeto pitónico


Machine Translated by Google

Para generar bytes, convertimos el código de tipo en bytes y concatenamos... ...bytes

convertidos a partir de una matriz creada al iterar sobre la instancia.

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.

La magnitud es la longitud de la hipotenusa del triángulo formado por las componentes x e y .


__bool__ usa abs(self) para calcular la magnitud, luego la convierte a bool, por lo que 0.0 se vuelve

falso, distinto de cero es verdadero.

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)

El método de clase es modificado por el decorador de método de clase .

Sin argumento propio ; en cambio, la clase misma se pasa como cls.

Lea el código de tipo desde el primer byte.

Un constructor alternativo | 251


Machine Translated by Google

Cree una vista de memoria a partir de la secuencia binaria de octetos y use el código de tipo para
lanzarlo.4

Descomprima la vista de memoria resultante de la conversión en el par de argumentos


necesario para el constructor.

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.

método de clase versus método estático


El decorador de método de clase no se menciona en el tutorial de Python, y tampoco
método estático. Cualquiera que haya aprendido OO en Java puede preguntarse por qué Python tiene ambos
de estos decoradores y no solo uno de ellos.

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.

Ejemplo 9-4. Comparación de comportamientos de classmethod y staticmethod


>>> demostración de clase :

... @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.

252 | Capítulo 9: Un objeto pitónico


Machine Translated by Google

>>> Demo.statmeth('spam')
('spam',)

klassmeth simplemente devuelve todos los argumentos posicionales.

statmeth hace lo mismo.

No importa cómo lo invoque, Demo.klassmeth recibe la clase Demo como primer


argumento.
Demo.statmeth se comporta como una simple función antigua.

El decorador de método de clase es claramente útil, pero nunca he visto


un caso de uso convincente para el método estático. Si desea definir una
función que no interactúe con la clase, simplemente defínala en el módulo.
Tal vez la función esté estrechamente relacionada, incluso si nunca toca
la clase, por lo que desea que estén cerca en el código. Aun así, definir
la función justo antes o después de la clase en el mismo módulo está lo
suficientemente cerca para todos los propósitos prácticos.5

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:

• El segundo argumento en format(my_obj, format_spec), o • Lo que aparezca después

de los dos puntos en un campo de reemplazo delimitado con {} dentro de una cadena de formato utilizada con

str.format()

Por ejemplo:

>>> brl = 1/2,43 # tasa de conversión de BRL a USD >>> brl

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.

Pantallas formateadas | 253


Machine Translated by Google

>>> '1 BRL = {tasa:0.2f} USD'.format(tasa=brl) # '1 BRL =


0.41 USD'

El especificador de formato es '0.4f'.

El especificador de formato es '0.2f'. La subcadena 'tasa' en el campo de reemplazo se


denomina nombre de campo. No está relacionado con el especificador de formato, pero
determina qué argumento de .format() entra en ese campo de reemplazo.

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 format() y str.format() son nuevos para usted, la experiencia en el


aula ha demostrado que es mejor estudiar primero la función format() ,
que usa solo el minilenguaje de especificación de formato. Después de
entender la esencia de eso, lea Format String Syntax para aprender
sobre la notación de campo de reemplazo {:} , utilizada en el método
str.format() (incluyendo los indicadores de conversión !s, !r y !a ).

Algunos tipos incorporados tienen sus propios códigos de presentación en el minilenguaje de


especificación de formato. Por ejemplo, entre varios otros códigos, el tipo int admite b y x para salida
de base 2 y base 16, respectivamente, mientras que float implementa f para una visualización de
punto fijo y % para una visualización de porcentaje:

>>> formato(42, 'b')


'101010'
>>> formato(2/3, '.1%')
'66.7%'

El minilenguaje de especificación de formato es extensible porque cada clase puede interpretar el


argumento format_spec como quiera. Por ejemplo, las clases en el módulo datetime usan los
mismos códigos de formato en las funciones strftime() y en sus métodos __format__ . Aquí hay un
par de ejemplos que usan el método incorporado format() y el método str.format() :

>>> desde fechahora importar fechahora


>>> ahora = fechahora.ahora() >>>
formato(ahora, '%H:%M:%S') '18:49:05'

>>> "Ahora es {:%I:%M %p}".formato(ahora)


"Ahora son las 18:49"

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:

254 | Capítulo 9: Un objeto pitónico


Machine Translated by Google

>>> v1 = Vector2d(3, 4) >>>


formato(v1) '(3.0, 4.0)'

Sin embargo, si pasa un especificador de formato, object.__format__ genera TypeError:

>>> formato(v1, '.3f')


Rastreo (llamadas recientes más última):
...
TypeError: cadena de formato no vacía pasada a object.__format__

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:

>>> v1 = Vector2d(3, 4) >>>


formato(v1) '(3.0, 4.0)' >>>
formato(v1, '.2f') '(3.00,
4.00)' >>> formato(v1 , '.3e')
'(3.000e+00, 4.000e+00)'

El ejemplo 9-5 implementa __format__ para producir las pantallas que se acaban de mostrar.

Ejemplo 9-5. Método Vector2d.format, toma #1


# dentro de la clase Vector2d

def __format__(self, fmt_spec=''):


componentes = (formato(c, fmt_spec) for c en self) # return
'({}, {})'.format(*components) #

Utilice el formato incorporado para aplicar fmt_spec a cada componente vectorial, creando una
iteración de cadenas formateadas.

Inserte las cadenas formateadas en la fórmula '(x, y)'.

Ahora agreguemos un código de formato personalizado a nuestro minilenguaje: si el especificador de formato


termina con una 'p', mostraremos el vector en coordenadas polares: <r, ÿ>, donde r es la magnitud y ÿ (theta)
es el ángulo en radianes. El resto del especificador de formato (lo que esté antes de la 'p') se usará como
antes.

Al elegir la letra para el código de formato personalizado, evité la superposición


con los códigos utilizados por otros tipos. En Format Specification Mini-Language
vemos que los números enteros usan los códigos 'bcdoxXn', los flotantes usan
'eEfFgGn%' y las cadenas usan 's'. Así que elegí 'p' para las coordenadas polares.
Debido a que cada clase interpreta estos códigos de forma independiente,
reutilizar una letra de código en un formato personalizado para un nuevo tipo no
es un error, pero puede resultar confuso para los usuarios.

Pantallas formateadas | 255


Machine Translated by Google

Para generar coordenadas polares ya tenemos el método __abs__ para la magnitud,


y codificaremos un método de ángulo simple usando la función math.atan2() para obtener el ángulo.
Este es el código:

# dentro de la clase Vector2d

ángulo de definición (uno mismo):

devuelve math.atan2(self.y, self.x)

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

def __format__(self, fmt_spec=''):


si fmt_spec.termina con('p'):
fmt_spec = fmt_spec[:-1]
coords = (abs(self), self.angle())
outside_fmt = '<{}, {}>' else:

coordenadas = uno mismo

external_fmt = '({}, {})'


componentes = (formato(c, fmt_spec) para c en coords)
return external_fmt.format(*componentes)

El formato termina con 'p': use coordenadas polares.

Eliminar el sufijo 'p' de fmt_spec.

Construye una tupla de coordenadas polares: (magnitud, ángulo).

Configure el formato exterior con paréntesis angulares.

De lo contrario, use componentes x, y de sí mismo para coordenadas rectangulares.

Configure el formato externo con paréntesis.

Genere iterables con componentes como cadenas formateadas.

Inserte las cadenas formateadas en el formato externo.

Con el Ejemplo 9-6, obtenemos resultados similares a estos:

>>> formato(Vector2d(1, 1), 'p')


'<1.4142135623730951, 0.7853981633974483>'
>>> formato(Vector2d(1, 1), '.3ep')
'<1.414e+00, 7.854e-01>'
>>> formato(Vector2d(1, 1), '0.5fps')
'<1.41421, 0.78540>'

Como muestra esta sección, no es difícil extender el minilenguaje de especificación de formato a


admitir tipos definidos por el usuario.

256 | Capítulo 9: Un objeto pitónico


Machine Translated by Google

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:

>>> v1 = Vector2d(3, 4) >>>


hash(v1)
Rastreo (llamadas recientes más última):
...
TypeError: tipo no modificable: 'Vector2d' >>>
set([v1])
Rastreo (llamadas recientes más última):
...
TypeError: tipo no modificable: 'Vector2d'

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:

>>> v1.x, v1.y


(3.0, 4.0) >>>
v1.x = 7 Rastreo
(última llamada más reciente):
...
AttributeError: no se puede establecer el atributo

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'

def __init__(self, x, y): self.__x


= float(x) self.__y = float(y)

@property
def x(self):
return self.__x

@property
def y(self):
return self.__y

Un Hashable Vector2d | 257


Machine Translated by Google

def __iter__(self):
return (i for i in (self.x, self.y))

Siguen # métodos restantes (omitidos en la lista de libros)

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.

El método getter lleva el nombre de la propiedad pública que expone: x.

Solo devuélvelo self.__x.

Repita la misma fórmula para la propiedad y .

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.

Vector.x y Vector.y son ejemplos de propiedades de solo lectura.


Las propiedades de lectura/escritura se tratarán en el Capítulo 19,
donde profundizaremos en @property.

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.

Ejemplo 9-8. vector2d_v3.py: implementación de hash


# dentro de la clase Vector2d:

def __hash__(self):
return hash(self.x) ^ hash(self.y)

Con la adición del método __hash__ , ahora tenemos vectores hashable:

>>> v1 = Vector2d(3, 4) >>>


v2 = Vector2d(3.1, 4.2) >>>
hash(v1), hash(v2)

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.

258 | Capítulo 9: Un objeto pitónico


Machine Translated by Google

(7, 384307168202284039)
>>> conjunto([v1, v2])
{Vector2d(3.1, 4.2), Vector2d(3.0, 4.0)}

No es estrictamente necesario implementar propiedades o proteger los


atributos de la instancia para crear un tipo hashable. Implementar
__hash__ y __eq__ correctamente es todo lo que se necesita. Pero se
supone que el valor hash de una instancia nunca cambia, por lo que esto
brinda una excelente oportunidad para hablar sobre las propiedades de solo lectura.

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.

Ejemplo 9-9. vector2d_v3.py: el monto completo


"""
Una clase vectorial bidimensional

>>> v1 = Vector2d(3, 4) >>>


imprimir(v1.x, v1.y) 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 >>> 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

>>> bool(v1), bool(Vector2d(0, 0))


(Verdadero Falso)

Prueba del método de clase ``.frombytes()``:

Un Hashable Vector2d | 259


Machine Translated by Google

>>> v1_clone = Vector2d.frombytes(bytes(v1)) >>> v1_clone


Vector2d(3.0, 4.0) >>> v1 == v1_clone True

Pruebas de ``format()`` con coordenadas cartesianas:

>>> formato(v1)
'(3.0, 4.0)' >>>
formato(v1, '.2f') '(3.00, 4.00)'
>>> formato(v1, '.3e') '(3.000e+
00, 4.000e+00)'

Pruebas del método ``angle``::

>>> Vector2d(0, 0).ángulo() 0.0

>>> Vector2d(1, 0).ángulo()


0.0
>>> épsilon = 10**-8 >>>
abs(Vector2d(0, 1).ángulo() - math.pi/ 2) < épsilon
Verdadero

>>> abs(Vector2d(1, 1).ángulo() - math.pi/ 4) < épsilon


Verdadero

Pruebas de ``format()`` con coordenadas polares:

>>> formato(Vector2d(1, 1), 'p') # pruebadoc:+ELIPSIS '<1.414213...,


0.785398...>' >>> formato(Vector2d(1, 1), '.3ep' ) '<1.414e+00, 7.854e-01>'
>>> formato(Vector2d(1, 1), '0.5fp') '<1.41421, 0.78540>'

Pruebas de las propiedades de solo lectura `x` e `y`:

>>> v1.x, v1.y (3.0,


4.0) >>> v1.x = 123
Rastreo (última
llamada más reciente):
...
AttributeError: no se puede establecer el atributo

Pruebas de hashing:

260 | Capítulo 9: Un objeto pitónico


Machine Translated by Google

>>> v1 = Vector2d(3, 4) >>>


v2 = Vector2d(3.1, 4.2) >>>
hash(v1), hash(v2) (7,
384307168202284039) >>>
len(conjunto([v1, v2 ])) 2

"""

desde matriz importar matriz


importar matemáticas

clase Vector2d:
código de tipo = 'd'

def __init__(self, x, y): self.__x


= float(x) self.__y = float(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))

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 __eq__(uno mismo, otro):


return tupla(uno mismo) == tupla(otro)

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

Un Hashable Vector2d | 261


Machine Translated by Google

ángulo def (auto):


return math.atan2(auto.y, auto.x)

def __format__(self, fmt_spec=''): if


fmt_spec.endswith('p'):
fmt_spec = fmt_spec[:-1]
coords = (abs(self), self.angle()) outside_fmt
= '<{}, {}>' else: coords = self outside_fmt
= '({}, {})' componentes = (formato(c, fmt_spec)
para c en coords) devuelve
outside_fmt.format(*componentes)

@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.

Atributos privados y "protegidos" en Python


En Python, no hay forma de crear variables privadas como con el modificador privado en Java. Lo que
tenemos en Python es un mecanismo simple para evitar la sobreescritura accidental de un atributo "privado"
en una subclase.

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

262 | Capítulo 9: Un objeto pitónico


Machine Translated by Google

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.

El ejemplo 9-10 muestra el resultado en la clase Vector2d del ejemplo 9-7.

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:

Atributos privados y “protegidos” en Python | 263


Machine Translated by Google

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.

Los atributos con un solo prefijo _ se denominan "protegidos" en algunos rincones de la


documentación de Python.9 La práctica de "proteger" atributos por convención con la forma self._x
está muy extendida, pero llamar a ese atributo "protegido" no es tan común . Algunos incluso lo
llaman un atributo "privado".

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__.

Ahorro de espacio con el atributo de clase __slots__


De forma predeterminada, Python almacena atributos de instancia en un dictado por instancia denominado __dict__ .
Como vimos en “Consecuencias prácticas de cómo funciona dict” en la página 90, los diccionarios
tienen una sobrecarga de memoria significativa debido a la tabla hash subyacente que se usa para
proporcionar un acceso rápido. Si está tratando con millones de instancias con pocos atributos, el
atributo de clase __slots__ puede ahorrar mucha memoria al permitir que el intérprete almacene los
atributos de la instancia en una tupla en lugar de un dict.

7. De la Guía de estilo de pegado.

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.

9. Un ejemplo está en los documentos del módulo gettext.

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.

264 | Capítulo 9: Un objeto pitónico


Machine Translated by Google

Un atributo __slots__ heredado de una superclase no tiene efecto.


Python solo tiene en cuenta los atributos __slots__ definidos en cada
clase individualmente.

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.

Ejemplo 9-11. vector2d_v3_slots.py: el atributo de ranuras es la única adición a Vector2d


clase Vector2d:
__ranuras__ = ('__x', '__y')

código de tipo = 'd'

Siguen # métodos (omitidos en la lista de libros)

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.

Si está manejando millones de objetos con datos numéricos, realmente


debería usar matrices NumPy (consulte “NumPy y SciPy” en la página
52), que no solo son eficientes en memoria sino que tienen funciones
altamente optimizadas para el procesamiento numérico, muchas de las
cuales op - Erate en toda la matriz a la vez. Diseñé la clase Vector2d
solo para proporcionar contexto al discutir métodos especiales, porque
trato de evitar ejemplos vagos de foo y barra cuando puedo.

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

Ahorro de espacio con el atributo de clase __slots__ | 265


Machine Translated by Google

Uso final de RAM: 1,558,482,944

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.

Cuando se especifica __slots__ en una clase, sus instancias no


podrán tener ningún otro atributo aparte de los mencionados en
__slots__. Esto es realmente un efecto secundario, y no la razón por
la que existen __slots__ . Se considera una mala forma usar __slots__
solo para evitar que los usuarios de su clase creen nuevos atributos
en las instancias si así lo desean. __slots__ debe usarse para la
optimización, no para la restricción del programador.

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__.

266 | Capítulo 9: Un objeto pitónico


Machine Translated by Google

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.

Los problemas con __slots__


Para resumir, __slots__ puede proporcionar un ahorro de memoria significativo si se usa correctamente,
pero hay algunas advertencias:

• 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.

Anulación de atributos de clase


Una característica distintiva de Python es cómo los atributos de clase se pueden usar como valores
predeterminados para atributos de instancia. En Vector2d existe el atributo de clase typecode . Se usa
dos veces en el método __bytes__ , pero lo leemos como self.typecode por diseño. Debido a que las
instancias de Vector2d se crean sin un atributo typecode propio, self.typecode obtendrá el atributo de
clase Vector2d.typecode de forma predeterminada.

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 .

Anulación de atributos de clase | 267


Machine Translated by Google

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.

Estamos hablando de agregar un atributo de instancia personalizado, por


lo tanto, el Ejemplo 9-13 usa la implementación de Vector2d sin
__ranuras__ como se muestra en el Ejemplo 9-9.

Ejemplo 9-13. Personalizar una instancia configurando el atributo de código de tipo que anteriormente se heredó de la clase

>>> from vector2d_v3 import Vector2d >>>


v1 = Vector2d(1.1, 2.2) >>> dumpd =
bytes(v1) >>> dumpd
b'd\x9a\x99\x99\x99\x99\x99\xf1?\
x9a\x99\x99\x99\x99\x99\x01@' >>> len(dumpd) # 17 >>> v1.typecode = 'f' #
>>> dumpf = bytes(v1) >>> dumpf b' f\xcd\xcc\x8c?\xcd\xcc\x0c@' >>>
len(dumpf) # 9

>>> Vector2d.código tipo # 'd'

La representación de bytes predeterminada tiene una longitud de 17 bytes.

Establezca el código de tipo en 'f' en la instancia v1 .

Ahora el volcado de bytes tiene una longitud de 9 bytes .

Vector2d.typecode no ha cambiado; solo la instancia v1 usa el código de tipo 'f'.

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

diferentes formatos de exportación.

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:

>>> Vector2d.typecode = 'f'

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

subclases solo para personalizar los datos de una clase.

268 | Capítulo 9: Un objeto pitónico


Machine Translated by Google

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

>>> from vector2d_v3 import Vector2d


>>> clase ShortVector2d(Vector2d): #
... typecode = 'f'
...
>>> sv = VectorCorto2d(1/11, 1/27) #
>>> sv
ShortVector2d(0.09090909090909091, 0.037037037037037035) # >>>
len(bytes(sv)) # 9

Cree ShortVector2d como una subclase de Vector2d solo para sobrescribir el atributo de clase de
código de tipo .

Cree la instancia sv de ShortVector2d para la demostración.

Inspeccione el repr de sv.

Verifique que la longitud de los bytes exportados sea 9, no 17 como antes.

Este ejemplo también explica por qué no codifiqué class_name en Vec to2d.__repr__, sino que lo obtuve
de type(self).__name__, así:

# dentro de la clase Vector2d:

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.

Resumen del capítulo


El objetivo de este capítulo fue demostrar el uso de métodos y convenciones especiales en la construcción
de una clase Pythonic de buen comportamiento.

¿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

Resumen del capítulo | 269


Machine Translated by Google

si la primera o la última implementación de Vector2d es más idiomática depende del contexto en el


que se usaría. Zen of Python de Tim Peter dice:

Lo simple es mejor que lo complejo.

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:

• Todos los métodos de representación de cadenas/bytes: __repr__, __str__, __format__ y


__bytes__.

• Varios métodos para convertir un objeto en un número: __abs__, __bool__,


__picadillo__.

• El operador __eq__ , para probar la conversión de bytes y habilitar el hashing (junto con
__picadillo__).

Si bien admitimos la conversión a bytes , también implementamos un constructor alternativo,


Vector2d.frombytes(), que proporcionó el contexto para discutir los decoradores @classmethod (muy
útil) y @staticmethod (no tan útil, las funciones a nivel de módulo son más simples). El método
frombytes se inspiró en su homónimo en la clase de rayos array.ar .

Vimos que el minilenguaje de especificación de formato es extensible mediante la implementación de


un método __format__ que realiza un análisis mínimo de format_spec proporcionado al format(obj,
format_spec) incorporado o dentro de los campos de reemplazo '{:«format_spec»}' en cadenas
utilizado con el método str.format .

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.

Luego discutimos el ahorro de memoria y las advertencias de declarar un atributo __slots__ en


Vector2d. Debido a que usar __slots__ es algo complicado, realmente tiene sentido solo cuando se
maneja una gran cantidad de instancias; piense en millones de instancias, no solo en miles.

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.

270 | Capítulo 9: Un objeto pitónico


Machine Translated by Google

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.

— Antiguo proverbio chino

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:

Capítulo "Modelo de datos" de The Python Language Reference


La mayoría de los métodos que usamos en este capítulo están documentados en “3.3.1.
Personalización básica”.

Python en pocas palabras, 2.ª edición, por Alex Martelli


Excelente cobertura del modelo de datos, incluso si solo se cubre Python 2.5 (en la segunda
edición). Los conceptos fundamentales son todos iguales y la mayoría de las API del modelo de
datos no han cambiado en absoluto desde Python 2.2, cuando los tipos integrados y las clases
definidas por el usuario se volvieron más compatibles.

Python Cookbook, 3.ª edición, de David Beazley y Brian K. Jones


Prácticas de codificación muy modernas demostradas a través de recetas. El capítulo 8, "Clases y
objetos", en particular, tiene varias soluciones relacionadas con las discusiones de este capítulo.

Python Essential Reference, 4.ª edición, por David Beazley Cubre el


modelo de datos en detalle en el contexto de Python 2.6 y Python 3.

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.

Lectura adicional | 271


Machine Translated by Google

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

Las propiedades ayudan a reducir los costos iniciales

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.

Cuando sentimos la necesidad de evitar actualizaciones accidentales de los atributos x e y , implementamos


propiedades, pero nada cambió en ninguna otra parte del código y en la interfaz pública de Vector2d, como lo
verificaron las pruebas documentales. Todavía podemos acceder a my_vector.x y my_vector.y.

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

Solo para hacer esto:

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

272 | Capítulo 9: Un objeto pitónico


Machine Translated by Google

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.

Seguridad versus seguridad en atributos privados

Perl no está enamorado de la privacidad forzada. Preferiría que te quedaras fuera de su


sala porque no fuiste invitado, no porque tenga una escopeta.
— Larry Wall
Creador de Perl
Python y Perl son polos opuestos en muchos aspectos, pero Larry y Guido parecen estar de acuerdo en la
privacidad de los objetos.

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 {

Cadena privada secreta = "";

Public Confidential(String text) { secret =


text.toUpperCase();
}
}

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

mensaje = Confidencial(' texto ultrasecreto')


campo_secreto = Confidencial.getDeclaredField('secreto')

11. Consulte “Lo más simple que podría funcionar: una conversación con Ward Cunningham, Parte V”.

Lectura adicional | 273


Machine Translated by Google

secret_field.setAccessible(True) # ¡rompe el candado! imprimir


'mensaje.secreto =', campo_secreto.get(mensaje)

Si ejecuta el Ejemplo 9-16, esto es lo que obtiene:

$ 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.

274 | Capítulo 9: Un objeto pitónico


Machine Translated by Google

CAPÍTULO 10

Secuencia Hacking, Hashing y Slicing

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:

• Protocolo básico de secuencias: __len__ y __getitem__. •

Representación segura de instancias con muchos elementos. •

Soporte de corte adecuado, produciendo nuevas instancias de Vector .

• Hashing agregado teniendo en cuenta cada valor de elemento contenido. • Extensión

de lenguaje de formato personalizado.

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

Aplicaciones vectoriales más allá de las tres dimensiones

¿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.

Vector: un tipo de secuencia definido por el usuario

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.

Vector Take #1: Compatible con Vector2d

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 .

Ejemplo 10-1. Pruebas de Vector.__init__ y Vector.__repr__


>>> Vector([3.1, 4.2])
Vector([3.1, 4.2])
>>> Vector((3, 4, 5))
Vector([3.0, 4.0, 5.0])

276 | Capítulo 10: Secuencia Hacking, Hashing y Slicing


Machine Translated by Google

>>> 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]).

Cuando un Vector tiene más de seis componentes, la cadena producida


por repr() se abrevia con ... como se ve en la última línea del Ejemplo 10-1.
Esto es crucial en cualquier tipo de colección que pueda contener una gran
cantidad de elementos, porque repr se usa para la depuración (y no desea
que un solo objeto grande abarque miles de líneas en su consola o
registro). Use el módulo reprlib para producir representaciones de longitud
limitada, como en el ejemplo 10-2.

El módulo reprlib se llama repr en Python 2. La herramienta 2to3 reescribe


las importaciones desde repr automáticamente.

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

Ejemplo 10-2. vector_v1.py: derivado de vector2d_v1.py

desde matriz importar matriz


importar reprlib importar
matemáticas

clase vectorial:
código de tipo = 'd'

def __init__(uno mismo, componentes):


self._components = array(self.typecode, componentes)

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

Vector Take #1: Compatible con Vector2d | 277


Machine Translated by Google

def __eq__(uno mismo, otro):


return tupla(uno mismo) == tupla(otro)

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)

El atributo "protegido" de la instancia self._components contendrá una matriz con los


componentes del Vector .
Para permitir la iteración, devolvemos un iterador sobre self._components. 1

Utilice reprlib.repr() para obtener una representación de longitud limitada de self._components


(p. ej., array('d', [0.0, 1.0, 2.0, 3.0, 4.0, ...])).

Elimine la matriz ('d', el prefijo y el final ) antes de conectar la cadena en una llamada de
constructor de Vector .

Cree un objeto de bytes directamente desde self._components.

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 .

Al codificar __repr__, podría haber producido la visualización de componentes simplificada con


esta expresión: reprlib.repr(list(self._components)). Sin embargo, esto sería un desperdicio, ya que
estaría copiando cada elemento de self._components a una lista solo para usar la lista repr. En
cambio, decidí aplicar reprlib.repr a la matriz self._components

1. La función iter() se cubre en el Capítulo 14, junto con el método __iter__.

278 | Capítulo 10: Secuencia Hacking, Hashing y Slicing


Machine Translated by Google

directamente y luego corte los caracteres fuera del []. Eso es lo que hace la segunda línea de
__repr__ en el ejemplo 10-2.

Debido a su función en la depuración, llamar a repr() en un objeto


nunca debería generar una excepción. Si algo sale mal dentro de su
implementación de __repr__, debe lidiar con el problema y hacer todo
lo posible para producir algún resultado útil que le brinde al usuario la
oportunidad de identificar el objeto de destino.

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.

Protocolos y Duck Typing


Ya en el Capítulo 1, vimos que no es necesario heredar de ninguna clase especial para crear un tipo
de secuencia totalmente funcional en Python; solo necesita implementar los métodos que cumplen
con el protocolo de secuencia. Pero, ¿de qué tipo de protocolo estamos hablando?

En el contexto de la programación orientada a objetos, un protocolo es una interfaz informal, definida


solo en la documentación y no en el código. Por ejemplo, el protocolo de secuencia en Python implica
solo los métodos __len__ y __getitem__ . Cualquier clase Spam que implemente esos métodos con
la firma y la semántica estándar se puede usar en cualquier lugar donde se espere una secuencia. Si
Spam es una subclase de esto o aquello es irrelevante; lo único que importa es que proporciona los
métodos necesarios. Vimos eso en el Ejemplo 1-1, reproducido aquí en el Ejemplo 10-3.

Ejemplo 10-3. Código del Ejemplo 1-1, reproducido aquí por conveniencia
importar colecciones

Carta = colecciones.namedtuple('Carta', ['rango', 'palo'])

clase FrenchDeck:
rangos = [str(n) for n in range(2, 11)] + list('JQKA') palos = 'picas
diamantes tréboles corazones'.split()

def __init__(uno mismo):

Protocolos y Duck Typing | 279


Machine Translated by Google

self._cards = [Carta(rango, palo) para palo en self.palos para rango


en self.ranks]

def __len__(self):
return len(self._cards)

def __getitem__(self, posición): return


self._cards[posición]

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__.

Ahora implementaremos el protocolo de secuencia en Vector, inicialmente sin el soporte adecuado


para el corte, pero luego lo agregaremos.

Vector Take #2: Una secuencia rebanable


Como vimos con el ejemplo de FrenchDeck , admitir el protocolo de secuencia es realmente fácil si
puede delegar en un atributo de secuencia en su objeto, como nuestra matriz self._components . Estas
frases ingeniosas de __len__ y __getitem__ son un buen comienzo:

clase vectorial:
# muchas lineas omitidas
# ...

def __len__(self):
return len(self._components)

def __getitem__(uno mismo, índice):


volver self._components[índice]

Con estas adiciones, todas estas operaciones ahora funcionan:

>>> v1 = Vector([3, 4, 5]) >>>


largo(v1) 3

>>> v1[0], v1[-1] (3.0,


5.0) >>> v7 =
Vector(rango(7))

280 | Capítulo 10: Secuencia Hacking, Hashing y Slicing


Machine Translated by Google

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

Cómo funciona el corte

Una demostración vale más que mil palabras, así que observe el Ejemplo 10-4.

Ejemplo 10-4. Comprobando el comportamiento de __getitem__ y slices

>>> class MySeq:


... def __getitem__(self, index): return
... index #
...
>>> s = MySeq()
>>> s[1] # 1

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

Para esta demostración, __getitem__ simplemente devuelve lo que se le pasa.

Un solo índice, nada nuevo.

La notación 1:4 se convierte en slice(1, 4, None).

slice(1, 4, 2) significa comenzar en 1, detenerse en 4, avanzar en 2.

Sorpresa: la presencia de comas dentro de [] significa que __getitem__ recibe una tupla.

La tupla puede incluso contener varios objetos de corte.

Vector Take #2: Una secuencia rebanable | 281


Machine Translated by Google

Ahora echemos un vistazo más de cerca a slice en el Ejemplo 10-5.

Ejemplo 10-5. Inspeccionar los atributos de la clase de segmento

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

S.indices(len) -> (inicio, parada, zancada)


Suponiendo una secuencia de longitud len, calcule los índices de inicio y fin , y la longitud de zancada del corte
extendido descrito por S. Los índices fuera de límites se recortan de manera consistente con el manejo de
cortes normales.

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':

>>> segmento(Ninguno, 10, 2).índices(5) #


(0, 5, 2) >>> segmento(-3, Ninguno,
Ninguno).índices(5) # (2, 5, 1)

'ABCDE'[:10:2] es lo mismo que 'ABCDE'[0:5:2]


'ABCDE'[-3:] es lo mismo que 'ABCDE'[2:5:1]

282 | Capítulo 10: Secuencia Hacking, Hashing y Slicing


Machine Translated by Google

Mientras escribo esto, el método slice.indices aparentemente no está


documentado en la Referencia de la biblioteca de Python en línea. El
Manual de referencia de la API de Python Python/C documenta una
función de nivel C similar, PySlice_GetIndicesEx. Descubrí slice.indices
mientras exploraba objetos de división en la consola de Python, usando
dir() y help(). Otra prueba más del valor de la consola interactiva como
herramienta de descubrimiento.

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__ .

Un Ejemplo 10-6 de __getitem__ con Slice-

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)

def __getitem__(self, index): cls =


type(self) if isinstance(index,
slice): return
cls(self._components[index]) elif
isinstance(index, numbers.Integral):
return self._components[index]
else: msg = '{cls.__name__} los índices
deben ser enteros' raise TypeError(msg.format(cls=cls))

Obtenga la clase de la instancia (es decir, Vector) para su uso posterior.

Si el argumento de índice es una

porción... ...invoque la clase para construir otra instancia de Vector a partir de una porción de la matriz
_components .

Si el índice es un int o algún otro tipo de número entero... ...simplemente

devuelva el elemento específico de _components.

De lo contrario, genera una excepción.

Vector Take #2: Una secuencia rebanable | 283


Machine Translated by Google

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 .

Ejemplo 10-7. Pruebas de Vector.getitem mejorado del Ejemplo 10-6

>>> v7 = Vector(rango(7)) >>>


v7[-1] 6.0 >>> v7[1:4]

Vector([1.0, 2.0, 3.0]) >>>


v7[-1:]
Vector([6.0])
>>> v7[1,2]
Rastreo (última llamada más reciente):
...
TypeError: los índices vectoriales deben ser números enteros

Un índice entero recupera solo un valor de componente como flotante.


Un índice de corte crea un nuevo Vector.

Una porción de len == 1 también crea un Vector.

Vector no admite la indexación multidimensional, por lo que una tupla de índices o sectores
genera un error.

Vector Take #3: Acceso a atributos dinámicos


En la evolución de Vector2d a Vector, perdimos la capacidad de acceder a los componentes del vector
por su nombre (p. ej., vx, vy). Ahora estamos tratando con vectores que pueden tener una gran cantidad
de componentes. Aún así, puede ser conveniente acceder a los primeros componentes con letras
abreviadas como x, y, z en lugar de v[0], v[1] y v[2].

Esta es la sintaxis alternativa que queremos proporcionar para leer los primeros cuatro componentes de
un vector:

284 | Capítulo 10: Secuencia Hacking, Hashing y Slicing


Machine Translated by Google

>>> v = Vector(rango(10))
>>> vx
0.0
>>> vy, vz, vt (1.0,
2.0, 3.0)

En Vector2d, proporcionamos acceso de solo lectura a x e y usando el decorador @property (Ejemplo


9-7). Podríamos escribir cuatro propiedades en Vector, pero sería tedioso. El método especial
__getattr__ proporciona una mejor manera.

“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'

def __getattr__(self, name): cls =


type(self) if len(name) == 1:
pos =
cls.shortcut_names.find(name) if 0 <= pos
< len(self._components): return
self._components [posición]
msg = '{.__name__!r} objeto no tiene atributo {!r}' aumentar
AttributeError(msg.format(cls, name))

Obtenga la clase Vector para su uso posterior.

Si el nombre es un carácter, puede ser uno de los nombres_atajos.

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 la posición está dentro del rango, devuelve el elemento de matriz.

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á.

Vector Take #3: Acceso a atributos dinámicos | 285


Machine Translated by Google

No es difícil implementar __getattr__, pero en este caso no es suficiente. Considere la extraña


interacción del ejemplo 10-9.

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]) #

Acceder al elemento v[0] como vx

Asigne un nuevo valor a vx Esto debería generar una excepción.

Leer vx muestra el nuevo valor, 10.

Sin embargo, los componentes del vector no cambiaron.

¿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.

Ejemplo 10-10. Parte de vector_v3.py: método __setattr__ en la clase Vector


def __setattr__(yo, nombre, valor): cls =
tipo(yo)

286 | Capítulo 10: Secuencia Hacking, Hashing y Slicing


Machine Translated by Google

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)

Manejo especial para nombres de atributo de un solo carácter.

Si el nombre es uno de xyzt, establezca un mensaje de error específico.

Si el nombre está en minúsculas, establezca un mensaje de error sobre todos los nombres de una sola letra.

De lo contrario, establezca un mensaje de error en blanco.

Si hay un mensaje de error que no está en blanco, genere AttributeError.

Caso predeterminado: llame a __setattr__ en la superclase para el comportamiento estándar.

La función super() proporciona una forma de acceder dinámicamente a los


métodos de las superclases, una necesidad en un lenguaje dinámico que
admite la herencia múltiple como Python. Se usa para delegar alguna
tarea de un método en una subclase a un método adecuado en una
superclase, como se ve en el Ejemplo 10-10. Hay más información sobre
super en “Herencia múltiple y orden de resolución de métodos” en la página 351.

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

confusiones con los atributos de solo lectura admitidos x, y, z y t.

Vector Take #3: Acceso a atributos dinámicos | 287


Machine Translated by Google

Sabiendo que declarar __slots__ a nivel de clase evita establecer


nuevos atributos de instancia, es tentador usar esa función en lugar de
implementar __setattr__ como hicimos nosotros. Sin embargo, debido
a todas las advertencias discutidas en “Los problemas con __slots__”
en la página 267, no se recomienda usar __slots__ solo para evitar la
creación de atributos de instancia. __slots__ debe usarse solo para
ahorrar memoria, y solo si eso es un problema real.

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.

Si quisiéramos permitir el cambio de componentes, podríamos implementar __setitem__ para


habilitar v[0] = 1.1 y/o __setattr__ para hacer que vx = 1.1 funcione. Pero Vector permanecerá
inmutable porque queremos hacerlo hashable en la próxima sección.

Vector Take #4: Hashing y un == más rápido


Una vez más podemos implementar un método __hash__ . Junto con el __eq__ existente, esto hará
que las instancias de Vector sean hash.

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.

288 | Capítulo 10: Secuencia Hacking, Hashing y Slicing


Machine Translated by Google

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

>>> 2 * 3 * 4 * 5 # el resultado que queremos: 5! == 120


120
>>> importar funciones
>>> funciones.reducir(lambda a,b: a*b, range(1, 6)) 120

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 .

Ejemplo 10-11. Tres formas de calcular el xor acumulado de enteros del 0 al 5

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

Agregue xor con un bucle for y una variable acumuladora. functools.reduce

usando una función anónima. functools.reduce reemplazando custom

lambda con operator.xor.

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.

Vector Take #4: Hashing y un == más rápido | 289


Machine Translated by Google

Ejemplo 10-12. Parte de vector_v4.py: dos importaciones y el método __hash__ agregado a la clase Vector desde
vector_v3.py

from array import array import


reprlib import math import
functools # operador de
importación #

clase vectorial:
código de tipo = 'd'

# muchas líneas omitidas en la lista de libros...

def __eq__(uno mismo, otro): #


return tupla(uno mismo) == tupla(otro)

def __hash__(self):
hashes = (hash(x) for x in self._components) # return
functools.reduce(operator.xor, hashes, 0) #

# líneas más omitidas...

Importar funciones para usar reduce.

Operador de importación para usar xor.

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

Al usar reduce, es una buena práctica proporcionar el tercer


argumento, reduce(function, iterable, initializer), para evitar esta
excepción: TypeError: reduce() de secuencia vacía sin valor
inicial (excelente mensaje: explica el problema y cómo arreglarlo).
El inicializador es el valor devuelto si la secuencia está vacía y
se usa como primer argumento en el bucle de reducción, por lo
que debería ser el valor de identidad de la operación. Por
ejemplo, para +, |, ^ el inicializador debería ser 0, pero para *, & debería
ser 1

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

290 | Capítulo 10: Secuencia Hacking, Hashing y Slicing


Machine Translated by Google

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)

La solución con mapa sería menos eficiente en Python 2, donde la


función de mapa crea una nueva lista con los resultados. Pero en Python
3, el mapa es perezoso: crea un generador que produce los resultados
a pedido, ahorrando así memoria, al igual que la expresión del generador
que usamos en el método __hash__ del ejemplo 10-8.

Ya que estamos en el tema de la reducción de funciones, podemos reemplazar nuestra


implementación rápida de __eq__ con otra que será más barata en términos de procesamiento y
memoria, al menos para vectores grandes. Como se presentó en el Ejemplo 9-2, tenemos esta
implementación muy concisa de __eq__:

def __eq__(uno mismo,


otro): return tupla(uno mismo) == tupla(otro)

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.

Vector Take #4: Hashing y un == más rápido | 291


Machine Translated by Google

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

def __eq__(uno mismo,


otro): if len(uno mismo) != len(otro):
# devuelve Falso para a, b in
zip(uno mismo, otro): #
si a != b: #
devuelve Falso
devolver verdadero #

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.

En cuanto dos componentes sean diferentes, salga devolviendo False.

De lo contrario, los objetos son iguales.

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

def __eq__(yo, otro): return


len(yo) == len(otro) and all(a == b for a, b in zip(yo, otro))

Tenga en cuenta que primero verificamos que los operandos tengan la misma longitud, porque zip se detendrá
en el operando más corto.

El ejemplo 10-14 es la implementación que elegimos para __eq__ en vector_v4.py.

Finalizamos este capítulo recuperando el método __format__ de Vector2d a


Vector.

292 | Capítulo 10: Secuencia Hacking, Hashing y Slicing


Machine Translated by Google

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.

La función zip lleva el nombre del cierre de cremallera porque el


dispositivo físico funciona entrelazando pares de dientes tomados
de ambos lados de la cremallera, una buena analogía visual de lo
que hace zip (izquierda, derecha) . Sin relación con archivos comprimidos.

Ejemplo 10-15. El zip incorporado en el trabajo

>>> zip(rango(3), 'ABC') # <objeto


zip en 0x10063ae48> >>>
list(zip(rango(3), 'ABC')) # [(0, 'A'), (1 ,
'B'), (2, 'C')] >>> lista(zip(rango(3), 'ABC',
[0.0, 1.1, 2.2, 3.3])) # [(0, 'A', 0.0), (1, 'B', 1.1), (2, 'C', 2.2)] >>> from
itertools import zip_longest # >>> list(zip_longest(rango(3), 'ABC',
[0.0, 1.1, 2.2, 3.3], valor de relleno=-1))

[(0, 'A', 0.0), (1, 'B', 1.1), (2, 'C', 2.2), (-1, -1, 3.3)]

zip devuelve un generador que produce tuplas bajo demanda.

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 función itertools.zip_longest se comporta de manera diferente: utiliza un valor de relleno opcional


(Ninguno de forma predeterminada) para completar los valores faltantes, de modo que pueda generar
tuplas hasta que se agote el último iterable.

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.

Vector Take #4: Hashing y un == más rápido | 293


Machine Translated by Google

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.

Vector Take #5: Formateo


El método __format__ de Vector se parecerá al de Vector2d, pero en lugar de proporcionar una
visualización personalizada en coordenadas polares, Vector utilizará coordenadas esféricas, también
conocidas como coordenadas "hiperesféricas", porque ahora admitimos n dimensiones, y las esferas son
"hiperesferas". ” en 4D y más allá.6 En consecuencia, cambiaremos el sufijo de formato personalizado
de 'p' a 'h'.

Como vimos en “Pantallas formateadas” en la página 253, cuando se amplía


el minilenguaje de especificación de formato , es mejor evitar la reutilización
de códigos de formato admitidos por tipos integrados. En particular, nuestro
minilenguaje extendido también usa los códigos de formato flotante 'eEfFgGn
%' en su significado original, por lo que definitivamente debemos evitarlos.
Los números enteros usan 'bcdoxXn' y las cadenas usan 's'. Elegí 'p' para
las coordenadas polares Vec tor2d . El código 'h' para coordenadas
hiperesféricas es una buena elección.

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

>>> formato(Vector([-1, -1, -1, -1]), 'h') '<2.0,


2.0943951023931957, 2.186276035465284, 3.9269908169872414>' >>>
formato(Vector([2, 2, 2 , 2]), '.3eh') '<4.000e+00, 1.047e+00, 9.553e-01,
7.854e-01>' >>> formato(Vector([0, 1, 0, 0]) , '0.5fh') '<1.00000, 1.57080,
0.00000, 0.00000>'

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”.

294 | Capítulo 10: Secuencia Hacking, Hashing y Slicing


Machine Translated by Google

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__
"""

Una clase ``Vector`` multidimensional, toma 5

Un ``Vector`` se construye a partir de una iteración de números::

>>> Vector([3.1, 4.2])


Vector([3.1, 4.2])
>>> Vector((3, 4, 5))
Vector([3.0, 4.0, 5.0])
>>> Vector(rango(10))
Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])

Pruebas con dos dimensiones (mismos resultados que ``vector2d_v1.py``)::

>>> v1 = Vector([3, 4]) >>> x, y =


v1
>>> x, y
(3.0, 4.0)
>>> v1
Vector([3.0, 4.0]) >>>
v1_clone = eval(repr(v1)) >>> v1 ==
v1_clone
Verdadero

>>> 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@' >>> abs(v1)

5.0
>>> bool(v1), bool(Vector([0, 0]))
(Verdadero Falso)

Prueba del método de clase ``.frombytes()``:

>>> v1_clone = Vector.frombytes(bytes(v1)) >>> v1_clone


Vector([3.0, 4.0]) >>> v1 == v1_clone True

Ensayos con tres dimensiones:

Vector Take #5: Formateo | 295


Machine Translated by Google

>>> v1 = Vector([3, 4, 5]) >>> x,


y, z = v1 >>> x, y, z (3.0, 4.0,
5.0) >>> v1 Vector([3.0, 4.0 ,
5.0]) >>> v1_clone =
eval(repr(v1)) >>> v1 ==
v1_clone True >>> print(v1) (3.0,
4.0, 5.0) >>> abs(v1) # doctest:
+ELIPSIS

7.071067811...
>>> bool(v1), bool(Vector([0, 0, 0]))
(Verdadero Falso)

Pruebas con muchas dimensiones:

>>> v7 = Vector(rango(7)) >>>


v7
Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...]) >>> abs(v7)
# doctest:+ELIPSIS 9.53939201...

Prueba de los métodos ``.__bytes__`` y ``.frombytes()``:

>>> v1 = Vector([3, 4, 5]) >>>


v1_clone = Vector.frombytes(bytes(v1)) >>> v1_clone
Vector([3.0, 4.0, 5.0]) >>> v1 == v1_clone Verdadero

Pruebas de comportamiento de secuencias::

>>> v1 = Vector([3, 4, 5]) >>>


largo(v1) 3

>>> v1[0], v1[largo(v1)-1], v1[-1] (3.0,


5.0, 5.0)

Prueba de corte::

>>> v7 = Vector(rango(7)) >>>


v7[-1] 6.0 >>> v7[1:4]

Vector([1.0, 2.0, 3.0])

296 | Capítulo 10: Secuencia Hacking, Hashing y Slicing


Machine Translated by Google

>>> v7[-1:]
Vector([6.0])
>>> v7[1,2]
Rastreo (llamadas recientes más última):
...
TypeError: los índices vectoriales deben ser números enteros

Pruebas de acceso a atributos dinámicos:

>>> v7 = Vector(rango(10)) >>>


v7.x
0.0
>>> v7.y, v7.z, v7.t (1.0,
2.0, 3.0)

Errores de búsqueda de atributos dinámicos::

>>> 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::

>>> v1 = Vector([3, 4]) >>>


v2 = Vector([3.1, 4.2]) >>> v3 =
Vector([3, 4, 5]) >>> v6 =
Vector(rango( 6)) >>> hash(v1),
hash(v3), hash(v6) (7, 2, 1)

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:

>>> import sys


>>> hash(v2) == (384307168202284039 si sys.maxsize > 2**32 else 357915986)
Verdadero

Pruebas de ``format()`` con coordenadas cartesianas en 2D::

Vector Take #5: Formateo | 297


Machine Translated by Google

>>> v1 = Vector([3, 4]) >>>


formato(v1) '(3.0, 4.0)' >>>
formato(v1, '.2f') '(3.00, 4.00)'
>>> formato (v1, '.3e')
'(3.000e+00, 4.000e+00)'

Pruebas de ``format()`` con coordenadas cartesianas en 3D y 7D:

>>> v3 = Vector([3, 4, 5]) >>>


formato(v3) '(3.0, 4.0, 5.0)' >>>
formato(Vector(rango(7))) '(0.0,
1.0, 2.0, 3.0, 4.0, 5.0, 6.0)'

Pruebas de ``format()`` con coordenadas esféricas en 2D, 3D y 4D:

>>> formato(Vector([1, 1]), 'h') # pruebadoc:+ELIPSIS '<1.414213...,


0.785398...>' >>> formato(Vector([1, 1]), '.3eh') '<1.414e+00,
7.854e-01>' >>> formato(Vector([1, 1]), '0.5fh') '<1.41421, 0.78540>'
>>> formato(Vector ([1, 1, 1]), 'h') # doctest:+ELIPSIS '<1.73205...,
0.95531..., 0.78539...>' >>> formato(Vector([2, 2, 2 ]), '.3eh')
'<3.464e+00, 9.553e-01, 7.854e-01>' >>> formato(Vector([0, 0,
0]), '0.5fh') '<0.00000 , 0.00000, 0.00000>' >>> format(Vector([-1, -1,
-1, -1]), 'h') # doctest:+ELIPSIS '<2.0, 2.09439..., 2.18627... ,
3.92699...>' >>> formato(Vector([2, 2, 2, 2]), '.3eh') '<4.000e+00,
1.047e+00, 9.553e-01, 7.854e- 01>' >>> formato(Vector([0, 1, 0, 0]),
'0.5fh') '<1.00000, 1.57080, 0.00000, 0.00000>'

"""

from matriz importar matriz


importar reprlib importar
matemáticas importar
números importar funciones
importar operador importar
itertools

clase vectorial:
código de tipo = 'd'

298 | Capítulo 10: Secuencia Hacking, Hashing y Slicing


Machine Translated by Google

def __init__(uno mismo, componentes):


self._components = array(self.typecode, componentes)

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 __eq__(yo, otro): return


(len(yo) == len(otro) and all(a == b for a, b
in zip(yo, otro)))

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)

def __getitem__(self, index): cls =


type(self) if isinstance(index,
slice): return
cls(self._components[index]) elif
isinstance(index, numbers.Integral):
return self._components[index]
else: msg = '{.__name__} los índices
deben ser enteros' raise TypeError(msg.format(cls))

atajo_nombres = 'xyzt'

def __getattr__(self, name): cls =


type(self) if len(name) == 1:
pos =
cls.shortcut_names.find(name) if 0 <= pos
< len(self._components):

Vector Take #5: Formateo | 299


Machine Translated by Google

devolver self._components[pos]
msg = '{.__name__!r} objeto no tiene atributo {!r}'
aumentar AttributeError(msg.format(cls, nombre))

def ángulo(self, n): r =


math.sqrt(sum(x * x for x in self[n:]))
a = matemáticas.atan2(r, self[n-1])
si (n == len(self) - 1) y (self[-1] < 0):
devuelve matemáticas.pi * 2 - a
más:
devolver un

ángulos def (auto):


return (auto.ángulo (n) para n en el rango (1, len (auto)))

def __format__(self, fmt_spec=''):


if fmt_spec.endswith('h'): # coordenadas hiperesféricas
especificación_fmt = especificación_fmt[:-1]
coords = itertools.chain([abs(self)],
self.ángulos())
exterior_fmt = '<{}>'
más:
coordenadas = uno mismo

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)

Importe itertools para usar la función de cadena en __formato__.

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.

Configure la visualización de coordenadas esféricas con corchetes angulares.

Configure la visualización de coordenadas cartesianas con paréntesis.

Cree una expresión de generador para dar formato a cada elemento de coordenadas a pedido.

Conecte los componentes formateados separados por comas dentro de corchetes o


paréntesis.

300 | Capítulo 10: Secuencia Hacking, Hashing y Slicing


Machine Translated by Google

Estamos haciendo un uso intensivo de expresiones generadoras en


__formato__, ángulo y ángulos , pero nuestro enfoque aquí es
proporcionar __formato__ para llevar a Vector al mismo nivel de
implementación que Vector2d. Cuando cubramos los generadores en el
Capítulo 14, usaremos parte del código en Vector como ejemplos, y
luego se explicarán los trucos del generador en detalle.

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.

Resumen del capítulo


El ejemplo de Vector en este capítulo fue diseñado para ser compatible con Vector2d, excepto por el
uso de una firma de constructor diferente que acepta un solo argumento iterable, tal como lo hacen los
tipos de secuencia integrados. El hecho de que Vector se comporte como una secuencia simplemente
implementando __getitem__ y __len__ provocó una discusión sobre protocolos, las interfaces informales
utilizadas en lenguajes tipo pato.

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

Resumen del capítulo | 301


Machine Translated by Google

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

Protocolos como interfaces informales

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".

302 | Capítulo 10: Secuencia Hacking, Hashing y Slicing


Machine Translated by Google

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.

— Capítulo "Modelo de datos" de The Python Language Reference

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.

Orígenes de Duck Typing

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.

Un formato seguro , con Usabilidad Mejorada

Al implementar __format__, no tomamos ninguna precaución con respecto a las instancias de


Vector con una gran cantidad de componentes, como hicimos en __repr__ usando re prlib. El
razonamiento es que repr() es para la depuración y el registro, por lo que siempre debe generar
una salida útil, mientras que __format__ se usa para mostrar la salida a los usuarios finales que
presumiblemente quieren ver todo el Vector. Si cree que esto es peligroso, sería bueno implementar
una extensión adicional al minilenguaje del especificador de formato.

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!

La búsqueda de una suma pitónica

Lectura adicional | 303


Machine Translated by Google

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 :

>>> functools.reduce(lambda a, b: a + b[1], mi_lista, 0)


60

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:

>>> import numpy as np


>>> my_array = np.array(my_list) >>>
np.sum(my_array[:, 1]) 60

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

Entonces Evan Simpson preguntó: “¿Qué tiene de malo esto?”:

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.

304 | Capítulo 10: Secuencia Hacking, Hashing y Slicing


Machine Translated by Google

>>> 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?

Entonces Alex Martelli vuelve a sugerir:

"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:

>>> suma([sub[1] para sub en mi_lista]) 60

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:

>>> suma(sub[1] para sub en mi_lista) 60

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.

Lectura adicional | 305


Machine Translated by Google
Machine Translated by Google

CAPÍTULO 11

Interfaces: de protocolos a ABC

Una clase abstracta representa una interfaz.1

- 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.

Comenzaremos el capítulo revisando cómo la comunidad de Python tradicionalmente entendía las


interfaces como un tanto flexibles, en el sentido de que una interfaz parcialmente implementada suele
ser aceptable. Lo dejaremos claro a través de un par de ejemplos que resaltan la naturaleza dinámica
de la tipificación pato.

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.

1. Bjarne Stroustrup, El diseño y la evolución de C++ (Addison-Wesley, 1994), pág. 278.

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.

Comencemos con la vista Pythonic de las interfaces.

Interfaces y protocolos en la cultura de Python


Python ya tenía un gran éxito antes de que se introdujeran los ABC, y la mayoría de los códigos
existentes no los utilizan en absoluto. Desde el Capítulo 1, hemos estado hablando sobre la tipificación
de patos y los protocolos. En “Protocolos y Duck Typing” en la página 279, los protocolos se definen
como las interfaces informales que hacen que el polimorfismo funcione en lenguajes con escritura
dinámica como Python.

¿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'

def __init__(self, x, y): self.x =


float(x) self.y = float(y)

def __iter__(uno mismo):

308 | Capítulo 11: Interfaces: de protocolos a ABC


Machine Translated by Google

return (i por i en (self.x, self.y))

Siguen # más métodos (omitidos en esta lista)

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.

Ejemplo 11-2. vector2d_v3.py: x e y reimplementadas como propiedades (consulte la lista completa


en el Ejemplo 9-9)
clase Vector2d:
código de tipo = 'd'

def __init__(self, x, y): self.__x


= float(x) self.__y = float(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))

Siguen # más métodos (omitidos en esta lista)

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

Interfaces y Protocolos en la Cultura Python | 309


Machine Translated by Google

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.

Secuencias de excavaciones de Python

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".

310 | Capítulo 11: Interfaces: de protocolos a ABC


Machine Translated by Google

...
>>> 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

Carta = colecciones.namedtuple('Carta', ['rango', 'palo'])

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)

def __getitem__(self, posición): return


self._cards[posición]

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

Secuencias de excavaciones de Python | 311


Machine Translated by Google

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.

Monkey-Patching para implementar un protocolo en tiempo de ejecución

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".

Cuando sigue los protocolos establecidos, mejora sus posibilidades de


aprovechar la biblioteca estándar existente y el código de terceros,
gracias a la tipificación pato.

La función random.shuffle estándar se usa así:

>>> de importación aleatoria


aleatoria >>> l = lista(rango(10)) >>>
aleatoria(l) >>> l [5, 2, 9, 7, 8, 3, 1, 4,
0, 6 ]

Sin embargo, si tratamos de barajar una instancia de FrenchDeck , obtenemos una excepción, como
en el Ejemplo 11-5.

Ejemplo 11-5. random.shuffle no puede manejar FrenchDeck


>>> from random import shuffle
>>> from frenchdeck import FrenchDeck
>>> mazo = FrenchDeck() >>> shuffle(mazo)

Rastreo (llamadas recientes más última):


Archivo "<stdin>", línea 1, en <módulo>
Archivo ".../python3.3/random.py", línea 265, en orden aleatorio
x[i], x[j] = x[j], x [i]
TypeError: el objeto 'FrenchDeck' no admite la asignación de elementos

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__ .

312 | Capítulo 11: Interfaces: de protocolos a ABC


Machine Translated by Google

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)

>>> def establecer_carta(mazo, posición,


... carta): mazo._cartas[posición] = carta
...
>>> FrenchDeck.__setitem__ = set_card
>>> shuffle(mazo) >>> mazo[:5]

[Carta(rango='3', palo='corazones'), Carta(rango='4', palo='diamantes'), Carta(rango='4',


palo='tréboles'), Carta(rango= '7', palo='corazones'), Carta(rango='9', palo='picas')]

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.

Monkey-Patching para implementar un protocolo en tiempo de ejecución | 313


Machine Translated by Google

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.

Las aves acuáticas de Alex Martelli

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.

Estoy muy agradecido a Alex Martelli. Ya era la persona más citada


en este libro antes de convertirse en uno de los editores técnicos.
Sus ideas han sido invaluables, y luego se ofreció a escribir este
ensayo. Somos increíblemente afortunados de tenerlo. ¡Quítatelo,
Álex!

Aves acuáticas y ABC

Por Alex Martelli Se

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:

314 | Capítulo 11: Interfaces: de protocolos a ABC


Machine Translated by Google

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.

Las aves acuáticas de Alex Martelli | 315


Machine Translated by Google

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:

>>> lucha de clases :


... def __len__(self): return 23
...
>>> from collections import abc >>>
isinstance(Struggle(), abc.Sized)
Verdadero

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

316 | Capítulo 11: Interfaces: de protocolos a ABC


Machine Translated by Google

Y, no defina ABC personalizados (o metaclases) en el código de producción... si siente la necesidad de


hacerlo, apuesto a que es probable que sea un caso de "todos los problemas parecen un clavo"-
síndrome para alguien que simplemente obtuvo un nuevo y brillante martillo: usted (y los futuros
mantenedores de su código) estarán mucho más felices si se quedan con un código directo y simple,
evitando tales profundidades. ¡Valle!

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 .

Hay una excepción común y práctica a la recomendación anterior: algunas


API de Python aceptan una sola str o una secuencia de elementos de
str ; si es solo una cadena, desea envolverla en una lista para facilitar el
procesamiento. Debido a que str es un tipo de secuencia, la forma más
sencilla de distinguirla de cualquier otra secuencia inmutable es realizar
una comprobación explícita isinstance(x, str) .4

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.

Las aves acuáticas de Alex Martelli | 317


Machine Translated by Google

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

intente: field_names = field_names.replace(',', ' ').split() excepto AttributeError:

pasar
nombres_de_campo = tupla(nombres_de_campo)

Supongamos que es una cadena (EAFP = es más fácil pedir perdón que permiso).

Convierta comas en espacios y divida el resultado en una lista de nombres.

Lo siento, field_names no grazna como una cadena... o no hay .replace, o devuelve algo que no
podemos .split.

Ahora asumimos que ya es un iterable de nombres.

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.

5. Este fragmento se extrajo del Ejemplo 21-2.

318 | Capítulo 11: Interfaces: de protocolos a ABC


Machine Translated by Google

Ahora veamos la escritura de ganso en la práctica.

Subclasificación de un ABC

Siguiendo el consejo de Martelli, aprovecharemos un ABC existente, collections.MutableSequence, antes de


atrevernos a inventar el nuestro. En el Ejemplo 11-8, FrenchDeck2 se declara explícitamente como una subclase
de collections.MutableSequence.

Ejemplo 11-8. frenchdeck2.py: FrenchDeck2, una subclase de colecciones.MutableSequence

importar colecciones

Carta = colecciones.namedtuple('Carta', ['rango', 'palo'])

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)

def __getitem__(self, posición): return


self._cards[posición]

def __setitem__(self, posición, valor): #


self._cards[posición] = valor

def __delitem__(yo, posición): #


del self._cards[posicion]

def insert(auto, posición, valor): #


self._cards.insert(posición, valor)

__setitem__ es todo lo que necesitamos para habilitar la reproducción aleatoria...

Pero subclasificar MutableSequence nos obliga a implementar __delitem__, un método abstracto de ese
ABC.

También estamos obligados a implementar insert, el tercer método abstracto de MutableSequence.

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".

Subclasificación de un ABC | 319


Machine Translated by Google

clase abstracta FrenchDeck2 con métodos abstractos __delitem__, insertar".


Es por eso que debemos implementar __delitem__ e insertar, incluso si nuestros ejemplos de
FrenchDeck2 no necesitan esos comportamientos: MutableSequence ABC los exige.

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)

De Sequence, FrenchDeck2 hereda los siguientes métodos concretos listos para


usar: __contains__, __iter__, __reversed__, index y count. Desde MutableSequence,
se agrega, invierte, extiende , extrae, elimina y __iadd__ .
Los métodos concretos en cada collections.abc ABC se implementan en términos de la interfaz
pública de la clase, por lo que funcionan sin ningún conocimiento de la estructura interna de las
instancias.

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.

320 | Capítulo 11: Interfaces: de protocolos a ABC


Machine Translated by Google

ABC en la biblioteca estándar


Desde Python 2.6, los ABC están disponibles en la biblioteca estándar. La mayoría están
definidas en el módulo collections.abc , pero hay otras. Puede encontrar ABC en los
paquetes de números y io , por ejemplo. Pero el más utilizado es collections.abc. Veamos
qué hay disponible allí.

ABC en colecciones.abc

Hay dos módulos llamados abc en la biblioteca estándar. Aquí estamos


hablando de colecciones.abc. Para reducir el tiempo de carga, en
Python 3.4, se implementa fuera del paquete de colecciones , en Lib/
_collections_abc.py), por lo que se importa por separado de las
colecciones. El otro módulo abc es simplemente abc (es decir, Lib/
abc.py) donde se define la clase abc.ABC . Cada ABC depende de él,
pero no necesitamos importarlo nosotros mismos excepto para crear un nuevo 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.

ABC en la biblioteca estándar | 321


Machine Translated by Google

Figura 11-3. Diagrama de clases UML para ABC en colecciones.abc

Revisemos los clústeres en la Figura 11-3:


Iterable, Contenedor y Tamaño Cada
colección debe heredar de estos ABC o al menos implementar protocolos compatibles. Iterable
admite la iteración con __iter__, Container admite el operador in con __contains__ y Sized
admite len() con __len__.

Secuencia, mapeo y conjunto


Estos son los principales tipos de colecciones inmutables y cada uno tiene una subclase mutable.
Un diagrama detallado para MutableSequence está en la Figura 11-2; para MutableMapping y
MutableSet, hay diagramas en el Capítulo 3 (Figuras 3-1 y 3-2).

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

322 | Capítulo 11: Interfaces: de protocolos a ABC


Machine Translated by Google

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.

La Torre de los Números del ABC

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.

Sorprendentemente, decimal.Decimal no está registrado como una


subclase virtual de números.Real. La razón es que, si necesita la
precisión de Decimal en su programa, entonces quiere estar
protegido contra la mezcla accidental de decimales con otros tipos
numéricos menos precisos, particularmente flotantes.

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.

ABC en la biblioteca estándar | 323


Machine Translated by Google

Después de ver algunos ABC existentes, practiquemos la mecanografía de ganso implementando un


ABC desde cero y poniéndolo en práctica. El objetivo aquí no es alentar a todos a comenzar a
codificar ABC de izquierda a derecha, sino aprender a leer el código fuente de ABC que encontrará
en la biblioteca estándar y otros paquetes.

Definición y uso de un ABC


Para justificar la creación de un ABC, debemos encontrar un contexto para usarlo como un punto de
extensión en un marco. Este es nuestro contexto: imagine que necesita mostrar anuncios en un sitio
web o en una aplicación móvil en orden aleatorio, pero sin repetir un anuncio antes de que se
muestre el inventario completo de anuncios. Ahora supongamos que estamos construyendo un
marco de gestión de anuncios llamado ADAM. Uno de sus requisitos es admitir clases de selección
aleatoria no repetitivas proporcionadas por el usuario.8 Para dejar claro a los usuarios de ADAM lo
que se espera de un componente de “selección aleatoria no repetitiva”, definiremos un ABC.

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:

• .load(…): poner elementos en el contenedor.

• .pick(): quita un elemento al azar del contenedor, devolviéndolo.

Los métodos concretos son:

• .loaded(): devuelve True si hay al menos un elemento en el contenedor.

• .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).

La figura 11-4 muestra la Tómbola ABC y tres implementaciones concretas.

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”.

324 | Capítulo 11: Interfaces: de protocolos a ABC


Machine Translated by Google

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

El ejemplo 11-9 muestra la definición de la tómbola ABC.

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.

Este método debería generar `LookupError` cuando la instancia está vacía.


"""

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.

Definición y uso de un ABC | 325


Machine Translated by Google

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

Para definir un ABC, subclase abc.ABC.

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

implementadores que generen LookupError si no hay elementos para elegir.

Un ABC puede incluir métodos concretos.

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 resultado de la inspección al vaciar la tómbola con llamadas sucesivas a .pick()… …luego

usar .load(…) para devolver todo.

Un método abstracto en realidad puede tener una implementación.


Incluso si lo hace, las subclases aún se verán obligadas a anularlo, pero
podrán invocar el método abstracto con super(), añadiéndole
funcionalidad en lugar de implementarlo desde cero. Consulte la
documentación del módulo abc para obtener detalles sobre el uso de @abstractmethod .

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.

326 | Capítulo 11: Interfaces: de protocolos a ABC


Machine Translated by Google

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

Elegí la excepción LookupError debido a su lugar en la jerarquía de Python de exÿ


cepciones en relación con IndexError y KeyError, las excepciones más probables que se planteen
por las estructuras de datos utilizadas para implementar una tómbola concreta . Por lo tanto, implementar
Las acciones pueden generar LookupError, IndexError o KeyError para cumplir. Ver Ejemplo 11-10
(Para ver un árbol completo, consulte "5.4. Jerarquía de excepciones" de la biblioteca estándar de Python).

Ejemplo 11-10. Parte de la jerarquía de clases de excepción

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.

LookupError es la excepción que manejamos en Tombola.inspect.

IndexError es la subclase LookupError generada cuando intentamos obtener un elemento de


una secuencia con un índice más allá de la última posición.

KeyError se genera cuando usamos una clave inexistente para obtener un elemento de un mapeo.

Definición y uso de un ABC | 327


Machine Translated by Google

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.

Ejemplo 11-11. Una tómbola falsa no pasa desapercibida


>>> from tombola import Tombola
>>> class Fake(Tombola): # def
... pick(self): return 13
...
...
>>> Falso #
<clase '__principal__.Falso'>
<clase 'abc.ABC'>, <clase 'objeto'>) >>> f =
Falso() # Rastreo (última llamada más
reciente):
Archivo "<stdin>", línea 1, en <módulo>
TypeError: no se puede crear una instancia de clase abstracta Fake con carga de métodos abstractos

Declare Fake como una subclase de Tombola.

La clase fue creada, sin errores hasta ahora.

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__ :

clase Tombola(objeto): # esto es Python 2!!!


__metaclase__ = abc.ABCMeta #
...

328 | Capítulo 11: Interfaces: de protocolos a ABC


Machine Translated by Google

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.

Además de @abstractmethod, el módulo abc define los decoradores @abstractclassmethod ,


@abstractstaticmethod y @abstractproperty . Sin embargo, estos últimos tres están en desuso desde
Python 3.3, cuando fue posible apilar decoradores sobre @abstractmethod, haciendo que los demás
fueran redundantes. Por ejemplo, la forma preferida de declarar un método de clase abstracto es:

class MyABC(abc.ABC):
@classmethod
@abc.abstractmethod def
an_abstract_classmethod(cls, ...):
pasar

El orden de los decoradores de funciones apiladas suele ser importante


y, en el caso de @abstractmethod, la documentación es explícita:

Cuando se aplica abstractmethod() en combinación con otros


descriptores de métodos, debe aplicarse como el decorador
más interno, …12

En otras palabras, no puede aparecer ningún otro decorador entre el


método @abstract y la instrucción def .

Ahora que hemos cubierto estos problemas de sintaxis ABC, pongamos a Tombola en uso
implementando algunos descendientes concretos de él.

Subclase de la Tómbola ABC Dada la

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__.

Ejemplo 11-12. bingo.py: BingoCage es una subclase concreta de Tombola

importar al azar

de tómbola importar tómbola

12. Entrada @abc.abstractmethod en la documentación del módulo abc.

Definición y uso de un ABC | 329


Machine Translated by Google

clase BingoCage(Tómbola):

def __init__(uno mismo, elementos):


self._randomizer = random.SystemRandom()
self._items = [] self.load(elementos)

def load(self, items):


self._items.extend(items)
self._randomizer.shuffle(self._items)

def pick(self): try:


return
self._items.pop() excepto
IndexError: raise
LookupError('seleccionar de BingoCage vacío')

def __call__(self):
self.pick()

Esta clase BingoCage amplía explícitamente Tombola.

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 .

Delegue la carga inicial al método .load(…) .

En lugar de la simple función random.shuffle() , usamos el método .shuffle() de nuestra instancia


SystemRandom .

pick se implementa como en el Ejemplo 5-8.

__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.

330 | Capítulo 11: Interfaces: de protocolos a ABC


Machine Translated by Google

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

de tómbola importar tómbola

clase LotteryBlower(Tómbola):

def __init__(auto, iterable):


self._bolas = lista(iterable)

def load(auto, iterable):


self._balls.extend(iterable)

def pick(self): try:


position =
random.randrange(len(self._balls)) excepto ValueError:
raise LookupError('seleccionar de BingoCage vacío')
return self._balls.pop(posición)

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.

La función random.randrange(…) genera ValueError si el rango está vacío, por lo que


capturamos eso y lanzamos LookupError en su lugar, para que sea compatible con Tombola.

De lo contrario, el elemento seleccionado al azar se extrae de self._balls.

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.

Anule la inspección con una sola línea.

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.

Definición y uso de un ABC | 331


Machine Translated by Google

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ó . .

Una subclase virtual de Tombola


Una característica esencial de la tipificación de ganso, y la razón por la que merece un nombre de ave
acuática, es la capacidad de registrar una clase como una subclase virtual de un ABC, incluso si no
hereda de él. Al hacerlo, prometemos que la clase implementa fielmente la interfaz definida en el ABC,
y Python nos creerá sin verificar. Si mentimos, seremos atrapados por las excepciones de tiempo de
ejecución habituales.

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.

Las subclases virtuales no se heredan de sus ABC registrados y no se


verifica su conformidad con la interfaz ABC en ningún momento, ni siquiera
cuando se crean instancias. Depende de la subclase implementar
realmente todos los métodos necesarios para evitar errores de tiempo de ejecución.

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

problema que acabamos de evitar aquí.

332 | Capítulo 11: Interfaces: de protocolos a ABC


Machine Translated by Google

Figura 11-5. Diagrama de clase UML para TomboList, una subclase real de lista y una virtual
subclase de tómbola

Ejemplo 11-14. tombolist.py: la clase TomboList es una subclase virtual de Tombola


de randrange de importación aleatoria

de tómbola importar tómbola

@Tombola.register # clase
TomboList(lista): #

def elegir(auto):
if self: # posición
= randrange(len(self))
return self.pop(posición) # else:

aumentar LookupError(' salir de TomboList vacío')

cargar = lista.extender #

def cargado(auto):
volver bool(uno mismo) #

def inspeccionar(auto):
return tuple(ordenado(auto))

Definición y uso de un ABC | 333


Machine Translated by Google

# Tombola.registro(TomboList) #

Tombolist está registrado como una subclase virtual de Tombola.


Tombolist amplía lista.

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.

Tombolist.load es lo mismo que list.extend.


15
delegados cargados a bool.
Si usa Python 3.3 o anterior, no puede usar .register como decorador de clases.
Debe utilizar la sintaxis de llamada estándar.

Tenga en cuenta que debido al registro, las funciones issubclass e isinstance actúan como si
TomboList fuera una subclase de Tombola:

>>> from tombola import Tombola


>>> from tombolist import TomboList
>>> issubclass(TomboList, Tombola)
Verdadero

>>> t = TomboList(rango(100)) >>>


esinstancia(t, Tombola)
Verdadero

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á.

334 | Capítulo 11: Interfaces: de protocolos a ABC


Machine Translated by Google

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.

Cómo se probaron las subclases de tómbola


El script que usé para probar los ejemplos de Tombola usa dos atributos de clase que permiten la introspección de
una jerarquía de clases: __subclasses__()

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.

Una ejecución exitosa del script de prueba se ve así:

$ 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.

Ejemplo 11-15. tombola_runner.py: corredor de pruebas para las subclases de Tombola

importar documento

de tómbola importar tómbola

# módulos para probar


importación de bingo, lotería, tombolista, tambor

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)

para cls en subclases_reales + subclases_virtuales : test(cls,


detallado)

Cómo se probaron las subclases de Tombola | 335


Machine Translated by Google

def test(cls, detallado=Falso):

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

si __name__ == '__main__': import


sys main(sys.argv)

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.

El resultado de la prueba se imprime con el nombre de la clase, el número de pruebas


intentadas, las pruebas fallidas y una etiqueta de 'OK' o 'FAIL' .

El archivo doctest es el Ejemplo 11-16.

Ejemplo 11-16. tombola_tests.rst: doctests para las subclases de Tombola


==============
Pruebas de tómbola
==============

Cada subclase concreta de Tombola debería pasar estas pruebas.

Crear y cargar una instancia desde iterable::

>>> bolas = lista(rango(3)) >>>


globo = ConcreteTombola(bolas) >>>
globo.loaded()

Verdadero >>>
globo.inspeccionar() (0, 1, 2)

336 | Capítulo 11: Interfaces: de protocolos a ABC


Machine Translated by Google

Elige y recoge bolas::

>>> selecciones
= [] >>> selecciones.append(globo.pick())
>>> selecciones.append(globo.pick())
>>> selecciones.append(globo.pick())

Comprobar estado y resultados::

>>> globo.loaded()
Falso
>>> ordenado(selecciona) == bolas
Verdadero

Recargar::

>>> globo.load(bolas) >>>


globo.loaded()
Verdadero

>>> selecciones = [globo.pick() for i in balls] >>>


globo.loaded()
Falso

Verifique que `LookupError` (o una subclase) sea la excepción lanzada


cuando el dispositivo está vacío:

>>> globo = ConcreteTombola([]) >>>


prueba: globo.pick() ... excepto
... LookupError como exc:
print('OK')
...
OK

Carga y recoge 100 bolas para comprobar que salen todas:

>>> bolas = lista(rango(100)) >>>


globo = ConcreteTombola(bolas) >>> picks
= [] >>> while globo.inspect():

... picks.append(globo.pick()) >>>


len(picks) == len(bolas)
Verdadero

>>> conjunto(selecciones) == conjunto(bolas)


Verdadero

Cómo se probaron las subclases de Tombola | 337


Machine Translated by Google

Comprueba que el orden ha cambiado y no está simplemente invertido:

>>> recoge != pelotas


Verdadero >>> recoge[::-1] !
= pelotas Verdadero

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.

Uso del registro en la práctica


En el Ejemplo 11-14, usamos Tombola.register como decorador de clase. Antes de Python 3.3, el
registro no se podía usar de esa manera: tenía que llamarse como una función simple después de la
definición de la clase, como sugiere el comentario al final del Ejemplo 11-14.

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.

Los gansos pueden comportarse como patos

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:

338 | Capítulo 11: Interfaces: de protocolos a ABC


Machine Translated by Google

>>> lucha de clases :


... def __len__(self): return 23
...
>>> from collections import abc >>>
isinstance(Struggle(), abc.Sized)
Verdadero

>>> issubclass(Lucha, abc.Tamaño)


Verdadero

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)

Tamaño de clase (metaclase = ABCMeta):

__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 hay un atributo llamado __len__ en el __dict__ de cualquier clase enumerada en


C.__mro__ (es decir, C y sus superclases)... ...devuelve True, lo que indica que C es una

subclase virtual de Sized.

De lo contrario, devuelva NotImplemented para permitir que continúe la verificación de la subclase.

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.

El __subclasshook__ agrega algo de ADN de tipificación de pato a toda la propuesta de tipificación


de ganso. Puede tener definiciones de interfaz formales con ABC, puede realizar verificaciones de
instancias en todas partes y aún así tener una clase completamente no relacionada solo porque
implementa un método determinado (o porque hace lo que sea necesario para convencer a un
__subclasshook__ para que responda por ello ) . Por supuesto, esto solo funciona para ABC que
sí proporcionan un __subclasshook__.

Los gansos pueden comportarse como patos | 339


Machine Translated by Google

¿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.

Resumen del capítulo


El objetivo de este capítulo fue viajar desde la naturaleza altamente dinámica de las interfaces
informales, llamadas protocolos, visitar las declaraciones de interfaz estática de ABC y concluir
con el lado dinámico de ABC: subclases virtuales y detección de subclases dinámicas con
__subclasshook__.

Comenzamos el viaje revisando la comprensión tradicional de las interfaces en la comunidad de


Python. Durante la mayor parte de la historia de Python, hemos tenido en cuenta las interfaces,
pero eran informales como los protocolos de Smalltalk, y los documentos oficiales usaban lenguaje
como "protocolo foo", "interfaz foo" y "objeto similar a foo". ” indistintamente.
Las interfaces de estilo de protocolo no tienen nada que ver con la herencia; cada clase está sola
cuando implementa un protocolo. Así es como se ven las interfaces cuando adoptas la escritura
pato.

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.

340 | Capítulo 11: Interfaces: de protocolos a ABC


Machine Translated by Google

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?"

Resumen del capítulo | 341


Machine Translated by Google

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.

Aquí hay una cita apropiada para terminar este capítulo:

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

—David Beazley y Brian Jones


Libro de cocina de Python

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.

342 | Capítulo 11: Interfaces: de protocolos a ABC


Machine Translated by Google

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”.

El paquete zope.interface proporciona una forma de declarar interfaces, verificar si los


objetos las implementan, registrar proveedores y consultar proveedores de una interfaz
determinada. El paquete comenzó como una pieza central de Zope 3, pero puede y ha sido
usado fuera de Zope. Es la base de la arquitectura de componentes flexibles de proyectos
Python a gran escala como Twisted, Pyramid y Plone. Lennart Regebro tiene una gran
introducción a zope.interface en “A Python Component Architecture”. Baiju M escribió un
libro completo al respecto: Una guía completa para la arquitectura de componentes de Zope.

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

Lectura adicional | 343


Machine Translated by Google

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.

Finalmente, el PEP 484 establece claramente:

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.

¿Está Python débilmente tipificado?

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

escrito. Java, C++ y Python están fuertemente tipados.

PHP, JavaScript y Perl tienen un tipo débil.

Escritura estática versus dinámica


Si la comprobación de tipos se realiza en tiempo de compilación, el lenguaje se tipifica estáticamente; si sucede en
tiempo de ejecución, se escribe dinámicamente. La escritura estática requiere declaraciones de tipos (algunos lenguajes
modernos usan la inferencia de tipos para evitar algo de eso). Fortran y Lisp son los dos lenguajes de programación más
antiguos que siguen vivos y utilizan, respectivamente, tipos estáticos y dinámicos.

La escritura fuerte ayuda a detectar errores temprano.

Estos son algunos ejemplos de por qué la escritura débil es mala:21

// esto es JavaScript (probado con Node.js v0.10.33)


''
== '0' // falso //
0 == '' verdadero //
0 == '0' verdadero //
'' < 0
falso //
'' < '0'
verdadero
Python no realiza coerción automática entre cadenas y números, por lo que todas las expresiones == dan como resultado False

(preservando la transitividad de ==) y las comparaciones < generan TypeError en Python 3.

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.

344 | Capítulo 11: Interfaces: de protocolos a ABC


Machine Translated by Google

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.

Interfaces en Java, Go y Ruby

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

Lectura adicional | 345


Machine Translated by Google

NotImplementedError en el cuerpo de los métodos para hacerlos abstractos obligando al


usuario a subclasificarlos e implementarlos.

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.

Tal vez los gansos estén a punto de alcanzar a los patos.

Metáforas y expresiones idiomáticas en las

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

La adherencia estricta a las metáforas vincula las interfaces innecesariamente con el


funcionamiento del mundo físico.
Se refiere a las interfaces de usuario, pero la advertencia también se aplica a las API. Pero Cooper reconoce
que cuando una metáfora "verdaderamente apropiada" "cae en nuestro regazo", podemos usarla (él escribe
"cae en nuestro regazo" porque es tan difícil encontrar metáforas adecuadas que no debe dedicar tiempo a
buscarlas activamente). ). Creo que la imagen de la máquina de bingo que usé en este capítulo es apropiada
y la mantengo.

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.

346 | Capítulo 11: Interfaces: de protocolos a ABC


Machine Translated by Google

CAPÍTULO 12

Herencia: para bien o para mal

[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:

• Los peligros de crear subclases a partir de tipos

integrados • La herencia múltiple y el orden de resolución del método

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

Subclasificar tipos integrados es complicado


Antes de Python 2.2, no era posible subclasificar tipos integrados como list o dict.
Desde entonces, se puede hacer, pero hay una advertencia importante: el código de los elementos integrados
(escritos en C) no llama a métodos especiales anulados por clases definidas por el usuario.

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:

Oficialmente, CPython no tiene ninguna regla sobre cuándo se llama implícitamente o no el


método anulado exactamente de las subclases de tipos integrados. Como aproximación, estos
métodos nunca son llamados por otros métodos integrados del mismo objeto. Por ejemplo, un
__getitem__() anulado en una subclase de dict no será llamado, por ejemplo, por el método
incorporado get() .

El ejemplo 12-1 ilustra el problema.

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

348 | Capítulo 12: Herencia: para bien o para mal


Machine Translated by Google

asuntos, el método __missing__ , que vimos en “El método __missing__” en la página


72: funciona como se documenta solo porque se maneja como un caso especial.

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.

Ejemplo 12-2. dict.update omite el __getitem__ de AnswerDict

>>> clase AnswerDict(dict):


... def __getitem__(uno mismo, clave): #
... volver 42
...
>>> anuncio = AnswerDict(a='foo') #
>>> anuncio['a'] # 42

>>> re = {}
>>> d.actualizar(anuncio)
# >>> d['a'] # 'foo'

>>> re
{'a': 'foo'}

AnswerDict.__getitem__ siempre devuelve 42, sin importar la clave.

ad es un AnswerDict cargado con el par clave-valor ('a', 'foo').


ad['a'] devuelve 42, como se esperaba.

d es una instancia de dictado simple, que actualizamos con ad.

El método dict.update ignoró nuestro AnswerDict.__getitem__.

La subclasificación de tipos incorporados como dict o list o str directamente es propensa a


errores porque los métodos incorporados en su mayoría ignoran los definidos por el usuario.
anula En lugar de subclasificar los incorporados, derive sus clases
desde el módulo de colecciones usando UserDict, UserList y
UserString, que están diseñados para extenderse fácilmente.

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.

Ejemplo 12-3. DoppelDict2 y AnswerDict2 funcionan como se esperaba porque amplían


UserDict y no dictar

>>> importar colecciones


>>>
>>> clase DoppelDict2(colecciones.UserDict):
... def __setitem__(auto, clave, valor):

Crear subclases de tipos integrados es complicado | 349


Machine Translated by Google

... super().__setitem__(clave, [valor] * 2)


...
>>> dd = DoppelDict2(uno=1)
>>> dd {'uno': [1, 1]} >>> dd['dos']
= 2 >>> dd {'dos': [2, 2 ], 'uno':
[1, 1]} >>> dd.update(tres=3)
>>> dd {'dos': [2, 2], 'tres': [3, 3],
'uno ': [1, 1]}

>>>
>>> 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.

350 | Capítulo 12: Herencia: para bien o para mal


Machine Translated by Google

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.

Orden de Resolución de Método y Herencia Múltiple


Cualquier lenguaje que implemente la herencia múltiple debe lidiar con posibles conflictos de nombres
cuando las clases de ancestros no relacionados implementan un método con el mismo nombre. Esto se
llama el "problema del diamante" y se ilustra en la figura 12-1 y el ejemplo 12-4.

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.

Ejemplo 12-4. diamond.py: las clases A, B, C y D forman el gráfico de la figura 12-1


clase A:
def ping(auto):
print('ping:', auto)

clase B(A):
def pong(self):
print('pong:', self)

clase C(A):
def pong(auto):

Orden de Resolución de Herencia Múltiple y Método | 351


Machine Translated by Google

imprimir('PONG:', uno mismo)

clase D (B, C):

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

>>> from importación de


diamantes * >>> d = D() >>>
d.pong() # pong: <diamante.D
objeto en 0x10066c278> >>> C.pong(d) #
PONG: <diamante.D objeto en 0x10066c278>

Simplemente llamar a d.pong() hace que se ejecute la versión B.

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

La forma recomendada de delegar llamadas a métodos a superclases es la función integrada super() ,


que se volvió más fácil de usar en Python 3, como método ping -pong de la clase D en

352 | Capítulo 12: Herencia: para bien o para mal


Machine Translated by Google

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)

>>> from diamond import D


>>> d = D() >>> d.ping() #
ping: <diamond.D object at
0x10cc40630> # post-ping: <diamond.D object at
0x10cc40630> #

El ping de D hace dos llamadas.

La primera llamada es super().ping(); el super delega la llamada de ping a la clase A; A.ping


genera esta línea.

La segunda llamada es print('post-ping:', self), que genera esta línea.

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)

>>> desde la importación de


diamantes D >>> d = D() >>>
d.pingpong() >>> d.pingpong()
ping: <diamante.D objeto en
0x10bf235c0> # post-ping: <diamante.D objeto en
0x10bf235c0> ping: <objeto diamante.D en
0x10bf235c0> # pong: <objeto diamante.D en
0x10bf235c0> # pong: <objeto diamante.D en
0x10bf235c0> # PONG: <objeto diamante.D en
0x10bf235c0> #

4. En Python 2, la primera línea de D.pingpong se escribiría como super(D, self).ping() en lugar de


super().ping()

Orden de Resolución de Herencia Múltiple y Método | 353


Machine Translated by Google

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.

La llamada #3 es self.pong(), que encuentra la implementación B de pong, de acuerdo con


__mro__.

La llamada #4 es super.pong(), que encuentra la misma implementación de B.pong , también


siguiendo el __mro__.

La llamada #5 es C.pong(self), que encuentra la implementación de C.pong , ignorando el


__mro__.

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.

Ejemplo 12-8. Inspeccionando el atributo __mro__ en varias clases


>>> bool.__mro__
(<clase 'bool'>, <clase 'int'>, <clase 'objeto'>) >>> def
print_mro(cls):
... print(', '.join(c.__name__ para c en cls.__mro__))
...
>>> print_mro(bool)
bool, int, object >>>
from frenchdeck2 import FrenchDeck2 >>>
print_mro(FrenchDeck2)
FrenchDeck2, MutableSequence, Sequence, Sized, Iterable, Container, object >>> importar
números >>> print_mro(numbers.Integral)

Integral, Racional, Real, Complejo, Número, objeto >>>


import io >>> print_mro(io.BytesIO)

BytesIO, _BufferedIOBase, _IOBase, objeto >>>


print_mro(io.TextIOWrapper)
TextIOWrapper, _TextIOBase, _IOBase, objeto

bool hereda métodos y atributos de int y object.


print_mro produce visualizaciones más compactas del MRO.
Los antepasados de FrenchDeck2 incluyen varios ABC de la colección.
módulo tions.abc .

354 | Capítulo 12: Herencia: para bien o para mal


Machine Translated by Google

Estos son los ABC numéricos proporcionados por el módulo de números .


El módulo io incluye ABC (aquellos con el sufijo …Base ) y clases concretas como BytesIO y
TextIOWrapper, que son los tipos de objetos de archivo de texto y binarios devueltos por
open(), según el argumento de modo.

El MRO se calcula usando un algoritmo llamado C3. El artículo


canónico sobre Python MRO que explica C3 es “The Python 2.3
Method Resolution Order” de Michele Simionato. Si está interesado
en las sutilezas del MRO, "Lectura adicional" en la página 367
tiene otros consejos. Pero no se preocupe demasiado por esto, el
algoritmo es sensato; como escribe Simionato:
[…] a menos que haga un uso intensivo de la herencia
múltiple y tenga jerarquías no triviales, no es necesario
que comprenda el algoritmo C3, y puede omitir este
artículo fácilmente.

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:

>>> importar tkinter


>>> print_mro(tkinter.Texto)
Texto, Widget, BaseWidget, Varios, Paquete, Lugar, Cuadrícula, XView, YView, objeto

Orden de Resolución de Herencia Múltiple y Método | 355


Machine Translated by Google

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.

Herencia múltiple en el mundo real


Es posible hacer un buen uso de la herencia múltiple. El patrón Adaptador en el libro Patrones de
diseño usa herencia múltiple, por lo que no puede ser completamente incorrecto hacerlo (los 22
patrones restantes en el libro usan solo herencia única, por lo que la herencia múltiple claramente no
es una panacea).

En la biblioteca estándar de Python, el uso más visible de la herencia múltiple es el paquete


col·lections.abc . Eso no es controvertido: después de todo, incluso Java admite la herencia múltiple de
interfaces, y los ABC son declaraciones de interfaz que, opcionalmente, pueden proporcionar
implementaciones de métodos concretos.5

Un ejemplo extremo de herencia múltiple en la biblioteca estándar es el kit de herramientas GUI de


Tkinter (módulo tkinter: interfaz de Python para Tcl/Tk). Usé parte de la jerarquía de widgets de Tkinter
para ilustrar el MRO en la Figura 12-2, pero la Figura 12-3 muestra todo el widget

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.

356 | Capítulo 12: Herencia: para bien o para mal


Machine Translated by Google

clases en el paquete base tkinter (hay más widgets en el subpaquete tkinter.ttk ).

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.

Considere estas clases de la Figura 12-3: ÿ

Toplevel: La clase de una ventana de nivel superior en una aplicación Tkinter.

ÿ Widget: La superclase de cada objeto visible que se puede colocar en una ventana.

ÿ Botón: un widget de botón simple. ÿ

Entrada: un campo de texto editable de una sola

línea. ÿ Texto: un campo de texto editable de varias líneas.

Herencia múltiple en el mundo real | 357


Machine Translated by Google

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.

• Subclases de entrada Widget y XView, la clase que implementa el desplazamiento horizontal.

• Subclases de texto de Widget, XView e YView, que proporciona desplazamiento vertical


funcionalidad.

Ahora discutiremos algunas buenas prácticas de herencia múltiple y veremos si Tkinter las acepta.

Hacer frente a la herencia múltiple


[…] necesitábamos una teoría mejor sobre la herencia por completo (y todavía la necesitamos). Por
ejemplo, la herencia y la creación de instancias (que es un tipo de herencia) confunden tanto la
pragmática (como la factorización de código para ahorrar espacio) como la semántica (utilizada para
demasiadas tareas como: especialización, generalización, especiación, etc.).

— Alan Kay
La historia temprana de Smalltalk

358 | Capítulo 12: Herencia: para bien o para mal


Machine Translated by Google

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.

1. Distinguir la herencia de interfaz de la herencia de


implementación
Cuando se trata de herencia múltiple, es útil mantener claras las razones por las que se realizan las
subclases en primer lugar. Las razones principales son:

• La herencia de la interfaz crea un subtipo, lo que implica una relación "es-un". • La

herencia de implementación evita la duplicación de código mediante la reutilización.

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.

2. Hacer que las interfaces sean explícitas con

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

3. Use Mixins para la reutilización de código

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.

4. Hacer que los Mixins sean explícitos

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.

Hacer frente a la herencia múltiple | 359


Machine Translated by Google

5. Un ABC también puede ser un Mixin; Lo contrario no es cierto

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.

6. No subclase de más de una clase concreta


Las clases concretas deben tener cero o como máximo una superclase concreta.6 En otras
palabras, todas menos una de las superclases de una clase concreta deben ser ABC o mixins.
Por ejemplo, en el siguiente código, si Alpha es una clase concreta, entonces Beta y Gamma
deben ser ABC o mixins:

class MyConcreteClass(Alpha, Beta, Gamma):


"""Esta es una clase concreta: puede ser instanciada.""" ... más
# código ...

7. Proporcione clases agregadas a los usuarios

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

Por ejemplo, aquí está el código fuente completo de tkinter.Widget:

class Widget(BaseWidget, Pack, Place, Grid): """Clase


interna.

Clase base para un widget que se puede posicionar con los


administradores de geometría Pack, Place o Grid."""
pasar

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.

360 | Capítulo 12: Herencia: para bien o para mal


Machine Translated by Google

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.

8. "Favorecer la composición de objetos sobre la herencia de clases".

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.

Ahora analizaremos Tkinter desde el punto de vista de estas recomendaciones.

Tkinter: lo bueno, lo malo y lo feo


Tenga en cuenta que Tkinter ha sido parte de la biblioteca estándar desde
que se lanzó Python 1.1 en 1994. Tkinter es una capa sobre el excelente
conjunto de herramientas Tk GUI del lenguaje Tcl. El combo Tcl/Tk no está
originalmente orientado a objetos, por lo que la API de Tk es básicamente un
amplio catálogo de funciones. Sin embargo, el conjunto de herramientas está
muy orientado a objetos en sus conceptos, si no en su implementación.

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

Hacer frente a la herencia múltiple | 361


Machine Translated by Google

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.

Un ejemplo moderno: mixins en vistas genéricas de Django

No necesitas saber Django para seguir esta sección. Solo estoy


usando una pequeña parte del marco como un ejemplo práctico
de herencia múltiple, e intentaré brindar todos los antecedentes
necesarios, suponiendo que tenga alguna experiencia con el
desarrollo web del lado del servidor en otro lenguaje o marco.

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.

362 | Capítulo 12: Herencia: para bien o para mal


Machine Translated by Google

renderizado a partir de una plantilla HTML y enumerando múltiples productos con botones para comprar y
enlaces a páginas de detalles.

Originalmente, Django proporcionaba un conjunto de funciones, llamadas vistas genéricas, que


implementaban algunos casos de uso comunes. Por ejemplo, muchos sitios necesitan mostrar resultados
de búsqueda que incluyan información de numerosos elementos, con la lista que abarca varias páginas, y
para cada elemento un enlace a una página con información detallada al respecto. En Django, una vista de
lista y una vista de detalles están diseñadas para trabajar juntas para resolver este problema: una vista de
lista muestra los resultados de la búsqueda y una vista de detalles produce páginas para elementos individuales.

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.

Un gran recurso para estudiar estas clases es el sitio web Classy


Class-Based Views , donde puede navegar fácilmente a través de
ellas, ver todos los métodos en cada clase (métodos heredados,
anulados y agregados), ver diagramas, explorar su documentación
y salta a su código fuente en GitHub.

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í.

Un ejemplo moderno: mixins en vistas genéricas de Django | 363


Machine Translated by Google

El método View.dispatch verifica que no haya un controlador de publicación y genera una


respuesta HTTP 405 Method Not Allowed.10

Figura 12-4. Diagrama de clases UML para el módulo django.views.generic.base

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.

364 | Capítulo 12: Herencia: para bien o para mal


Machine Translated by Google

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

Un ejemplo moderno: mixins en vistas genéricas de Django | 365


Machine Translated by Google

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.

Resumen del capítulo


Comenzamos nuestra cobertura de la herencia explicando el problema con la subclasificación de tipos
incorporados: sus métodos nativos implementados en C no llaman a métodos anulados en subclases,
excepto en muy pocos casos especiales. Es por eso que, cuando necesitamos una lista personalizada, un
dictado o un tipo de cadena, es más fácil subclasificar UserList, UserDict o UserString , todo definido en el
módulo de colecciones, que en realidad envuelve los tipos integrados y delega operaciones en ellos. tres
ejemplos de favorecer la composición sobre la herencia en la biblioteca estándar. Si el comportamiento
deseado es muy diferente de lo que ofrecen los integrados, puede ser más fácil subclasificar el ABC
apropiado de collections.abc y escribir su propia implementación.

El resto del capítulo se dedicó a la espada de doble filo de la herencia múltiple.


Primero vimos cómo el orden de resolución del método, codificado en el atributo de clase __mro__ , aborda
el problema de los posibles conflictos de nombres en los métodos heredados. También vimos cómo el
super() incorporado sigue al __mro__ para llamar a un método en una superclase. Luego estudiamos cómo
se usa la herencia múltiple en el kit de herramientas GUI de Tkinter que viene con la biblioteca estándar de
Python. Tkinter no es un ejemplo de las mejores prácticas actuales, por lo que discutimos algunas formas
de lidiar con la herencia múltiple, incluido el uso cuidadoso de clases mixtas y evitar la herencia múltiple por
completo mediante el uso de la composición en su lugar. Después de considerar cómo se abusa de la
herencia múltiple en Tkinter, terminamos estudiando las partes centrales de la jerarquía de vistas basadas
en clases de Django, que considero un mejor ejemplo del uso de mixin.

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.

366 | Capítulo 12: Herencia: para bien o para mal


Machine Translated by Google

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.

La publicación de Raymond Hettinger Python's super() ¡considerada super! explica el funcionamiento


de la superherencia y la herencia múltiple en Python desde una perspectiva positiva. Fue escrito en
respuesta a Python's Super is nifty, but you can't use it (también conocido como Python's Super
Considered Harmful) por James Knight.

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

Piense en las clases que realmente necesita

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

Lectura adicional | 367


Machine Translated by Google

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:

• Estás reinventando la rueda. Vaya a buscar un marco o biblioteca que proporcione


componentes que puede reutilizar en su aplicación. • Está

utilizando un marco mal diseñado. Ve a buscar una alternativa. • Está haciendo un exceso

de ingeniería. Recuerda el principio KISS. • Te aburriste de codificar aplicaciones y

decidiste comenzar un nuevo marco.


¡Felicitaciones y buena suerte!

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.

Componentes incorporados que se comportan mal: ¿Error o característica?

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.

Herencia entre idiomas

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

368 | Capítulo 12: Herencia: para bien o para mal


Machine Translated by Google

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.

Lectura adicional | 369


Machine Translated by Google
Machine Translated by Google

CAPÍTULO 13

Sobrecarga del operador: hacerlo bien

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.

En “Emulación de tipos numéricos” en la página 9 (Capítulo 1) vimos algunas implementaciones triviales


de operadores en una clase Vector básica. Los métodos __add__ y __mul__ en el Ejemplo 1-2 se
escribieron para mostrar cómo los métodos especiales soportan la sobrecarga de operadores, pero hay
problemas sutiles en sus implementaciones que pasamos por alto. Además, en el Ejemplo 9-2, notamos
que el método Vector2d.__eq__ considera que esto es verdadero: Vector(3, 4) == [3, 4], lo que puede o
no tener sentido. Abordaremos esos asuntos en este capítulo.

En las siguientes secciones, cubriremos:

• 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

• El manejo predeterminado de los operadores de asignación aumentada, como +=, y cómo


cargarlos

Sobrecarga del operador 101


La sobrecarga de operadores tiene mala fama en algunos círculos. Es una característica del lenguaje
de la que se puede abusar (y se ha abusado), lo que genera confusión para el programador, errores
y cuellos de botella inesperados en el rendimiento. Pero si se usa bien, conduce a API placenteras y
código legible. Python logra un buen equilibrio entre flexibilidad, facilidad de uso y seguridad al
imponer algunas limitaciones:

• No podemos sobrecargar operadores para los tipos integrados.

• No podemos crear nuevos operadores, solo sobrecargar los existentes. •

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 *.

Comencemos con el tema más fácil: operadores unarios.

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

Negación unaria aritmética. Si x es -2 entonces -x == 2.

+ (__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

372 | Capítulo 13: Sobrecarga de operadores: hacerlo bien


Machine Translated by Google

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.

Ejemplo 13-1. vector_v6.py: operadores unarios - y + agregados al Ejemplo 10-16

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.

Cuando x y +x no son iguales


Todo el mundo espera que x == +x, y eso es cierto casi todo el tiempo en Python, pero encontré
dos casos en la biblioteca estándar donde x != +x.

El primer caso implica la clase decimal.Decimal . Puedes tener x != +x si x es un Deci


instancia mal creada en un contexto aritmético y +x luego se evalúa en un contexto con diferentes
configuraciones. Por ejemplo, x se calcula en un contexto con cierta precisión, pero

Operadores Unarios | 373


Machine Translated by Google

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

Obtenga una referencia al contexto aritmético global actual.

Establezca la precisión del contexto aritmético en 40.

Calcule 1/3 usando la precisión actual.

Inspeccione el resultado; hay 40 dígitos después del punto decimal.

un_tercio == +un_tercio es Verdadero.

Baje la precisión a 28, el valor predeterminado para la aritmética decimal en Python 3.4.

Ahora un tercio == +un tercio es falso.

Inspeccionar +un_tercio; hay 28 dígitos después del '.' aquí.

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.

El segundo caso donde x != +x lo puedes encontrar en las colecciones . Contra documentación. La


clase Counter implementa varios operadores aritméticos, incluido el infijo + para sumar las cuentas de
dos instancias de Counter . Sin embargo, por razones prácticas, la adición de contador descarta del
resultado cualquier elemento con un recuento negativo o cero. Y el prefijo + es un atajo para agregar
un Contador vacío , por lo tanto, produce un nuevo Contador conservando solo las cuentas que son
mayores que cero. Vea el Ejemplo 13-3.

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

374 | Capítulo 13: Sobrecarga de operadores: hacerlo bien


Machine Translated by Google

>>> ct
Contador ({'a': 5, 'b': 2, 'c': 1, 'd': 0, 'r': -3})
>>> +ct
Contador ({'a': 5, 'b': 2, 'c': 1})

Ahora, de vuelta a nuestra programación regular.

Sobrecarga + para suma de vectores

La clase Vector es de tipo secuencia, y el apartado “3.3.6. Emulación de


tipos de contenedores” en el capítulo “Modelo de datos” dice que las
secuencias deben admitir el operador + para la concatenación y * para
la repetición. Sin embargo, aquí implementaremos + y * comooperaciones
vectoriales matemáticas, que son un poco más difíciles pero más
significativas para un tipo Vector .

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:

>>> v1 = Vector([3, 4, 5]) >>>


v2 = Vector([6, 7, 8]) >>> v1 +
v2 Vector([9.0, 11.0, 13.0]) >>>
v1 + v2 == Vector([3+6, 4+7,
5+8])
Verdadero

¿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:

>>> v1 = Vector([3, 4, 5, 6]) >>> v3


= Vector([1, 2]) >>> v1 + v3

Vector([4.0, 6.0, 5.0, 6.0])

Dados estos requisitos básicos, la implementación de __add__ es breve y agradable, como se muestra
en el Ejemplo 13-4.

Ejemplo 13-4. Método Vector.add, toma #1


# dentro de la clase Vector

def __add__(self, other): pairs


= itertools.zip_longest(self, other, fillvalue=0.0) # return Vector(a + b for a,
b in pairs) #

Sobrecarga + para adición de vectores | 375


Machine Translated by Google

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.

Los métodos especiales que implementan operadores unarios o infijos nunca


deben cambiar sus operandos. Se espera que las expresiones con dichos
operadores produzcan resultados mediante la creación de nuevos objetos.
Solo los operadores de asignación aumentada pueden cambiar el primer
operando (self), como se explica en "Operadores de asignación aumentada" en la página 388.

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 .

Ejemplo 13-5. Vector.__add__ take #1 también admite objetos no vectoriales


>>> v1 = Vector([3, 4, 5]) >>>
v1 + (10, 20, 30)
Vector([13.0, 24.0, 35.0]) >>>
from vector2d_v3 import Vector2d >>>
v2d = Vector2d(1, 2) >>> v1 + v2d
Vector([4.0, 6.0, 5.0])

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.

Ejemplo 13-6. Vector.__add__ toma #1 falla con operandos izquierdos no vectoriales


>>> v1 = Vector([3, 4, 5]) >>>
(10, 20, 30) + v1
Rastreo (llamadas recientes más última):
Archivo "<stdin>", línea 1, en <módulo>
TypeError: solo puede concatenar tupla (no "Vector") a tupla >>> from
vector2d_v3 import Vector2d >>> v2d = Vector2d(1, 2) >>> v2d + v1

Rastreo (llamadas recientes más última):


Archivo "<stdin>", línea 1, en <módulo>
TypeError: tipos de operandos no admitidos para +: 'Vector2d' y 'Vector'

376 | Capítulo 13: Sobrecarga de operadores: hacerlo bien


Machine Translated by Google

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.

2. Si a no tiene __add__, o al llamarlo devuelve No implementado, verifique si b tiene __radd__, luego


llame a b.__radd__(a) y devuelva el resultado a menos que no esté implementado.

3. Si b no tiene __radd__, o al llamarlo devuelve NotImplemented, genere TypeError


con un mensaje de tipos de operandos no admitidos .

Figura 13-1. Diagrama de flujo para calcular a + b con __add__ y __radd__

Sobrecarga + para adición de vectores | 377


Machine Translated by Google

El método __radd__ se llama la versión "reflejada" o "invertida" de __add__. Prefiero llamarlos


métodos especiales "invertidos".2 Tres de los revisores técnicos de este libro, Alex, Anna y Leo,
me dijeron que les gusta pensar en ellos como los métodos especiales "correctos", porque se
llaman en el operando de la derecha. . Cualquiera que sea la palabra "r" que prefiera, eso es lo
que significa el prefijo "r" en __radd__, __rsub__ y similares.

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 confunda NotImplemented con NotImplementedError. El primero,


NotImplemented, es un valor singleton especial que un método especial
de operador infijo debe devolver para decirle al intérprete que no puede
manejar un operando dado. Por el contrario, NotImplementedEr ror es
una excepción que los métodos stub en clases abstractas generan
para advertir que deben ser sobrescritos por subclases.

El __radd__ más simple posible que funciona se muestra en el ejemplo 13-7.

Ejemplo 13-7. Métodos Vector.__add__ y __radd__


# dentro de la clase Vector

def __add__(uno mismo, otro): #


pares = itertools.zip_longest(self, other, fillvalue=0.0) return Vector(a + b para a, b
en pares)

def __radd__(uno mismo, otro): #


volver yo + otro

No hay cambios en __add__ del Ejemplo 13-4; enumerados aquí porque __radd__ usa
eso.

__radd__ solo delega a __add__.

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.

378 | Capítulo 13: Sobrecarga de operadores: hacerlo bien


Machine Translated by Google

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.

Ejemplo 13-8. El método Vector.__add__ necesita un operando iterable


>>> v1 + 1
Rastreo (llamadas recientes más última):
Archivo "<stdin>", línea 1, en <módulo> Archivo
"vector_v6.py", línea 328, en __add__ pares =
itertools.zip_longest(self, other, fillvalue=0.0)
TypeError: el argumento zip_longest n.º 2 debe admitir la iteración

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.

Ejemplo 13-9. El método Vector.__add__ necesita un iterable con elementos numéricos


>>> v1 + 'ABC'
Rastreo (llamadas recientes más última):
Archivo "<stdin>", línea 1, en <módulo> Archivo
"vector_v6.py", línea 329, en __add__ devuelve
Vector(a + b para a, b en pares)
Archivo "vector_v6.py", línea 243, en __init__
self._components = array(self.typecode, componentes)
Archivo "vector_v6.py", línea 329, en <genexpr> devuelve
Vector(a + b para a, b en pares)
TypeError: tipos de operandos no admitidos para +: 'float' y 'str'

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".

La implementación final de los métodos especiales para la suma de vectores se encuentra en el


ejemplo 13-10.

Ejemplo 13-10. vector_v6.py: operador + métodos agregados a vector_v5.py (Ejemplo 10-16)

def __add__(uno mismo, otro):


prueba:

Sobrecarga + para adición de vectores | 379


Machine Translated by Google

pares = itertools.zip_longest(self, other, fillvalue=0.0) return Vector(a + b


for a, b in pairs) excepto TypeError: return NotImplemented

def __radd__(uno mismo, otro):


return uno mismo + otro

Si un método de operador infijo genera una excepción, aborta el


algoritmo de envío del operador. En el caso particular de TypeError,
a menudo es mejor detectarlo y devolver NotImplemented. Esto
permite que el intérprete intente llamar al método del operador
invertido, que puede manejar correctamente el cálculo con los
operandos intercambiados, si son de diferentes tipos.

En este punto, hemos sobrecargado de forma segura el operador + al escribir __add__ y __radd__. Ahora
abordaremos otro operador infijo: *.

Sobrecarga * para multiplicación escalar


¿Qué significa Vector([1, 2, 3]) * x ? Si x es un número, sería un producto escalar y el resultado sería un
nuevo Vector con cada componente multiplicado por x, también conocida como multiplicación por elementos:

>>> v1 = Vector([1, 2, 3]) >>> v1


* 10
Vector([10.0, 20.0, 30.0])
>>> 11 * v1
Vector([11.0, 22.0, 33.0])

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:

# dentro de la clase Vector

def __mul__(self, escalar): return


Vector(n * escalar for n in self)

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.

380 | Capítulo 13: Sobrecarga de operadores: hacerlo bien


Machine Translated by Google

def __rmul__(self, escalar): return


self * escalar

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.

Como recordará de “ABC en la biblioteca estándar” en la página 321,


decimal.Decimal no está registrado como una subclase virtual de
números.Real. Por lo tanto, nuestra clase Vector no manejará números
decimales. Decimales .

Ejemplo 13-11. vector_v7.py: operadores * métodos agregados

from matriz importar matriz


importar reprlib importar
matemáticas importar
funciones importar operador
importar itertools importar
números #

clase vectorial:
código de tipo = 'd'

def __init__(uno mismo, componentes):


self._components = array(self.typecode, componentes)

# muchos métodos omitidos en la lista de libros, consulte vector_v7.py # en


https:// github.com/ fluentpython/ example-code ...

def __mul__(self, escalar): if


isinstance(scalar, numeros.Real): #
return Vector(n * escalar para n en sí mismo) else:
# return NotImplemented

Sobrecarga * para multiplicación escalar | 381


Machine Translated by Google

def __rmul__(uno mismo, escalar):


volver self * escalar #

Importe el módulo de números para la verificación de tipos.

Si escalar es una instancia de una subclase de números. Real, cree un nuevo Vector con
valores de los componentes multiplicados.

De lo contrario, genera TypeError con un mensaje explícito.

En este ejemplo, __rmul__ funciona bien simplemente realizando self * scalar,


delegar al método __mul__ .

Con el Ejemplo 13-11, podemos multiplicar Vectores por valores escalares de los habituales y no tan
tipos numéricos habituales:

>>> v1 = Vector([1.0, 2.0, 3.0])


>>> 14 * v1
Vector([14.0, 28.0, 42.0])
>>> v1 * Verdadero
Vector([1.0, 2.0, 3.0])
>>> desde fracciones import Fracción
>>> v1 * Fracción(1, 3)
Vector([0.3333333333333333, 0.6666666666666666, 1.0])

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)

Operador Delantero Reverso En su lugar Descripción

+ __agregar__ __radd__ __añado__ Adición o concatenación

- __sub__ __rsub__ __isub__ Sustracción

*
__mul__ __rmul__ __imul__ Multiplicación o repetición

División verdadera
/ __truediv__ __rtruediv__ __itruediv__

// __pisodiv__ __rfloordiv__ __ifloordiv__ División del piso


Módulo
% __modificación__ __rmod__ __imod__

divmod() __divmod__ __rdivmod__ __idivmod__ Devuelve tupla de piso

cociente de división y
módulo

**, poder() __pow__ __rpow__ __ipow__ Exponenciacióna

@ __matmul__ __rmatmul__ __imatmul__ Multiplicación de matricesb

& __y__ __rand__ __yo y__ bit a bit y

bit a bit o
| __o__ __ror__ __ior__

382 | Capítulo 13: Sobrecarga de operadores: hacerlo bien


Machine Translated by Google

Operador Delantero Reverso En su lugar Descripción

^ bit a bit xor


__xor__ __rxor__ __ixor__
Desplazamiento bit a bit a la izquierda
<< __cambio__ __rlshift__ __ilshift__
>> __rshift__ __rrshift__ __irshift__
Desplazamiento bit a bit a la derecha

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

b Nuevo en Python 3.5.

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” .

adores” en la página 384.

La siguiente barra lateral opcional trata sobre el operador @ introducido en Python 3.5, no

todavía publicado en el momento de escribir este artículo.

El nuevo operador @ Infix en Python 3.5


Python 3.4 no tiene un operador infijo para el producto punto. Sin embargo, mientras escribo esto,
Python 3.5 pre-alfa ya implementa PEP 465: un operador infijo dedicado para
multiplicación de matrices, haciendo que el signo @ esté disponible para ese propósito (por ejemplo, a @ b es el
producto escalar de a y b). El operador @ es compatible con los métodos especiales __mat
mul__, __rmatmul__ y __imatmul__, llamados así por “multiplicación de matrices”. Estos métodos
ods no se utilizan en ninguna parte de la biblioteca estándar en este momento, pero son reconocidos por el
intérprete en Python 3.5 para que el equipo de NumPy, y el resto de nosotros, podamos admitir @
operador en tipos definidos por el usuario. El analizador también se cambió para manejar el infijo @ (a @
b es un error de sintaxis en Python 3.4).

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 .

Estas son las pruebas simples que hice:

>>> va = Vector([1, 2, 3])


>>> vz = Vector([5, 6, 7])
>>> va @ vz == 38.0 # 1*5 + 2*6 + 3*7
Verdadero

>>> [10, 20, 30 ] @vz


380.0
>>> van @ 3
Rastreo (llamadas recientes más última):
...
TypeError: tipos de operandos no admitidos para @: 'Vector' e 'int'
Y aquí está el código de los métodos especiales relevantes:

clase vectorial:
# muchos métodos omitidos en la lista de libros

Sobrecarga * para multiplicación escalar | 383


Machine Translated by Google

def __matmul__(uno mismo, otro):


probar:

return sum(a * b for a, b in zip(self, other))


excepto TypeError:
volver No implementado

def __rmatmul__(uno mismo, otro):


volver yo @ otro

La fuente completa está en el archivo vector_py3_5.py en el repositorio de código de Fluent Python .

¡Recuerde probarlo con Python 3.5, de lo contrario obtendrá un SyntaxError!

Operadores de comparación enriquecidos

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.

• En el caso de == y !=, si falla la llamada inversa, Python compara los ID de objeto


en lugar de generar TypeError.

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

Igualdad a == b a.__eq__(b) b.__eq__(a) Devolver id(a) == id(b)


un != segundo a.__ne__(b) b.__ne__(a) No devuelvas (a == b)

Ordenar a > b a.__gt__(b) b.__lt__(a) Levantar error de tipo

un < segundo a.__lt__(b) b.__gt__(a) Levantar error de tipo

un > = segundo a.__ge__(b) b.__le__(a) Levantar error de tipo

un <= segundo a.__le__(b) b.__ge__(a) Levantar error de tipo

384 | Capítulo 13: Sobrecarga de operadores: hacerlo bien


Machine Translated by Google

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

def __eq__(yo, otro): return


(len(yo) == len(otro) and all(a == b for a, b
in zip(yo, otro)))
Ese método produce los resultados del ejemplo 13-12.

Ejemplo 13-12. Comparación de un vector con un vector, un Vector2d y una tupla

>>> va = Vector([1.0, 2.0, 3.0]) >>> vb


= Vector(rango(1, 4)) >>> va == vb #
True >>> vc = Vector([1, 2] ) >>> from
vector2d_v3 import Vector2d >>> v2d
= Vector2d(1, 2) >>> vc == v2d # True
>>> t3 = (1, 2, 3) >>> va == t3 #

Verdadero

Dos instancias de Vector con componentes numéricos iguales se comparan iguales.

Un Vector y un Vector2d también son iguales si sus componentes son iguales.

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:

Frente a la ambigüedad, rechace la tentación de adivinar.

Operadores de comparación enriquecidos | 385


Machine Translated by Google

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.

Ejemplo 13-13. vector_v8.py: __eq__ mejorado en la clase Vector

def __eq__(self, otro): if


isinstance(other, Vector): return
(len(self) == len(other) and all(a == b for a,
b in zip(self, other)))
else:
devuelve No Implementado

Si el otro operando es una instancia de Vector (o de una subclase de Vector ), realice la comparación como
antes.

De lo contrario, devuelva No implementado.

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

>>> va = Vector([1.0, 2.0, 3.0]) >>> vb =


Vector(rango(1, 4)) >>> va == vb # True
>>> vc = Vector([1, 2] ) >>> from
vector2d_v3 import Vector2d >>> v2d
= Vector2d(1, 2) >>> vc == v2d # True
>>> t3 = (1, 2, 3) >>> va == t3 # False

Mismo resultado que antes, como se esperaba.

Mismo resultado que antes, pero ¿por qué? Explicación próximamente.

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:

386 | Capítulo 13: Sobrecarga de operadores: hacerlo bien


Machine Translated by Google

1. Para evaluar vc == v2d, Python llama a Vector.__eq__(vc, v2d).

2. Vector.__eq__(vc, v2d) verifica que v2d no es un Vector y devuelve NotImple


mentalizado

3. Python obtiene el resultado NotImplemented , por lo que prueba Vector2d.__eq__(v2d, vc).

4. Vector2d.__eq__(v2d, vc) convierte ambos operandos en tuplas y los compara: el resultado es


Verdadero (el código para Vector2d.__eq__ está en el ejemplo 9-9).

En cuanto a la comparación entre Vector y tupla en el Ejemplo 13-14, los pasos reales
son:

1. Para evaluar va == t3, Python llama a Vector.__eq__(va, t3).

2. Vector.__eq__(va, t3) verifica que t3 no es un Vector y devuelve NotImplemen


ted.
3. Python obtiene el resultado NotImplemented , por lo que intenta tuple.__eq__(t3, va).

4. tuple.__eq__(t3, va) no tiene idea de qué es un Vector , por lo que devuelve NotImplemented.

5. En el caso especial de ==, si la llamada invertida devuelve NotImplemented, Python comÿ


empareja los ID de objeto como último recurso.

¿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

def __ne__(uno mismo, otro):


eq_result = self == otro si
eq_result no está implementado:
devuelve No implementado

4. La lógica para object.__eq__ y object.__ne__ está en la función object_richcompare en Objects/typeobÿ


ject.c en el código fuente de CPython.

Operadores de comparación enriquecidos | 387


Machine Translated by Google

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.

Operadores de asignación aumentada


Nuestra clase Vector ya admite los operadores de asignación aumentada += y *=.
El ejemplo 13-15 los muestra en acción.

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 += Vector([4, 5, 6]) #


>>> v1 #
Vector([5.0, 7.0, 9.0]) >>>
id(v1) # 4302859904

>>> 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.

Realiza sumas aumentadas.

388 | Capítulo 13: Sobrecarga de operadores: hacerlo bien


Machine Translated by Google

El resultado esperado…
…pero se creó un nuevo Vector .

Inspeccione v1_alias para confirmar que el Vector original no se modificó.

Realiza multiplicaciones aumentadas.

Nuevamente, 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.

Los métodos especiales en el lugar nunca deben implementarse para tipos


inmutables como nuestra clase Vector . Esto es bastante obvio, pero vale la
pena decirlo de todos modos.

Para mostrar el código de un operador en el lugar, extenderemos la clase BingoCage del Ejemplo 11-12
para implementar __add__ y __iadd__.

Llamaremos a la subclase AddableBingoCage. El ejemplo 13-16 es el comportamiento que queremos


para el operador + .

Ejemplo 13-16. Se puede crear una nueva instancia de AddableBingoCage con

>>> vocales = 'AEIOU'


>>> globo = AddableBingoCage(vocales)
>>> globo.inspect()
('A', 'E', 'I', 'O', 'U') >>>
globo.pick() en vocales
Verdadero >>> len(globo.inspect())
4

>>> globo2 = AddableBingoCage('XYZ') >>>


globo3 = globo + globo2 >>>
len(globo3.inspect()) 7 >>> void = globo +
[10, 20]

Rastreo (última llamada más reciente):


...
TypeError: tipos de operandos no admitidos para +: 'AddableBingoCage' y 'list'

Operadores de asignación aumentada | 389


Machine Translated by Google

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.

Confirme que el globo se ha reducido a cuatro elementos.

Cree una segunda instancia, con tres elementos.

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)

>>> globo_orig = globo


>>> len(globo.inspeccionar())
4
>>> globo += globo2
>>> largo(globo.inspeccionar())
7 >>> globo += ['M', 'N'] >>>
largo(globo.inspeccionar()) 9

>>> globo es globo_orig

Verdadero >>> globo += 1


Rastreo (última llamada más reciente):
...
TypeError: el operando derecho en += debe ser 'AddableBingoCage' o iterable

Cree un alias para que podamos verificar la identidad del objeto más tarde. globo tiene

cuatro artículos aquí.

Una instancia de AddableBingoCage puede recibir elementos de otra instancia de la misma clase.

El operando derecho de += también puede ser iterable.

A lo largo de este ejemplo, globo siempre se ha referido al objeto globo_orig .

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

390 | Capítulo 13: Sobrecarga de operadores: hacerlo bien


Machine Translated by Google

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.

Validé el comportamiento contrastante de + y += observando cómo


el tipo incorporado de lista funciona. Escribiendo my_list + x, solo puedes
concatenar una lista a otra lista, pero si escribe my_list +=
x, puede ampliar la lista de la izquierda con elementos de cualquier iterable
x en el lado derecho. Esto es consistente con la forma en que list.ex
El método tend() funciona: acepta cualquier argumento iterable.

Ahora que tenemos claro el comportamiento deseado para AddableBingoCage, podemos ver su
implementación en el ejemplo 13-18.

Ejemplo 13-18. bingoaddable.py: AddableBingoCage amplía BingoCage para admitir +


y +=

importar itertools

de tómbola importar tómbola


de bingo import BingoCage

clase AddableBingoCage(BingoCage):

def __add__(uno mismo, otro):


si es instancia (otro, Tombola):
devuelve AddableBingoCage(self.inspect() + other.inspect()) else:

volver No implementado

def __iadd__(uno mismo, otro):


si es instancia (otro, Tombola):
otro_iterable = otro.inspeccionar ()
más:
probar:

other_iterable = iter(otro) excepto


TypeError: self_cls = type(self).__name__

msg = "el operando derecho en += debe ser {!r} o iterable"


aumentar TypeError(msg.format(self_cls))
self.load(other_iterable) return
self

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.

AddableBingoCage amplía BingoCage.


Nuestro __add__ solo funcionará con una instancia de Tombola como segundo operando.

Operadores de asignación aumentada | 391


Machine Translated by Google

Recupera elementos de otros, si es una instancia de Tombola.


5
De lo contrario, intente obtener un iterador sobre otro.

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.

Si llegamos tan lejos, podemos cargar el otro_iterable en uno mismo.

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.

En general, si un método de operador de infijo directo (p. ej.,


__mul__) está diseñado para funcionar solo con operandos del
mismo tipo que self, es inútil implementar el método inverso
correspondiente (p. ej., __rmul__) porque, por definición, solo será
invocado cuando se trata de un operando de un tipo diferente.

Esto concluye nuestra exploración de la sobrecarga de operadores en Python.

Resumen del capítulo


Comenzamos este capítulo revisando algunas restricciones que Python impone sobre la
sobrecarga de operadores: sin sobrecarga de operadores en tipos integrados y sobrecarga
limitada a operadores existentes, excepto por algunos (es, y, o no).

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.

392 | Capítulo 13: Sobrecarga de operadores: hacerlo bien


Machine Translated by Google

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.

Lectura adicional | 393


Machine Translated by Google

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.

La función functools.total_ordering es un decorador de clases (compatible con Python 2.7 y versiones


posteriores) que genera automáticamente métodos para todos los operadores de comparación enriquecidos
en cualquier clase que defina al menos un par de ellos. Consulte los documentos del módulo functools.

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++

394 | Capítulo 13: Sobrecarga de operadores: hacerlo bien


Machine Translated by Google

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

Sobrecarga de operadores: pros y contras

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:

Probablemente alrededor del 20 al 30 por ciento de la población piensa que la sobrecarga de


operadores es el engendro del diablo; alguien ha hecho algo con la sobrecarga de operadores que
realmente los ha enfadado, porque han usado como + para la inserción de listas y hace que la vida
sea muy, muy confusa. Gran parte de ese problema se debe al hecho de que solo hay alrededor
de media docena de operadores que puede sobrecargar con sensatez y, sin embargo, hay miles o
millones de operadores que a las personas les gustaría definir, por lo que debe elegir y, a menudo,
las opciones entran en conflicto. con tu sentido de la intuición.

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.

Gosling continúa diciendo:

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í:

interés = principal * ((1 + tasa) ** períodos - 1)

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:

Lectura adicional | 395


Machine Translated by Google

Interés BigDecimal = principal.multiply(BigDecimal.ONE.add(tasa)


.pow(puntos).subtract(BigDecimal.ONE));

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.

Un vistazo a la evaluación perezosa

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.

Ejemplo 13-19. Igual que el Ejemplo 13-9


>>> v1 + 'ABC'
Rastreo (llamadas recientes más última):
Archivo "<stdin>", línea 1, en <módulo>
Archivo "vector_v6.py", línea 329, en __add__
return Vector(a + b para a, b en pares) #
Archivo "vector_v6.py", línea 243, en __init__
self._components = array(self.typecode, components) # Archivo
"vector_v6.py", línea 329, en <genexpr> return Vector(a + b for a, b
in pares) #
TypeError: tipos de operandos no admitidos para +: 'float' y 'str'

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.

La excepción se propaga a la llamada del constructor Vector, donde se informa.

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.

396 | Capítulo 13: Sobrecarga de operadores: hacerlo bien


Machine Translated by Google

Por el contrario, si el constructor de Vector se invocara como Vector([a + b para a, b en pares]),


entonces la excepción ocurriría allí mismo, porque la lista de comprensión intentó construir una
lista para pasarla como argumento al Llamada a Vector() . El cuerpo de Vector.__init__ no se
alcanzaría en absoluto.

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.

Lectura adicional | 397


Machine Translated by Google
Machine Translated by Google

PARTE V

Flujo de control
Machine Translated by Google
Machine Translated by Google

CAPÍTULO 14

Iterables, iteradores y generadores

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.

Cada generador es un iterador: los generadores implementan completamente


la interfaz del iterador. Pero un iterador, como se define en el libro GoF,
recupera elementos de una colección, mientras que un generador puede
producir elementos "de la nada". Es por eso que el generador de secuencias
de Fibonacci es un ejemplo común: una serie infinita de números no se
puede almacenar en una colección. Sin embargo, tenga en cuenta que la
comunidad de Python trata al iterador y al generador como sinónimos la
mayor parte del tiempo.

1. De “La venganza de los nerds”, una publicación de blog.

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 •

Construcción y extensión de tipos de colección • Bucles

sobre archivos de texto línea por línea • Comprensiones

de listas, dictados y conjuntos • Desempaquetado de

tuplas • Desempaquetado de parámetros reales con * en

llamadas a funciones

Este capitulo cubre los siguientes topicos:

• 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

una función de generador o generador exÿ


presion

• Aprovechamiento de las funciones de generador de uso general en la biblioteca estándar • Uso

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.

Toma de oración #1: una secuencia de palabras


Comenzaremos nuestra exploración de iterables implementando una clase Sentence : le das a su constructor
una cadena con algo de texto y luego puedes iterar palabra por palabra. La primera versión implementará el
protocolo de secuencia y es iterable porque todas las secuencias son iterables, como hemos visto antes,
pero ahora veremos exactamente por qué.

El ejemplo 14-1 muestra una clase Oración que extrae palabras de un texto por índice.

402 | Capítulo 14: Iterables, iteradores y generadores


Machine Translated by Google

Ejemplo 14-1. sentencia.py: una oración como una secuencia de palabras

importar re
importar reprlib

RE_PALABRA = re.compilar('\w+')

oración de clase :

def __init__(uno mismo, texto):


self.texto = texto
self.palabras = RE_PALABRA.findall(texto)

def __getitem__(uno mismo, índice):


volver self.palabras[índice]

def __len__(auto):
return len(auto.palabras)

def __repr__(uno mismo):


devuelve 'Oración(%s)' % reprlib.repr(self.text)

re.findall devuelve una lista con todas las coincidencias no superpuestas del regular
expresión, como una lista de cadenas.

self.words contiene el resultado de .findall, por lo que simplemente devolvemos la palabra en el


índice dado.

Para completar el protocolo de secuencia, implementamos __len__, pero no es necesario


para hacer un objeto iterable.

reprlib.repr es una función de utilidad para generar representaciones de cadenas abreviadas


de estructuras de datos que pueden ser muy grandes.3

De forma predeterminada, reprlib.repr limita la cadena generada a 30 caracteres. ver la consola


session en el ejemplo 14-2 para ver cómo se usa Sentence .

Ejemplo 14-2. Prueba de iteración en una instancia de Sentence

>>> s = Oración('" Ha llegado el momento," dijo la Morsa,') #


>>> s
Oración('"El tiempo ha... Morsa dijo,') # >>> for palabra en
s: # print(palabra)
...
los
tiempo
posee

venir

3. Primero usamos reprlib en “Vector Take #1: Vector2d Compatible” en la página 276.

Toma de oración #1: Una secuencia de palabras | 403


Machine Translated by Google

la
morsa
dijo
>>> lista(s) #
['El', 'tiempo', 'ha', 'venir', 'el', 'morsa', 'dijo']

Una oración se crea a partir de una cadena.

Tenga en cuenta la salida de __repr__ usando ... generada por reprlib.repr.

Las instancias de oraciones son iterables; veremos por qué en un momento.

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

el intérprete necesita iterar sobre un objeto x, automáticamente llama a iter(x).

La función integrada iter :

1. Comprueba si el objeto implementa __iter__ y lo llama para obtener un iterador.

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

404 | Capítulo 14: Iterables, iteradores y generadores


Machine Translated by Google

__iter__, pero también cuando implementa __getitem__, siempre que __getitem__ acepte
claves int a partir de 0.

En el enfoque de escritura de ganso, la definición de iterable es más simple pero no tan


flexible: un objeto se considera iterable si implementa el método __iter__ . No se requieren
subclases ni registros, porque abc.Iterable implementa __subclasshook__, como se ve en
“Los gansos pueden comportarse como patos” en la página 338. Aquí hay una demostración:

>>> clase Foo:


... def __iter__(auto):
... pasar
...
>>> desde colecciones import abc
>>> issubclass(Foo, abc.Iterable)
Verdadero

>>> 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.

A partir de Python 3.4, la forma más precisa de verificar si un


objeto x es iterable es llamar a iter(x) y manejar una excepción
TypeError si no lo es. Esto es más preciso que usar isinstance(x,
abc.Iterable), porque iter(x) también considera el método
heredado __getitem__ , mientras que Iterable ABC no lo hace.

Verificar explícitamente si un objeto es iterable puede no valer la pena si justo después de la


verificación vas a iterar sobre el objeto. Después de todo, cuando se intenta la iteración en un
no iterable, la excepción que plantea Python es lo suficientemente clara: TypeError: objeto 'C'
un bloque try/except .no
Siiterable
puede hacerlo
en lugarmejor
de haciendo
que simplemente
un controlgenerar
explícito.
TypeError,
La verificación
hágalo en
explícita puede tener sentido si se aferra al objeto para iterarlo más tarde; en este caso,
puede ser útil detectar el error a tiempo.

La siguiente sección hace explícita la relación entre iterables e iteradores.

Iterables versus iteradores


De la explicación en “Por qué las secuencias son iterables: la función iter” en la página 404 ,
podemos extrapolar una definición:
iterable
Cualquier objeto del que la función integrada iter pueda obtener un iterador. Los objetos
que implementan un método __iter__ que devuelve un iterador son iterables. Secuencias

Iterables versus iteradores | 405


Machine Translated by Google

son siempre iterables; al igual que los objetos que implementan un método __getitem__ que toma
Índices basados en 0.

Es importante tener clara la relación entre iterables e iteradores: Python


obtiene iteradores de iterables.

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

Cree un iterador a partir de iterable.

Llame repetidamente a next en el iterador para obtener el siguiente elemento.

El iterador genera StopIteration cuando no hay más elementos.

Liberar referencia a él: el objeto iterador se descarta.

Sal del bucle.

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.

La interfaz estándar para un iterador tiene dos métodos:

__Siguiente__

Devuelve el siguiente elemento disponible, elevando StopIteration cuando no hay más


elementos.

406 | Capítulo 14: Iterables, iteradores y generadores


Machine Translated by Google

__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.

Ejemplo 14-3. abc. Clase Iterator; extraído de Lib/ _collections_abc.py

Iterador de clase (Iterable):

__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__)):

Iterables versus iteradores | 407


Machine Translated by Google

volver verdadero
volver No implementado

El método abstracto Iterator ABC es it.__next__() en Python 3 e


it.next() en Python 2. Como de costumbre, debe evitar llamar a
métodos especiales directamente. Simplemente use next(it): esta
función integrada hace lo correcto en Python 2 y 3.

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

Teniendo en cuenta los consejos de Lib/ types.py y la lógica


implementada en Lib/ _collections_abc.py, la mejor manera de verificar
si un objeto x es un iterador es llamar a isinstance(x, abc.Iterator).
Gracias a Iterator .__subclasshook__, esta prueba funciona incluso si
la clase de x no es una subclase real o virtual de Iterator.

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:

>>> s3 = Oración('Pig and Pepper') # >>> it =


iter(s3) # >>> it # doctest: +ELIPSIS <objeto
iterador en 0x...> >>> next(it) # 'Cerdo' >>>
siguiente(eso) 'y' >>> siguiente(eso)

'Pepper'
>>> next(it) #
Traceback (última llamada más reciente):
...
StopIteration
>>> list(it) # [] >>>
list(iter(s3)) #

['Cerdo', 'y', 'Pimiento']

Crea una oración s3 con tres palabras.

408 | Capítulo 14: Iterables, iteradores y generadores


Machine Translated by Google

Obtenga un iterador de s3.

next(it) recupera la siguiente palabra.

No hay más palabras, por lo que el iterador genera una excepción StopIteration .
Una vez agotado, un iterador se vuelve inútil.

Para repasar la oración nuevamente, se debe construir un nuevo iterador.

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

Para concluir esta sección, aquí hay una definición de iterador:


iterador

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.

Toma de oración #2: un iterador clásico


La siguiente clase Sentence se construye de acuerdo con el patrón de diseño clásico de Iterator siguiendo
el modelo del libro GoF. Tenga en cuenta que esto no es Python idiomático, como lo dejarán muy claro
las próximas refactorizaciones. Pero sirve para hacer explícita la relación entre la colección iterable y el
objeto iterador.

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.

Ejemplo 14-4. sentencia_iter.py: Sentencia implementada usando el patrón Iterator


importar
volver a importar reprlib

RE_PALABRA = re.compilar('\w+')

Toma de oración n.º 2: un iterador clásico | 409


Machine Translated by Google

oración de clase :

def __init__(uno mismo, texto):


self.texto = texto
self.palabras = RE_PALABRA.findall(texto)

def __repr__(uno mismo):


devuelve 'Oración(%s)' % reprlib.repr(self.text)

def __iter__(self): return


SentenceIterator(self.words)

iterador de oraciones de clase :

def __init__(uno mismo, palabras):


self.palabras = palabras
auto.índice = 0

def __siguiente__(uno mismo):


probar:

palabra = self.words[self.index] excepto


IndexError:
aumentar StopIteration()
self.index += 1 palabra de retorno

def __iter__(auto): retornar


auto

El método __iter__ es la única adición a la oración anterior


implementación. Esta versión no tiene __getitem__, para dejar claro que la clase
es iterable porque implementa __iter__.

__iter__ cumple con el protocolo iterable instanciando y devolviendo un iterador.


SentenceIterator contiene una referencia a la lista de palabras.

self.index se utiliza para determinar la siguiente palabra a buscar.

Obtenga la palabra en self.index.

Si no hay ninguna palabra en self.index, genere StopIteration .


Incremento self.index.

Devuelve la palabra.

Implementar self.__iter__.

El código del ejemplo 14-4 pasa las pruebas del ejemplo 14-2.

410 | Capítulo 14: Iterables, iteradores y generadores


Machine Translated by Google

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.

Hacer de la oración un iterador: mala idea Una causa

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.

La sección "Aplicabilidad"4 del patrón de diseño Iterator en el libro GoF dice:

Usar el patrón de iterador

• acceder al contenido de un objeto agregado sin exponer su representación interna. • para

admitir recorridos múltiples de objetos agregados. • proporcionar una interfaz uniforme para

atravesar diferentes estructuras agregadas (es decir,


para soportar la iteración polimórfica).

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.

Toma de oración n.º 2: un iterador clásico | 411


Machine Translated by Google

Un iterable nunca debe actuar como un iterador sobre sí mismo. En otras


palabras, los iterables deben implementar __iter__, pero no __next__.
Por otro lado, por conveniencia, los iteradores deben ser iterables.
El __iter__ de un iterador debería devolverse a sí mismo.

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.

Toma de oración #3: Una función generadora


Una implementación Pythonic de la misma funcionalidad usa una función generadora para
reemplazar la clase SequenceIterator . Una explicación adecuada de la función del generador
viene justo después del ejemplo 14-5.

Ejemplo 14-5. sentencia_gen.py: Sentencia implementada usando una función generadora


importar
volver a importar reprlib

RE_PALABRA = re.compilar('\w+')

oración de clase :

def __init__(auto, texto): auto.texto


= texto auto.palabras =
RE_PALABRA.findall(texto)

def __repr__(self): return


'Oración(%s)' % reprlib.repr(self.text)

def __iter__(self): for


palabra en self.words: yield word

devolver

# ¡hecho!

Iterar sobre self.word.

Produce la palabra actual.

412 | Capítulo 14: Iterables, iteradores y generadores


Machine Translated by Google

Esta devolución no es necesaria; la función puede simplemente "fallar" y regresar


automáticamente. De cualquier manera, una función generadora no genera StopIteration:
simplemente sale cuando termina de producir valores.5 ¡ No se necesita una clase de iterador

separada!

Aquí nuevamente tenemos una implementación diferente de Oración que pasa las pruebas del
Ejemplo 14-2.

Volviendo al código de Sentence en el Ejemplo 14-4, __iter__ llamó al constructor SentenceIterator


para construir un iterador y devolverlo. Ahora, el iterador del ejemplo 14-5 es, de hecho, un objeto
generador, construido automáticamente cuando se llama al método __iter__ , porque __iter__ aquí
es una función generadora.

A continuación se incluye una explicación completa de las funciones del generador.

Cómo funciona una función de generador

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

>>> def gen_123(): #


... rendimiento 1 #
... rendimiento 2
... rendimiento 3
...
>>> gen_123 # doctest: +ELIPSIS
<función gen_123 en 0x...> #

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__.

7. Gracias a David Kwast por sugerir este ejemplo.

Frase Toma #3: Una Función Generadora | 413


Machine Translated by Google

>>> gen_123() # doctest: +ELIPSIS <objeto


generador gen_123 en 0x...> # >>> para i en
gen_123(): # print(i)
...
1
2
3
>>> g = gen_123() # >>>
siguiente(g) # 1 >>>
siguiente(g) 2

>>> 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.

Mirando de cerca, vemos que gen_123 es un objeto de función.

Pero cuando se invoca, gen_123() devuelve un objeto generador.

Los generadores son iteradores que producen los valores de las expresiones pasadas a yield.

Para una inspección más cercana, asignamos el objeto generador a g.

Debido a que g es un iterador, llamar a next(g) obtiene el siguiente elemento producido por yield.

Cuando se completa el cuerpo de la función, el objeto generador genera un StopIt


eración

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 .

414 | Capítulo 14: Iterables, iteradores y generadores


Machine Translated by Google

Encuentro útil ser estricto al hablar de los resultados obÿ


de un generador: digo que un generador rinde o produce
valores. Pero es confuso decir que un generador "devuelve" valores. Función
ciones devuelven valores. Llamar a una función de generador devuelve un generador
ador Un generador cede o produce valores. Un generador no
valores "devueltos" de la manera habitual: la declaración de retorno en el cuerpo
de una función de generador hace que StopIteration sea generado por el
objeto generador.8

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.

>>>

La función generadora se define como cualquier función, pero usa rendimiento.

La primera llamada implícita a next() en el ciclo for imprimirá 'inicio' y se detendrá


en el primer rendimiento, produciendo el valor 'A'.

La segunda llamada implícita a next() en el ciclo for imprimirá 'continuar' y


detenerse en el segundo rendimiento, produciendo el valor 'B'.

La tercera llamada a next() imprimirá 'fin'. y caer hasta el final de la función


body, lo que hace que el objeto generador genere StopIteration.

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.

Frase Toma #3: Una Función Generadora | 415


Machine Translated by Google

Para iterar, la maquinaria for hace el equivalente de g = iter(gen_AB()) para obtener un


objeto generador, y luego next(g) en cada iteración.

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.

La cadena 'start' aparece como resultado de print('start') en el cuerpo de la función del


generador. yield 'A' en el cuerpo de la función del generador produce el valor A consumido

por el bucle for , que se asigna a la variable c y da como resultado la salida :

> 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.

A continuación, construiremos una clase de oración perezosa .

Toma de oración n.º 4: una implementación perezosa

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.

416 | Capítulo 14: Iterables, iteradores y generadores


Machine Translated by Google

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

def __init__(self, texto): self.texto


= texto

def __repr__(self): return


'Oración(%s)' % reprlib.repr(self.text)

def __iter__(self): for


match in RE_WORD.finditer(self.text):
rendimiento partido.grupo()

No es necesario tener una lista de palabras .

finditer construye un iterador sobre las coincidencias de RE_WORD en self.text, produciendo instancias
de MatchObject .

match.group() extrae el texto coincidente real de la instancia 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.

Toma de oración n.° 5: una expresión generadora


Las funciones generadoras simples como la de la clase Oración anterior (Ejemplo 14-7) se pueden reemplazar
por una expresión generadora.

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.

Toma de oración #5: Una expresión generadora | 417


Machine Translated by Google

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

>>> def gen_AB(): #


... imprimir('comenzar')
... rendimiento 'A'
... imprimir('continuar')
... rendimiento 'B'
... imprimir('fin.')
...
>>> res1 = [x*3 para x en gen_AB()] # inicio

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.

Esta es la misma función gen_AB del ejemplo 14-6.

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í.

res2 es un objeto generador.

418 | Capítulo 14: Iterables, iteradores y generadores


Machine Translated by Google

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.

Ejemplo 14-9. sentencia_genexp.py: Sentencia implementada usando una expresión generadora


importar
volver a importar reprlib

RE_PALABRA = re.compilar('\w+')

oración de clase :

def __init__(self, texto):


self.texto = texto

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.

Expresiones generadoras: cuándo usarlas


Usé varias expresiones generadoras al implementar la clase Vector en el Ejemplo 10-16.
Cada uno de los métodos __eq__, __hash__, __abs__, angle, angles, format, __add__
y __mul__ tiene una expresión generadora. En todos esos métodos, también funcionaría
una lista por comprensión, a costa de usar más memoria para almacenar los valores
intermedios de la lista.

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

Expresiones generadoras: cuándo usarlas | 419


Machine Translated by Google

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.

Otro Ejemplo: Generador de Progresión Aritmética


El patrón clásico de iterador tiene que ver con el recorrido: navegar por alguna estructura de datos. Pero
una interfaz estándar basada en un método para obtener el siguiente elemento de una serie también es
útil cuando los elementos se producen sobre la marcha, en lugar de recuperarlos de una colección. Por
ejemplo, el rango incorporado genera una progresión aritmética limitada (AP) de enteros, y la función
itertools.count genera un AP ilimitado.

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

420 | Capítulo 14: Iterables, iteradores y generadores


Machine Translated by Google

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.

Ejemplo 14-10. Demostración de una clase ArithmeticProgression


>>> ap = ProgresiónAritmética(0, 1, 3) >>>
lista(ap) [0, 1, 2] >>> ap = ProgresiónAritmética(1,
.5, 3) >>> lista(ap) [1.0 , 1.5, 2.0, 2.5] >>> ap =
ProgresiónAritmética(0, 1/3, 1) >>> lista(ap) [0.0,
0.3333333333333333, 0.6666666666666666]
>>> from fracciones import Fracción >>> ap =
ProgresiónAritmética( 0, Fracción(1, 3), 1) >>>
lista(ap)

[Fracción(0, 1), Fracción(1, 3), Fracción(2, 3)] >>> from


decimal import Decimal >>> ap = ProgresiónAritmética(0,
Decimal('.1'), .3) > >> lista(ap)

[Decimal('0.0'), Decimal('0.1'), Decimal('0.2')]

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 .

El ejemplo 14-11 enumera la implementación de la clase ArithmeticProgression .

Ejemplo 14-11. La clase de progresión aritmética


clase Progresión Aritmética:

def __init__(self, begin, step, end=Ninguno):


self.begin = comenzar self.step = paso self.end
= end # Ninguno -> serie "infinita"

def __iter__(self): result


= type(self.begin + self.step)(self.begin) forever = self.end is
None index = 0 while forever or result < self.end:

rendimiento
resultado
índice += 1 resultado = self.begin + self.step * índice

Otro Ejemplo: Generador de Progresión Aritmética | 421


Machine Translated by Google

__init__ requiere dos argumentos: begin y step. end es opcional, si es


None, la serie será ilimitada.
Esta línea produce un valor de resultado igual a self.begin, pero forzado al tipo de las adiciones
subsiguientes.9 Para facilitar la lectura, el indicador forever será True si el atributo self.end es

None, lo que da como resultado una serie ilimitada.

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 produce el resultado actual .

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.

Ejemplo 14-12. La función generadora aritprog_gen

def aritprog_gen(comienzo, paso, fin=Ninguno):


resultado = tipo(comienzo + paso)(comienzo)
para siempre = fin es Ninguno índice = 0
mientras que para siempre o resultado < fin:
dar resultado

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 .

422 | Capítulo 14: Iterables, iteradores y generadores


Machine Translated by Google

í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 .

Progresión aritmética con itertools El módulo

itertools en Python 3.4 tiene 19 funciones generadoras que se pueden combinar en una variedad de
formas interesantes.

Por ejemplo, la función itertools.count devuelve un generador que produce números.


Sin argumentos, produce una serie de números enteros que comienzan con 0. Pero puede
proporcionar valores de inicio y paso opcionales para lograr un resultado muy similar a nuestras
funciones aritprog_gen :

>>> import itertools >>>


gen = itertools.count(1, .5) >>> next(gen) 1

>>> siguiente (generación)


1.5
>>> siguiente (generación)
2.0 >>> siguiente

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

Aprovechando takewhile y count, el ejemplo 14-13 es agradable y breve.

Ejemplo 14-13. aritprog_v3.py: esto funciona como las funciones anteriores de aritprog_gen

importar itertools

def aritprog_gen(comienzo, paso, fin=Ninguno):


primero = tipo(comienzo + paso)(comienzo)
ap_gen = itertools.count(primero, paso) si el final
no es Ninguno:

Otro Ejemplo: Generador de Progresión Aritmética | 423


Machine Translated by Google

ap_gen = itertools.takewhile(lambda n: n < fin, ap_gen)


volver ap_gen

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.

Funciones de generador en la biblioteca estándar


La biblioteca estándar proporciona muchos generadores, desde objetos de archivo de texto sin formato que brindan
iteración línea por línea, hasta la increíble función os.walk , que produce nombres de archivo mientras atraviesa un
árbol de directorios, lo que hace que las búsquedas recursivas del sistema de archivos sean tan simples como un bucle for .

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.

Tabla 14-1. Filtrado de funciones del generador

Módulo Función Descripción

iterar compress(it, selector_it) Consume dos iterables en paralelo; produce elementos de él siempre que el

instrumentos elemento correspondiente en selector_it sea verdadero

iterar dropwhile (predicado, Lo consume omitiendo elementos mientras el predicado calcula la verdad, luego produce todos

instrumentos eso) los elementos restantes (no se realizan más comprobaciones)

424 | Capítulo 14: Iterables, iteradores y generadores


Machine Translated by Google

Módulo Función Descripción

(incorporado) filtrar (predicado, Aplica el predicado a cada elemento de iterable, generando el elemento si el predicado (elemento)

eso) es verdadero; si el predicado es Ninguno, solo se obtienen elementos veraces

iterar filterfalse(predicado, Igual que el filtro, con la lógica de predicado negada: produce elementos cada vez que el predicado

instrumentos it) calcula falso

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.

Ejemplo 14-14. Ejemplos de funciones de generador de filtrado

>>> def vocal(c):


... devuelve c.inferior() en 'aeiou'
...
>>> lista(filtro(vocal, 'Aardvark'))
['A', 'a', 'a'] >>>
import itertools >>>
list(itertools.filterfalse(vocal, 'Aardvark')) ['r', 'd', 'v', 'r' , 'k']
>>> lista(itertools.dropwhile(vocal, 'Aardvark')) ['r', 'd', 'v', 'a',
'r', 'k'] >>> lista (itertools.takewhile(vocal, 'Aardvark'))

['A', 'a'] >>>


lista(itertools.compress('Aardvark', (1,0,1,1,0,1)))
['A', 'r', 'd', 'a'] >>>
lista(itertools.islice('Aardvark', 4))
['A', 'a', 'r', 'd'] >>>
lista(itertools.islice('Aardvark', 4, 7)) ['v', 'a', 'r'] >> >
lista(itertools.islice('Aardvark', 1, 7, 2)) ['a', 'd', 'a']

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.

funciones de generador en la biblioteca estándar | 425


Machine Translated by Google

Tabla 14-2. Funciones del generador de mapas


Módulo Función Descripción

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

elementos iterables iit, y func se aplica como func(*iit)

El ejemplo 14-15 demuestra algunos usos de itertools.accumulate.

Ejemplo 14-15. Ejemplos de funciones itertools.accumulate generador


>>> muestra = [5, 4, 2, 8, 7, 6, 3, 0, 9, 1]
>>> importar itertools
>>> list(itertools.accumulate(muestra)) # [5, 9, 11,
19, 26, 32, 35, 35, 44, 45]
>>> list(itertools.accumulate(muestra, min)) # [5, 4, 2, 2, 2,
2, 2, 0, 0, 0]
>>> list(itertools.accumulate(muestra, max)) # [5, 5, 5, 8,
8, 8, 8, 8, 9, 9]
>>> operador de importación
>>> list(itertools.accumulate(muestra, operator.mul)) # [5, 20, 40, 320,
2240, 13440, 40320, 0, 0, 0]
>>> lista(itertools.acumula(rango(1, 11), operador.mul))
[1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800] #

Suma corriente.

Funcionamiento mínimo.

Correr al máximo.

Producto en ejecución.
Factoriales desde 1! a las 10!.

Las funciones restantes de la tabla 14-2 se muestran en el ejemplo 14-16.

Ejemplo 14-16. Ejemplos de funciones del generador de mapas


>>> lista(enumerar('albatroz', 1)) # [(1, 'a'), (2,
'l'), (3, 'b'), (4, 'a'), (5 , 't'), (6, 'r'), (7, 'o'), (8, 'z')]
>>> operador de importación
>>> lista(mapa(operador.mul, rango(11), rango(11))) # [0, 1, 4, 9,
16, 25, 36, 49, 64, 81, 100]
>>> lista(mapa(operador.mul, rango(11), [2, 4, 8])) # [0, 4, 16]

>>> lista(mapa(lambda a, b: (a, b), rango(11), [2, 4, 8])) #

426 | Capítulo 14: Iterables, iteradores y generadores


Machine Translated by Google

[(0, 2), (1, 4), (2, 8)] >>>


import itertools >>>
list(itertools.starmap(operator.mul, enumerate('albatroz', 1))) # [' a', 'll', 'bbb', 'aaaa',
'ttttt', 'rrrrrr', 'ooooooo', 'zzzzzzzz'] >>> muestra = [5, 4, 2, 8, 7, 6, 3 , 0, 9, 1] >>>
list(itertools.starmap(lambda a, b: b/a, enumerate(itertools.accumulate(muestra), 1))) #
[5.0, 4.5, 3.6666666666666665, 4.75, 5.2, 5.333333333333333, 5.0, 4.375,
... 4.888888888888889, 4.5]

Numere las letras de la palabra, comenzando desde el 1.

Cuadrados de números enteros del 0 al 10.

Multiplicar números de dos iterables en paralelo: los resultados se detienen cuando finaliza el iterable
más corto.

Esto es lo que hace la función integrada zip .

Repita cada letra de la palabra según su lugar en ella, comenzando desde 1.

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.

Tabla 14-3. Funciones de generador que fusionan múltiples iterables de entrada


Módulo Función Descripción

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

iterables de entrada se consuman más de una vez

(incorporado) zip(it1, …, itN) Produce N tuplas creadas a partir de elementos tomados de los iterables en paralelo,

deteniéndose silenciosamente cuando se agota el primer iterable

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.

funciones de generador en la biblioteca estándar | 427


Machine Translated by Google

Ejemplo 14-17. Ejemplos de funciones de generador de fusión

>>> lista(itertools.cadena('ABC', rango(2))) # ['A', 'B',


'C', 0, 1]
>>> lista(itertools.chain(enumerar('ABC'))) # [(0, 'A'), (1,
'B'), (2, 'C')]
>>> list(itertools.chain.from_iterable(enumerar('ABC'))) # [0, 'A', 1, 'B', 2,
'C']
>>> lista(zip('ABC', rango(5))) # [('A', 0),
('B', 1), ('C', 2)]
>>> lista(zip('ABC', rango(5), [10, 20, 30, 40])) # [('A', 0, 10),
('B', 1, 20), ( 'C', 2, 30)]
>>> list(itertools.zip_longest('ABC', range(5))) # [('A', 0), ('B', 1),
('C', 2), (Ninguno, 3) , (Ninguno, 4)]
>>> list(itertools.zip_longest('ABC', range(5), fillvalue='?')) # [('A', 0), ('B', 1), ('C',
2) , ('?', 3), ('?', 4)]

La cadena generalmente se llama con dos o más iterables.

chain no hace nada útil cuando se llama con un solo iterable.

Pero chain.from_iterable toma cada elemento del iterable y los encadena


en secuencia, siempre que cada elemento sea iterable en sí mismo.

zip se usa comúnmente para fusionar dos iterables en una serie de dos tuplas.

Zip puede consumir cualquier número de iterables en paralelo, pero el generador


se detiene tan pronto como finaliza el primer iterable.

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.

El argumento de la palabra clave fillvalue especifica un valor de relleno personalizado.

El generador itertools.product es una forma perezosa de calcular productos cartesianos,


que construimos usando listas de comprensión con más de una cláusula for en "Cartesian
Productos” en la página 23. También se pueden usar expresiones generadoras con múltiples cláusulas for
para producir productos cartesianos perezosamente. El ejemplo 14-18 demuestra itertools.product.

Ejemplo 14-18. Ejemplos de funciones del generador itertools.product

>>> lista(itertools.producto('ABC', rango(2))) # [('A', 0), ('A',


1), ('B', 0), ('B', 1), ('C', 0), ('C', 1)]
>>> palos = 'picas corazones diamantes tréboles'.split()
>>> list(itertools.product('AK', palos)) # [('A', 'picas'),
('A', 'corazones'), ('A', 'diamantes'), (' A', 'tréboles'),
('K', 'picas'), ('K', 'corazones'), ('K', 'diamantes'), ('K', 'tréboles')]
>>> lista(itertools.producto('ABC')) # [('A',),
('B',), ('C',)]
>>> lista(itertools.producto('ABC', repetir=2)) # [('A', 'A'),
('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'B'),
('B', 'C'), ('C', 'A'), ('C', 'B'), ('C', 'C')]
>>> lista(itertools.producto(rango(2), repetir=3))

428 | Capítulo 14: Iterables, iteradores y generadores


Machine Translated by Google

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

Módulo Función Descripción

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,

out_len) incluidas las combinaciones con elementos repetidos

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

completa repetidamente, indefinidamente

itertools permutaciones (it, Permutaciones de rendimiento de elementos out_len de los elementos


out_len=Ninguno) producidos por él; por defecto, out_len es len(list(it))

funciones de generador en la biblioteca estándar | 429


Machine Translated by Google

Módulo Función Descripción

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.

Ejemplo 14-19. contar, ciclar y repetir

>>> ct = itertools.contar() # >>>


siguiente(ct) # 0

>>> siguiente(ct), siguiente(ct), siguiente(ct)


# (1, 2, 3)
>>> lista(itertools.islice(itertools.cuenta(1, .3), 3)) # [1, 1.3, 1.6]

>>> cy = itertools.ciclo('ABC') # >>>


siguiente(cy)
'A'
>>> lista(itertools.islice(cy, 7)) # ['B', 'C', 'A',
'B', 'C', 'A', 'B']
>>> rp = itertools.repeat(7) # >>>
siguiente(rp), siguiente(rp)
(7, 7)
>>> lista(itertools.repetir(8, 4)) # [8, 8, 8, 8]

>>> lista(mapa(operador.mul, rango(11), itertools.repetir(5))) # [0, 5, 10, 15,


20, 25, 30, 35, 40, 45, 50]

Construya un generador de conteo ct.

Recupere el primer elemento de ct.

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.

Construya un generador de ciclos a partir de 'ABC' y obtenga su primer elemento, 'A'.

Una lista solo se puede construir si está limitada por islice; se recuperan los siete elementos siguientes
aquí.

Construya un generador de repetición que produzca el número 7 para siempre.

Un generador de repeticiones se puede limitar pasando el argumento times : aquí el


el número 8 se producirá 4 veces.

430 | Capítulo 14: Iterables, iteradores y generadores


Machine Translated by Google

Un uso común de repetir: proporcionar un argumento fijo en el mapa; aquí proporciona el multiplicador 5 .

Las funciones de generador de combinaciones, combinaciones_con_reemplazo y permutaciones , junto con el


producto, se denominan generadores combinatorios en la página de documentación de herramientas iter . También
existe una estrecha relación entre itertools.product y las funciones combinatorias restantes , como muestra el ejemplo
14-20 .

Ejemplo 14-20. Las funciones del generador combinatorio producen múltiples valores por elemento de entrada

>>> lista(itertools.combinaciones('ABC', 2)) # [('A', 'B'),


('A', 'C'), ('B', 'C')] >> >
lista(itertools.combinations_with_replacement('ABC', 2)) # [('A', 'A'), ('A', 'B'),
('A', 'C'), ('B' , 'B'), ('B', 'C'), ('C', 'C')] >>> lista(itertools.permutaciones('ABC', 2)) # [('A', '
B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', ' B')] >>> lista(itertools.producto('ABC',
repetir=2)) # [('A', 'A'), ('A', 'B'), ('A', ' C'), ('B', 'A'), ('B', 'B'), ('B', 'C'), ('C', 'A'), ('C', ' B'),
('C', 'C')]

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.

Producto cartesiano de 'ABC' y 'ABC' (ese es el efecto de repetir = 2).

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.

Tabla 14-5. Reorganización de las funciones del generador

Módulo Función Descripción

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

funciones de generador en la biblioteca estándar | 431


Machine Translated by Google

Módulo Función Descripción

(incorporado) invertido (secuencia) Entrega elementos de secuencia en orden inverso, del último al primero; seq debe ser una secuencia

o implementar el método especial __reversed__

camiseta itertools(es, n=2) Produce una tupla de n generadores, cada uno de los cuales produce los elementos de la entrada iterable

independientemente

El ejemplo 14-21 demuestra el uso de itertools.groupby y el incorporado inverso .


Tenga en cuenta que itertools.groupby asume que la entrada iterable está ordenada por la agrupación
criterio, o al menos que los elementos están agrupados por ese criterio, incluso si no están ordenados.

Ejemplo 14-21. itertools.groupby

>>> list(itertools.groupby('LLLLAAGGG')) # [('L',


<itertools._grouper objeto en 0x102227cc0>),
('A', <objeto itertools._grouper en 0x102227b38>),
('G', <objeto itertools._grouper en 0x102227b70>)]
>>> para char, grupo en itertools.groupby('LLLLAAAAGG'): #
... imprimir(caracter, '->', lista(grupo))
...
L -> ['L', 'L', 'L', 'L']
A -> ['A', 'A',]
G -> ['G', 'G', 'G']
>>> animales = ['pato', 'águila', 'rata', 'jirafa', 'oso',
... 'murciélago', 'delfín', 'tiburón', 'león']
>>> animales.sort(key=len) # >>>
animales
['rata', 'murciélago', 'pato', 'oso', 'león', 'águila', 'tiburón',
'jirafa', 'delfín']
>>> para longitud, grupo en itertools.groupby(animales, longitud ): #
... print(longitud, '->', lista(grupo))
...
3 -> ['rata', 'murciélago']
4 -> ['pato', 'oso', 'león']
5 -> ['águila', 'tiburón']
7 -> ['jirafa', 'delfín']
>>> para longitud, grupo en itertools.groupby(reversed(animals), len): #
... imprimir (longitud, '->', lista (grupo))
...
7 -> ['delfín', 'jirafa']
5 -> ['tiburón', 'águila']
4 -> ['león', 'oso', 'pato']
3 -> ['murciélago', 'rata']
>>>

groupby produce tuplas de (key, group_generator).


El manejo de generadores groupby implica una iteración anidada: en este caso, el exterior
for loop y el constructor de la lista interna .

Para usar groupby, la entrada debe estar ordenada; aquí las palabras están ordenadas por longitud.

432 | Capítulo 14: Iterables, iteradores y generadores


Machine Translated by Google

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.

Nueva sintaxis en Python 3.3: rendimiento de


Los bucles for anidados son la solución tradicional cuando una función de generador necesita producir valores
producidos por otro generador.

Por ejemplo, aquí hay una implementación casera de un generador de encadenamiento:12

>>> def cadena(*iterables): for


... it in iterables: for i in it:
...

12. El itertools.chain de la biblioteca estándar está escrito en C.

Nueva sintaxis en Python 3.3: rendimiento de | 433


Machine Translated by Google

... 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:

>>> cadena de definición (*iterables):


... para i en iterables:
... rendimiento de i
...
>>> lista(cadena(s, t))
['A', 'B', 'C', 0, 1, 2]

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.

Funciones reductoras iterables


Todas las funciones de la tabla 14-6 toman un iterable y devuelven un solo resultado. Ellos son conocidos
como funciones de “reducción”, “plegamiento” o “acumulación”. En realidad, cada una de las funciones integradas
enumeradas aquí se puede implementar con functools.reduce, pero existen como funciones integradas.
porque abordan algunos casos de uso común más fácilmente. Asimismo, en el caso de todos y
any, hay una optimización importante que no se puede hacer con reduce: estas funciones
cortocircuito (es decir, dejan de consumir el iterador tan pronto como se determina el resultado).
Vea la última prueba con cualquiera en el ejemplo 14-23.

Tabla 14-6. Funciones integradas que leen iterables y devuelven valores únicos

Módulo Función Descripción

(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

434 | Capítulo 14: Iterables, iteradores y generadores


Machine Translated by Google

Módulo Función Descripción

(incorporado) max(it, [clave=,] [por defecto=]) Devuelve el valor máximo de los elementos que contiene;a la clave es un pedido

función, como en ordenado; se devuelve el valor predeterminado si el iterable está

(incorporado) min(it, [clave=,] [por defecto=]) vacío. Devuelve el valor mínimo de los elementos en él. bkey es una función de

ordenamiento, como en sorted; se devuelve el valor predeterminado si el iterable está

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

con el primer elemento

(incorporado) sum(it, comienzo=0) La suma de todos los elementos que contiene, con el valor de inicio opcional

agregado (use math.fsum para una mayor precisión al agregar flotantes)

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.

La operación de todos y cualquiera se ejemplifica en el ejemplo 14-23.

Ejemplo 14-23. Resultados de all y any para algunas secuencias


>>> todo([1, 2, 3])

Verdadero >>> todo([1, 0, 3])


Falso
>>> todo([])

Verdadero >>> cualquier([1, 2, 3])


Verdadero

>>> cualquier([1, 0, 3])


Verdadero

>>> cualquier([0, 0.0])


Falso
>>> cualquiera([])
Falso
>>> g = (n para n en [0, 0.0, 7, 8]) >>> cualquier(g)

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.

Funciones reductoras iterables | 435


Machine Translated by Google

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.

Una mirada más cercana a la función iter


Como hemos visto, Python llama a iter(x) cuando necesita iterar sobre un objeto x.

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 :

>>> def d6():


... devuelve randint(1, 6)
...
>>> d6_iter = iter(d6, 1) >>>
d6_iter <objeto callable_iterator
en 0x00000000029BE6A0> >>> for roll in d6_iter: print(roll)

...
...
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:

con open('mydata.txt') como fp: for


line in iter(fp.readline, ''): process_line(line)

Para cerrar este capítulo, presento un ejemplo práctico del uso de generadores para manejar un gran
volumen de datos de manera eficiente.

436 | Capítulo 14: Iterables, iteradores y generadores


Machine Translated by Google

Estudio de caso: Generadores en una utilidad de conversión de base de datos

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.

Estudio de caso: Generadores en una utilidad de conversión de base de datos | 437


Machine Translated by Google

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.

438 | Capítulo 14: Iterables, iteradores y generadores


Machine Translated by Google

Generadores como corrutinas


Aproximadamente cinco años después de que se introdujeran las funciones de generador con la
palabra clave yield en Python 2.2, se implementó PEP 342 — Corrutinas a través de generadores
mejorados en Python 2.5. Esta propuesta agregó métodos y funciones adicionales a los objetos
generadores, sobre todo el método .send() .

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:

• Los generadores producen datos para la iteración

• Las corrutinas son consumidoras de datos.

• Para evitar que su cerebro explote, no mezcle los dos conceptos

• Las corrutinas no están relacionadas con la iteración.

• 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.

Resumen del capítulo


La iteración está tan profundamente arraigada en el lenguaje que me gusta decir que Python asimila a
los iteradores.15 La integración del patrón Iterator en la semántica de Python es un excelente ejemplo
de cómo los patrones de diseño no son igualmente aplicables en todos los lenguajes de programación.

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.”

Generadores como rutinas | 439


Machine Translated by Google

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.

Luego codificamos un generador de progresiones aritméticas y mostramos cómo aprovechar el módulo


itertools para hacerlo más simple. A continuación, se incluye una descripción general de 24 funciones de
generador de propósito general en la biblioteca estándar.

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 capítulo 4, “Iteradores y generadores”, de Python Cookbook, 3E (O'Reilly), de David Beazley y Brian K.


Jones, tiene 16 recetas que cubren este tema desde muchos ángulos diferentes, siempre enfocándose en
aplicaciones prácticas.

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.

440 | Capítulo 14: Iterables, iteradores y generadores


Machine Translated by Google

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

Sintaxis de la función del generador: Más azúcar estaría bien

Los diseñadores deben asegurarse de que los controles y las pantallas para diferentes propósitos sean
significativamente diferentes entre sí.

—Donald normando

El diseño de las cosas cotidianas

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:

Una declaración de "rendimiento" enterrada en el cuerpo no es suficiente advertencia de que la semántica es


tan diferente.

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

def f(): x=0


while True:
X+=1
rendimiento x

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.

Lectura adicional | 441


Machine Translated by Google

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.

17. Diapositiva 31, “Un curso curioso sobre corrutinas y concurrencia”.

442 | Capítulo 14: Iterables, iteradores y generadores


Machine Translated by Google

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.

Semántica de generador versus iterador

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:

>>> from collections import abc


>>> e = enumerate('ABC') >>>
isinstance(e, abc.Iterator)
Verdadero

El segundo es el punto de vista de la implementación. Desde este ángulo, un generador es una


construcción del lenguaje Python que se puede codificar de dos maneras: como una función con la
palabra clave yield o como una expresión generadora. Los objetos generadores resultantes de llamar a
una función generadora o evaluar una expresión generadora son instancias de un tipo de generador
interno. Desde esta perspectiva, cada generador también es un iterador, porque las instancias de
Generator Type implementan la interfaz del iterador. Pero puede escribir un iterador que no sea un
generador, implementando el patrón de iterador clásico, como vimos en el Ejemplo 14-4, o codificando
una extensión en C. Los objetos enumerados no son generadores desde esta perspectiva:

>>> importar tipos


>>> e = enumerar('ABC')
>>> isinstance(e, tipos.GeneratorType)
Falso

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

Lectura adicional | 443


Machine Translated by Google

tern, el generador devuelto por enumerate no es un iterador porque crea las tuplas que produce.

En este nivel conceptual, la técnica de implementación es irrelevante. Puede escribir un generador


sin usar un objeto generador de Python. El ejemplo 14-26 es un generador de Fibonacci que escribí
solo para aclarar este punto.

Ejemplo 14-26. fibo_by_hand.py: generador de Fibonacci sin GeneratorType enÿ


posturas

clase Fibonacci:

def __iter__(self):
devuelve el Generador de Fibonacci()

clase Generador de Fibonacci:

def __init__(self): self.a


= 0 self.b = 1

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:

Iterador: un objeto que representa un flujo de datos. […]

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.

444 | Capítulo 14: Iterables, iteradores y generadores


Machine Translated by Google

objeto que construye. Entonces, en la jerga de la comunidad de Python, iterador y generador son sinónimos
bastante cercanos.

La interfaz de iterador minimalista en Python En la

sección "Implementación" del patrón de iterador,18 Gang of Four escribió:

La interfaz mínima de Iterator consta de las operaciones First, Next, IsDone y


CurrentItem.

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.

Lectura adicional | 445


Machine Translated by Google
Machine Translated by Google

CAPÍTULO 15

Administradores de contexto y otros bloques

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 y los administradores de


contexto • La cláusula else en las declaraciones for, while y try

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

Haz esto, luego aquello: else Bloquea más allá if


Esto no es ningún secreto, pero es una característica del lenguaje subestimada: la cláusula else se puede
usar no solo en declaraciones if sino también en declaraciones for, while y try .

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é.

Estas son las reglas:

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 .

El uso de else en bucles generalmente sigue el patrón de este fragmento:

for item in my_list: if


item.flavor == 'banana': break

else:
aumentar ValueError('¡No se encontró sabor a plátano!')

448 | Capítulo 15: Administradores de contexto y otros bloques


Machine Translated by Google

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.

El glosario luego define LBYL:


LBI
Mira antes de saltar. Este estilo de codificación prueba explícitamente las condiciones
previas antes de realizar llamadas o búsquedas. Este estilo contrasta con el enfoque EAFP y
se caracteriza por la presencia de muchas sentencias if. En un entorno de subprocesos
múltiples, el enfoque LBYL puede correr el riesgo de introducir una condición de carrera entre
"mirar" y "saltar". Por ejemplo, el código, if key in mapping: return mapping[key] puede fallar si
otro subproceso elimina la clave de la asignación después de la prueba, pero antes de la búsqueda.
Este problema se puede resolver con candados o utilizando el enfoque EAFP.

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.

Haz esto, luego aquello: else Bloquea más allá if | 449


Machine Translated by Google

Gestores de Contexto y con Bloques


Los objetos del administrador de contexto existen para controlar una declaración with , al igual que los iteradores existen
para controlar una declaración for .

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.

Ejemplo 15-1. Demostración de un objeto de archivo como administrador de contexto


>>> con open('mirror.py') como fp: # src =
... fp.read(60) #
...
>>> len(src)
60 >>> fp #

<_io.TextIOWrapper name='mirror.py' mode='r' encoding='UTF-8'> >>>


fp.closed, fp.encoding # (True , 'UTF-8') >>> fp.read(60) # Rastreo (última
llamada más reciente):

Archivo "<stdin>", línea 1, en <módulo>


ValueError: operación de E/S en archivo cerrado.

fp está vinculado al archivo abierto porque el método __enter__ del archivo devuelve self.
Lea algunos datos de fp.

La variable fp todavía está disponible.2

Puede leer los atributos del objeto 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.

450 | Capítulo 15: Administradores de contexto y otros bloques


Machine Translated by Google

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__ .

Ejemplo 15-2. Pruebe la conducción de la clase de administrador de contexto LookingGlass

>>> from mirror import LookingGlass >>>


with LookingGlass() as what: print('Alice,
... Kitty and Snowdrop') print(what)
...
...
adn de pordwons yttik , ecila
ykcowrebaj
>>> qué
'JABBERWOCKY'
>>> print('Volver a la normalidad.')
Volver a la normalidad.

El administrador de contexto es una instancia de LookingGlass; Python llama a __enter__ en el administrador


de contexto y el resultado está vinculado a qué.

Imprima una cadena, luego el valor de la variable de destino qué.

La salida de cada impresión sale al revés.

Ahora el bloque con ha terminado. Podemos ver que el valor devuelto por __enter__, contenido en what, es
la cadena 'JABBERWOCKY'.

La salida del programa ya no es hacia atrás.

El ejemplo 15-3 muestra la implementación de LookingGlass.

Gestores de Contexto y con Bloques | 451


Machine Translated by Google

Ejemplo 15-3. mirror.py: código para la clase de administrador de contexto LookingGlass


clase LookingGlass:

def __enter__(self):
import sys
self.original_write = sys.stdout.write
sys.stdout.write = self.reverse_write devuelve
'JABBERWOCKY'

def reverse_write(self, texto):


self.original_write(texto[::-1])

def __exit__(self, exc_type, exc_value , traceback ):


import sys
sys.stdout.write = self.original_write si exc_type
es ZeroDivisionError: print('¡NO divida por
cero!')
volver verdadero

Python invoca __enter__ sin argumentos además de sí mismo.

Mantenga el método sys.stdout.write original en un atributo de instancia para más tarde


usar.

Monkey-patch sys.stdout.write, reemplazándolo con nuestro propio método.

Devuelve la cadena 'JABBERWOCKY' solo para que tengamos algo que poner en el objetivo
variable qué.

Nuestro reemplazo de sys.stdout.write invierte el argumento de texto y llama al


implementación original.

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.

Es barato volver a importar módulos porque Python los almacena en caché.

Restaure el método original a sys.stdout.write.

Si la excepción no es Ninguna y su tipo es ZeroDivisionError, imprima un mensaje...

…y devuelva True para decirle al intérprete que se manejó la excepció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.

452 | Capítulo 15: Administradores de contexto y otros bloques


Machine Translated by Google

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__ .

Ejemplo 15-4. Ejercicio de LookingGlass sin bloque

>>> from mirror import LookingGlass >>>


manager = LookingGlass()
>>>
administrador <mirror.LookingGlass object at 0x2a578ac>
>>> monstruo = administrador.__enter__() >>> monstruo
== 'JABBERWOCKY'
euro
>>> monstruo
'YKCOWREBAJ'
>>> gerente
>ca875a2x0 ta tcejbo ssalGgnikooL.rorrim< >>>
gerente.__exit__(Ninguno, Ninguno, Ninguno) >>> monstruo

'JABBERWOCKY'

Cree una instancia e inspeccione la instancia del administrador .

Llame al método del administrador de contexto __enter__() y almacene el resultado en monstruo.

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__.

Llame a manager.__exit__ para restaurar el stdout.write anterior.

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.

Gestores de Contexto y con Bloques | 453


Machine Translated by Google

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:

• Gestión de transacciones en el módulo sqlite3 ; ver “12.6.7.3. Usando la conexión


como gestor de contexto”.

• Retención de bloqueos, condiciones y semáforos en el código de subprocesamiento ; ver “17.1.10. Usando


bloqueos, condiciones y semáforos en la sentencia with ”.

• 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.

Las utilidades contextlib


Antes de implementar sus propias clases de administrador de contexto, eche un vistazo a "29.6 contextlib : utilidades para

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.

454 | Capítulo 15: Administradores de contexto y otros bloques


Machine Translated by Google

La más utilizada de estas utilidades es seguramente el decorador @contextmanager , por lo que


merece más atención. Ese decorador también es intrigante porque muestra un uso de la declaración
de rendimiento que no está relacionado con la iteración. Esto allana el camino hacia el concepto de
rutina, el tema del próximo capítulo.

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.

Ejemplo 15-5. mirror_gen.py: un administrador de contexto implementado con un generador


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 = reverse_write
rendimiento 'JABBERWOCKY'
sys.stdout.write = original_write

Aplique el decorador contextmanager .

Conservar el método sys.stdout.write original .


Definir la función personalizada reverse_write ; original_write estará disponible en el cierre.

Reemplace sys.stdout.write con reverse_write.

Proporcione el valor que se vinculará a la variable de destino en la cláusula as de la


instrucción with . Esta función se detiene en este punto mientras el cuerpo del con
ejecuta

Usando @contextmanager | 455


Machine Translated by Google

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 .

El ejemplo 15-6 muestra la función del espejo en funcionamiento.

Ejemplo 15-6. Pruebe la conducción de la función de administrador de contexto looking_glass

>>> from mirror_gen import looking_glass >>>


with looking_glass() as what: print('Alice, Kitty
... and Snowdrop') print(what)
...
...
adn de pordwons yttik , ecila
YKCOWREBAJ
>>> que
'JABBERWOCKY'

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

El método __enter__ de esa clase:

1. Invoca la función generadora y retiene el objeto generador, llamémoslo gen.

2. Llama a next(gen) para que se ejecute con la palabra clave yield .

3. Devuelve el valor producido por next(gen), por lo que puede vincularse a una variable de destino en el
formulario with/as.

Cuando el bloque with termina, el método __exit__ :

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.

2. De lo contrario, se llama a next(gen) , reanudando la ejecución de la función generador


cuerpo después del rendimiento.

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.

456 | Capítulo 15: Administradores de contexto y otros bloques


Machine Translated by Google

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.

Ejemplo 15-7. mirror_gen_exc.py: administrador de contexto basado en generador que implementa el


manejo de excepciones: el mismo comportamiento externo que el Ejemplo 15-3

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.

Manejar ZeroDivisionError configurando un mensaje de error.

Deshacer parches mono de sys.stdout.write.

Mostrar mensaje de error, si se configuró.

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.

Usando @contextmanager | 457


Machine Translated by Google

excepción en la función decorada si no desea que @contextmanager la suprima.


6

Tener un intento/finalmente (o un bloque with ) alrededor del rendimiento


es un precio inevitable de usar @contextmanager, porque nunca se
sabe qué van a hacer los usuarios de su administrador de contexto
dentro de su bloque with.7

Un ejemplo interesante de la vida real de @contextmanager fuera de la biblioteca estándar es el administrador de


contexto de reescritura de archivos en el lugar de Martijn Pieters . El ejemplo 15-8 muestra cómo se usa.

Ejemplo 15-8. Un administrador de contexto para reescribir archivos en su lugar

importar csv

with inplace(csvfilename, 'r', newline='') as (infh, outfh): lector =


csv.reader(infh) escritor = csv.writer(outfh)

para fila en lector:


fila += ['nuevo', 'columnas']
escritor.escritorfila(fila)

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!

458 | Capítulo 15: Administradores de contexto y otros bloques


Machine Translated by Google

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.

Resumen del capítulo


Este capítulo comenzó con bastante facilidad con la discusión de los bloques else en las sentencias for, while
y try . Una vez que te acostumbres al significado peculiar de la cláusula else en estas declaraciones, creo que
else puede aclarar tus intenciones.

Luego cubrimos los administradores de contexto y el significado de la declaración with , moviéndose


rápidamente más allá de su uso común para cerrar automáticamente los archivos abiertos. Implementamos un
administrador de contexto personalizado: la clase LookingGlass con los métodos __enter__/__exit__ , y vimos
cómo manejar las excepciones en el método __exit__ . Un punto clave que hizo Raymond Hettinger en su
discurso de apertura de PyCon US 2013 es que with no es solo para la administración de recursos, sino que
es una herramienta para factorizar el código de configuración y desmontaje común, o cualquier par de
operaciones que deban realizarse antes y después de otro. procedimiento (diapositiva 21, ¿Qué hace que
Python sea impresionante?).

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

Resumen del capítulo | 459


Machine Translated by Google

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.

460 | Capítulo 15: Administradores de contexto y otros bloques


Machine Translated by Google

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.

Lectura adicional | 461


Machine Translated by Google
Machine Translated by Google

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.

Cuando comienza a pensar en el rendimiento principalmente en términos de flujo de control, tiene la


mentalidad para comprender las 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:

• El comportamiento y los estados de un generador que opera como una corrutina •

Preparando una corrutina automáticamente con un decorador • Cómo la persona que

llama puede controlar una corrutina a través de .close() y .throw(…)


métodos del objeto generador

• Cómo las corrutinas pueden devolver valores al finalizar • Uso y

semántica del nuevo rendimiento a partir de la sintaxis • Un caso de uso:

corrutinas para administrar actividades concurrentes en una simulación

Cómo evolucionaron las corrutinas a partir de los generadores

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:

• Un generador ahora puede devolver un valor; previamente, dando un valor a la devolución


declaración dentro de un generador generó un SyntaxError.

• 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.

464 | Capítulo 16: Rutinas


Machine Translated by Google

Comportamiento básico de un generador utilizado como rutina

El ejemplo 16-1 ilustra el comportamiento de una rutina.

Ejemplo 16-1. La demostración más simple posible de coroutine en acción

>>> def rutina_simple(): # print('->


... rutina iniciada')
... x = rendimiento
... # print('-> rutina recibida:', x)
...
>>> mi_coro = rutina_simple()
>>> my_coro #
<objeto generador simple_coroutine en 0x100c2be10>
>>> next(my_coro) # ->
rutina iniciada
>>> my_coro.send(42) # ->
rutina recibida: 42
Rastreo (llamadas recientes más última): #
...
Detener iteración

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'

Esperando para iniciar la ejecución.

'GEN_EN EJECUCIÓN'

Actualmente siendo ejecutado por el interprete.1

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.

Comportamiento básico de un generador utilizado como rutina | 465


Machine Translated by Google

'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:

>>> mi_coro = rutina_simple() >>>


mi_coro.send(1729)
Rastreo (llamadas recientes más última):
Archivo "<stdin>", línea 1, en <módulo>
TypeError: no se puede enviar un valor que no sea Ninguno a un generador recién iniciado

Tenga en cuenta el mensaje de error: es bastante claro.

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.

Ejemplo 16-2. Una rutina que rinde el doble

>>> def simple_coro2(a):


... print('-> Iniciado: a =', a) b =
... arrojar a print('-> Recibido: b
... =', b) c = arrojar a + b print('->
... Recibido: c =', c)
...
...
>>> my_coro2 = simple_coro2(14)
>>> from inspect import getgeneratorstate >>>
getgeneratorstate(my_coro2)
'GEN_CREATED'
>>> siguiente(mi_coro2)
-> Iniciado: a = 14 14
>>>
getgeneratorstate(my_coro2)
'GEN_SUSPENDED'
>>> mi_coro2.send(28)
-> Recibido: b = 28 42

466 | Capítulo 16: Rutinas


Machine Translated by Google

>>> 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'

inspect.getgeneratorstate informa GEN_CREATED (es decir, la corrutina no ha


comenzado).

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

Envía el número 28 a la rutina suspendida; la expresión de rendimiento se evalúa como


28 y ese número está vinculado a b. Se muestra el mensaje -> Recibido: b = 28 , se arroja
el valor de a + b ( 42), y se suspende la rutina esperando que se asigne el valor a c.

Envía el número 99 a la rutina suspendida; la expresión de rendimiento se evalúa como


99 , el número está vinculado a c. Se muestra el mensaje -> Recibido: c = 99 , luego la
corrutina finaliza, lo que hace que el objeto generador genere StopIteration.
getgeneratorstate informa GEN_CLOSED (es decir, la ejecución de la rutina se ha
completado).

Es crucial comprender que la ejecución de la rutina se suspende exactamente en la palabra clave


yield . Como se mencionó anteriormente, en una declaración de asignación, el código a la
derecha de = se evalúa antes de que ocurra la asignación real. Esto significa que en una línea
como b = yield a, el valor de b solo se establecerá cuando el código del cliente active la rutina
más tarde. Se necesita algo de esfuerzo para acostumbrarse a este hecho, pero comprenderlo
es esencial para dar sentido al uso de yield en la programación asincrónica, como veremos más adelante.

La ejecución de la rutina simple_coro2 se puede dividir en tres fases, como se muestra en


Figura 16-1:

1. next(my_coro2) imprime el primer mensaje y se ejecuta para generar a, lo que genera el número

14. 2. my_coro2.send(28) asigna 28 a b, imprime el segundo mensaje y se ejecuta para generar a + b,


lo que genera el número 42.

3. my_coro2.send(99) asigna 99 a c, imprime el tercer mensaje y el terÿ


minas

Comportamiento básico de un generador utilizado como rutina | 467


Machine Translated by Google

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)

Ahora consideremos un ejemplo de rutina un poco más complicado.

Ejemplo: rutina para calcular un promedio móvil


Mientras discutíamos los cierres en el Capítulo 7, estudiamos objetos para calcular un promedio
móvil: el Ejemplo 7-8 muestra una clase simple y el Ejemplo 7-14 presenta una función de orden
superior que produce un cierre para mantener el total y contar variables entre invocaciones.
El ejemplo 16-3 muestra cómo hacer lo mismo con una rutina.2

Ejemplo 16-3. coroaverager0.py: código para una rutina promedio móvil


def promediador():
total = 0.0
conteo = 0

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.

468 | Capítulo 16: Rutinas


Machine Translated by Google

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_avg = promedio () >>>


siguiente(coro_avg) >>>
coro_avg.send(10) 10.0 >>>
coro_avg.send(30) 20.0

>>> coro_promedio.enviar(5)
15.0

Cree el objeto coroutine.

Prepáralo llamando a continuación.

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.

Decoradores para Coroutine Priming


No se puede hacer mucho con una rutina sin prepararla: siempre debemos recordar llamar a next(my_coro) antes de
my_coro.send(x). Para hacer que el uso de la rutina sea más conveÿ

Decoradores para imprimación de rutina | 469


Machine Translated by Google

No obstante, a veces se utiliza un decorador de imprimación. El decorador coroutine en el ejemplo 16-5


es un ejemplo.3

Ejemplo 16-5. coroutil.py: decorador para cebado de rutinas


desde functools importar envolturas

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

La función generadora decorada se sustituye por esta función de cebador que,


cuando se invoca, devuelve el generador cebado.

Llame a la función decorada para obtener un objeto generador.

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
"""

Una rutina para calcular un promedio móvil

>>> coro_avg = promediador() >>>


desde inspeccionar importar obtener estado del generador
>>> getgeneratorstate(coro_avg)
'GEN_SUSPENDIDO'
>>> coro_promedio.enviar(10)
10.0
>>> coro_promedio.send(30)
20.0
>>> coro_promedio.enviar(5)
15.0

"""

de coroutine de importación de coroutil

@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.

470 | Capítulo 16: Rutinas


Machine Translated by Google

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.

Importa el decorador de rutinas .

Aplicarlo a la función de promedio .

El cuerpo de la función es exactamente el mismo que el del ejemplo 16-3.

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

terminar y generar excepciones en ellas.

Terminación de rutinas y manejo de excepciones


Una excepción no controlada dentro de una corrutina se propaga a la persona que llama del siguiente envío
que la activó. El ejemplo 16-7 es un ejemplo que utiliza la rutina promediadora decorada del ejemplo 16-6.

Ejemplo 16-7. Cómo una excepción no controlada mata una rutina

>>> from coroaverager1 importar promedio >>>


coro_avg = promedior () >>> coro_avg.send(40) #

Terminación de rutinas y manejo de excepciones | 471


Machine Translated by Google

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

Archivo "<stdin>", línea 1, en <módulo>


Detener iteración

Usando el promediador decorado de @coroutine , podemos comenzar a enviar valores de


inmediato.

Enviar un valor no numérico provoca una excepción dentro de la rutina.

Debido a que la excepción no se manejó en la rutina, terminó. Cualquier intento de reactivarlo


generará StopIteration.

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.throw(exc_type[, exc_value[, traceback]])


Hace que la expresión de rendimiento donde se pausó el generador genere la excepción dada.
Si el generador maneja la excepción, el flujo avanza al siguiente rendimiento y el valor obtenido
se convierte en el valor de la llamada generator.throw . Si el generador no maneja la excepción,
se propaga al contexto de la persona que llama.

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.

472 | Capítulo 16: Rutinas


Machine Translated by Google

La documentación oficial de los métodos de objetos generadores se


encuentra profundamente en The Python Language Reference (ver 6.2.9.1.
métodos generador-iterador).

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

aumentar RuntimeError('Esta línea nunca debería ejecutarse.')

Manejo especial para DemoException.

Si no hay excepción, muestra el valor recibido.


Esta línea nunca se ejecutará.

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.

El funcionamiento normal de demo_exc_handling se muestra en el ejemplo 16-9.

Ejemplo 16-9. Activando y cerrando demo_exc_handling sin excepción


>>> exc_coro = demo_exc_handling() >>>
next(exc_coro) -> rutina iniciada >>>
exc_coro.send(11) -> rutina recibida: 11
>>> exc_coro.send(22) -> rutina recibida:
22 > >> exc_coro.close() >>> from
inspeccionar importar getgeneratorstate
>>> getgeneratorstate(exc_coro)

'GEN_CERRADO'

Terminación de rutinas y manejo de excepciones | 473


Machine Translated by Google

Si la excepción DemoException se lanza a la rutina, se maneja y la rutina demo_exc_handling


continúa, como en el Ejemplo 16-10.

Ejemplo 16-10. Lanzar DemoException en demo_exc_handling no lo rompe


>>> exc_coro = demo_exc_handling()
>>> next(exc_coro) -> rutina iniciada

>>> 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.

Ejemplo 16-11. Coroutine termina si no puede manejar una excepción lanzada en él


>>> exc_coro = demo_exc_handling()
>>> next(exc_coro) -> rutina iniciada >>>
exc_coro.send(11) -> rutina recibida: 11
>>> exc_coro.throw(ZeroDivisionError)

Rastreo (última llamada más reciente):


...
ZeroDivisionError
>>> getgeneratorstate(exc_coro)
'GEN_CERRADO'

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:

474 | Capítulo 16: Rutinas


Machine Translated by Google

print('-> rutina recibida: {!r}'.format(x))


finalmente:
print('-> final de rutina')

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.

Devolver un valor de una rutina


El ejemplo 16-13 muestra una variación de la rutina del promedio que devuelve un resultado. Por
razones didácticas, no arroja el promedio móvil con cada activación. Esto es para enfatizar que algunas
corrutinas no producen nada interesante, pero están diseñadas para devolver un valor al final, a
menudo como resultado de alguna acumulación.

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

desde colecciones importar namedtuple

Resultado = tupla nombrada('Resultado', 'conteo promedio')

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.

Devolver un valor desde una rutina | 475


Machine Translated by Google

Ejemplo 16-14. coroaverager2.py: doctest que muestra el comportamiento del promedio

>>> coro_avg = promediador()


>>> siguiente(coro_avg) >>>
coro_avg.send(10) >>>
coro_avg.send(30) >>>
coro_avg.send(6.5) >>>
coro_avg.send(Ninguno )
Rastreo (última llamada más reciente):
...
StopIteration: Resultado (recuento = 3, promedio = 15.5)

Esta versión no arroja valores.

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.

El ejemplo 16-15 muestra cómo recuperar el valor devuelto por la rutina.

Ejemplo 16-15. La captura de StopIteration nos permite obtener el valor devuelto por el promediador

>>> coro_avg = promediador()


>>> siguiente(coro_avg) >>>
coro_avg.send(10) >>>
coro_avg.send(30) >>>
coro_avg.send(6.5) >>>
prueba: coro_avg.send
... (Ninguno) ... excepto
StopIteration como exc: result =
... exc.value
...
>>> resultado
Resultado (recuento = 3, promedio = 15.5)

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

476 | Capítulo 16: Rutinas


Machine Translated by Google

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:

>>> def gen():


... para c en 'AB':
... produce c
... para i en el rango (1, 3):
... produce i
...
>>> lista(gen())
['A', 'B', 1, 2]
Se puede escribir como:

>>> def gen():


... rendimiento de
... 'AB' rendimiento de rango (1, 3)
...
>>> lista(gen())
['A', 'B', 1, 2]

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.

Usando el rendimiento de | 477


Machine Translated by Google

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

Ejemplo 16-16. Encadenamiento de iterables con rendimiento de

>>> cadena def (*iterables):


... para ello en iterables:
... rendimiento de ella
...
>>> s = 'ABC'
>>> t = tupla(rango(3)) >>>
lista(cadena(s, t))
['A', 'B', 'C', 0, 1, 2]

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

generadora que contiene el rendimiento de la expresión <iterable> .

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

478 | Capítulo 16: Rutinas


Machine Translated by Google

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

PEP 380 a menudo usa la palabra "iterador" para referirse al


subgenerador. Eso es confuso porque el generador de delegación
también es un iterador. Así que prefiero usar el término subgenerador,
de acuerdo con el título del PEP: “Sintaxis para delegar a un
subgenerador”. Sin embargo, el subgenerador puede ser un iterador
simple que implementa solo __next__, y yield from también puede
manejar eso, aunque se creó para admitir generadores que
implementan __next__, enviar, cerrar y lanzar.

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

7. La imagen de la figura 16-2 se inspiró en un diagrama de Paul Sokolovsky.

Usando el rendimiento de | 479


Machine Translated by Google

9 niños con un promedio de 1,39 m


10 niñas con un promedio de 42,04 kg
10 niñas con un promedio de 1,43 m

El código del ejemplo 16-17 ciertamente no es la solución más sencilla para el


problema, pero sirve para mostrar el rendimiento de la acción. Este ejemplo está inspirado en el
uno dado en What's New in Python 3.3.

Ejemplo 16-17. coroaverager3.py: usar el rendimiento de para generar un promedio e informar


Estadísticas

desde colecciones importar namedtuple

Resultado = tupla nombrada('Resultado', 'conteo promedio')

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

# el código del cliente, también conocido como la persona que llama

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!

# imprimir (resultados) # descomentar para depurar


informe (resultados)

# informe de salida

480 | Capítulo 16: Rutinas


Machine Translated by Google

def informe (resultados):


for key, result in sorted (results.items()): group, unit
= key.split(';') print('{:2} {:5} averaging {:.2f}
{ }'.formato(
result.count, group, result.average, unit))

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)

Misma rutina promediadora del ejemplo 16-13. Aquí está el subgenerador.

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 resultado devuelto será el valor del rendimiento de la expresión en el agrupador. grouper es

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.

Usando el rendimiento de | 481


Machine Translated by Google

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.

Si desea descubrir por sí mismo por qué no se recopilan resultados,


será una excelente manera de ejercitar su comprensión de cómo
funciona el rendimiento de . El código para coroaverager3.py está en el
repositorio de código de Fluent Python. La explicación es la siguiente.

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

482 | Capítulo 16: Rutinas


Machine Translated by Google

La conclusión clave de este experimento es: si un subgenerador


nunca termina, el generador delegado se suspenderá para siempre
en el rendimiento de. Esto no evitará que su programa progrese
porque el rendimiento de (como el rendimiento simple ) transfiere el
control al código del cliente (es decir, la persona que llama del
generador delegado). Pero sí significa que alguna tarea quedará 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í:

“Cuando el iterador es otro generador, el efecto es el mismo que si el cuerpo del


subgenerador estuviera alineado en el punto de la expresión yield from . Además, al
subgenerador se le permite ejecutar una declaración de retorno con un valor, y ese
valor se convierte en el valor de la expresión yield from .”8
Esas palabras tranquilizadoras ya no son parte del PEP, porque no cubren todos los casos de
esquina. Pero están bien como primera aproximación.

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

El Significado de rendimiento de | 483


Machine Translated by Google

• 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

generador delegado mediante send() se pasan directamente al subgenerador. Si el valor enviado


es Ninguno, se llama al método __next__() del subgenerador . Si el valor enviado no es
Ninguno, se llama al método send() del subgenerador . Si la llamada genera StopIteration, se
reanuda el generador de delegación. Cualquier otra excepción se propaga al generador
delegador.

• return expr en un generador (o subgenerador) hace que StopIteration(expr) se active al salir


del generador. • El valor de la expresión yield from es el primer argumento de StopItera

excepción de ción lanzada por el subgenerador cuando termina.

Las otras dos características de yield from tienen que ver con las excepciones y la terminación:

• Las excepciones que no sean GeneratorExit arrojadas al generador delegado se pasan al


método throw() del subgenerador. Si la llamada genera StopIteration, se reanuda el generador
de delegación. Cualquier otra excepción se propaga al generador de delegación. • Si se
genera una excepción GeneratorExit en el generador de delegación, o se llama al método

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.

Considere que el rendimiento de aparece en un generador de delegación. El código del cliente


impulsa el generador de delegación, que impulsa el subgenerador. Entonces, para simplificar la
lógica involucrada, supongamos que el cliente nunca llama a .throw(...) o .close() en el generador
delegador. Supongamos también que el subgenerador nunca genera una excepción hasta que
finaliza, cuando el intérprete genera StopIteration .

484 | Capítulo 16: Rutinas


Machine Translated by Google

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:

RESULTADO = rendimiento de EXPR

Trate de seguir la lógica del Ejemplo 16-18.

Ejemplo 16-18. Pseudocódigo simplificado equivalente a la instrucción RESULTADO = rendimiento


de EXPR en el generador de delegación (esto cubre el caso más simple: .throw(…)
y .close() no son compatibles; la única excepción manejada es StopIteration)

_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.

Si se generó StopIteration , extraiga el atributo de valor de la excepción y


asignarlo a _r: este es el RESULTADO en el caso más simple.

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.

Si el subgenerador generó StopIteration, obtenga el valor, asígnelo a _r y salga


el bucle, reanudando el generador delegado.

_r es el RESULTADO: el valor del rendimiento total de la expresión.

El Significado de rendimiento de | 485


Machine Translated by Google

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.

Para su comodidad, a continuación se muestra el pseudocódigo completo del rendimiento de la expansión de


PEP 380, resaltado en sintaxis y anotado. El ejemplo 16-19 se copió palabra por palabra; solo los números
destacados fueron agregados por mí.

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:

RESULTADO = rendimiento de EXPR

486 | Capítulo 16: Rutinas


Machine Translated by Google

Ejemplo 16-19. Pseudocódigo equivalente a la declaración RESULTADO = rendimiento de EXPR en


el generador delegador
_i = iter(EXPR)
prueba:
_y = siguiente(_i)
excepto StopIteration como _e:
_r = _e.valor más:

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.

Si se generó StopIteration , extraiga el atributo de valor de la excepción y


asignarlo a _r: este es el RESULTADO en el caso más simple.

El Significado de rendimiento de | 487


Machine Translated by Google

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.

Se trata de cerrar el generador delegador y el subgenerador. Debido a que el subgenerador puede


ser cualquier iterador, es posible que no tenga un método de cierre .

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.

Si no se recibió ninguna excepción al ceder…

Intenta hacer avanzar el subgenerador…

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.

488 | Capítulo 16: Rutinas


Machine Translated by Google

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.

Ahora pasaremos a un ejemplo clásico del uso de rutinas: simulaciones de programación.


Este ejemplo no muestra el rendimiento, pero revela cómo se usan las corrutinas para administrar
actividades simultáneas en un solo subproceso.

Caso de uso: corrutinas para simulación de eventos discretos


Las corrutinas son una forma natural de expresar muchos algoritmos, como simulaciones,
juegos, E/S asíncrona y otras formas de programación dirigida por eventos o multitarea
cooperativa.10
— Guido van Rossum y Phillip J. Eby PEP
342—Corrutinas a través de generadores mejorados

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.

La motivación para el siguiente ejemplo de simulación no es


académica. Las rutinas son el bloque de construcción fundamental
del paquete asyncio . Una simulación muestra cómo implementar
actividades concurrentes usando corrutinas en lugar de subprocesos,
y esto será de gran ayuda cuando abordemos asyncio with en el Capítulo 18.

Antes de entrar en el ejemplo, unas palabras sobre las simulaciones.

Acerca de las simulaciones de eventos discretos

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

10. Frase inicial de la sección “Motivación” en PEP 342.

Caso de uso: corrutinas para simulación de eventos discretos | 489


Machine Translated by Google

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.

En el campo de la simulación, el término proceso se refiere a las actividades de una


entidad en el modelo, y no a un proceso del sistema operativo. Un proceso de
simulación puede implementarse como un proceso de sistema operativo, pero
generalmente se usa un subproceso o una corrutina para ese propósito.

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.

La simulación de la flota de taxis

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.

490 | Capítulo 16: Rutinas


Machine Translated by Google

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.

Caso de uso: corrutinas para simulación de eventos discretos | 491


Machine Translated by Google

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.

Otras cosas a tener en cuenta sobre la Figura 16-3:

• Cada taxi sale del garaje 5 minutos después que el otro. • El taxi 0

tardó 2 minutos en recoger al primer pasajero en el tiempo=2; 3 minutos para


taxi 1 (tiempo=8), y 5 minutos para el taxi 2 (tiempo=15).

• 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

de 180 minutos; el último evento fue en el tiempo = 110.

La simulación también puede terminar con eventos pendientes. Cuando eso sucede, el mensaje final dice así:

*** fin del tiempo de simulación : 3 eventos pendientes ***

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í:

Evento = colecciones.namedtuple('Evento', 'acción de proceso de tiempo')

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.

Revisemos taxi_process jugada por jugada en el ejemplo 16-20.

13. Yo era el pasajero. Me di cuenta de que olvidé mi billetera.

492 | Capítulo 16: Rutinas


Machine Translated by Google

Ejemplo 16-20. taxi_sim.py: corrutina taxi_process que implementa las actividades de


cada taxi

def taxi_process(ident, viajes, start_time=0):


"""Rendimiento al evento de emisión del simulador en cada cambio de estado"""
hora = producir evento (hora de inicio, ident, 'salir del garaje') para i
en el rango (viajes): hora = producir evento (hora, ident, 'recoger
pasajero') hora = producir evento (hora, ident, 'dejar pasajero ')

yield Event(time, ident, 'going home') # fin del


proceso de taxi

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 primer Evento producido es 'salir del garaje'. Esto suspende la rutina, y


permite que el bucle principal de la simulación pase al siguiente evento programado. cuando es
tiempo para reactivar este proceso, el bucle principal enviará la simulación actual
tiempo, que se asigna al tiempo.

Este bloque se repetirá una vez por cada viaje.

Se produce un evento de señalización de recogida de pasajeros. La rutina se detiene aquí.


Cuando llegue el momento de reactivar esta rutina, el bucle principal volverá a enviar
la hora actual

Se produce un Evento que indica la bajada de pasajeros. La corrutina está suspendida.


de nuevo, esperando que el bucle principal le envíe la hora de reactivación.

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á.

Cuando la corrutina cae al final, el objeto generador genera StopIteration.

Puede “conducir” un taxi usted mismo llamando a taxi_process en la consola de Python.14


El ejemplo 16-21 muestra cómo.

Ejemplo 16-21. Conducir la rutina taxi_process


>>> desde taxi_sim importar taxi_process
>>> taxi = proceso_taxi(ident=13, viajes=2, hora_inicio=0) >>> siguiente(taxi)

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.

Caso de uso: corrutinas para simulación de eventos discretos | 493


Machine Translated by Google

Evento (tiempo = 0, proceso = 13, acción = 'salir del garaje')


>>> taxi.enviar(_.tiempo + 7)
Event(time=7, proc=13, action='recoger pasajero') >>>
taxi.send(_.time + 23)
Evento (hora = 30, proc = 13, acción = 'dejar al pasajero')
>>> taxi.enviar(_.tiempo + 5)
Evento(tiempo=35, proc=13, acción='recoger pasajero')
>>> taxi.enviar(_.tiempo + 48)
Evento (tiempo = 83, proc = 13, acción = 'dejar pasajero')
>>> taxi.enviar(_.tiempo + 1)
Evento(tiempo=84, proc=13, acción='ir a casa') >>>
taxi.send(_.tiempo + 10)
Rastreo (llamadas recientes más última):
Archivo "<stdin>", línea 1, en <módulo>
Detener iteración

Cree un objeto generador para representar un taxi con ident=13 que hará dos
viajes y comienza a trabajar en t=0.

Prepare la rutina; produce el evento inicial.

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.

Esto lo produce el bucle for al comienzo del primer viaje.

Enviar _.time + 23 significa que el viaje con el primer pasajero durará 23 minutos.

Luego el taxi merodeará durante 5 minutos.

El último viaje tomará 48 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.

Para instanciar la clase Simulator , la función principal de taxi_sim.py construye un taxis


diccionario como este:

taxis = {i: proceso_taxi(i, (i + 1) * 2, i * INTERVALO_SALIDA)


para i en el rango (num_taxis)}
sim = Simulador (taxis)

494 | Capítulo 16: Rutinas


Machine Translated by Google

DEPARTURE_INTERVAL es 5; si num_taxis es 3 como en la muestra, las líneas anteriores


harán lo mismo que:

taxis = {0: proceso_taxi(ident=0, viajes=2, hora_inicio=0), 1:


proceso_taxi(ident=1, viajes=4, hora_inicio=5), 2:
proceso_taxi(ident=2, viajes=6, hora_inicio =10)} sim =
Simulador(taxis)

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 .

El método Simulator.__init__ se muestra en el ejemplo 16-22. Las principales estructuras de datos de


Simulator son:

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.

Ejemplo 16-22. taxi_sim.py: Inicializador de clase de simulador


Simulador de clase :

def __init__(self, procs_map):


self.events = cola.PriorityQueue() self.procs
= dict(procs_map)

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:

Evento(tiempo=14, proc=0, acción='recoger pasajero')


Evento(tiempo=11, proc=1, acción='recoger pasajero')

Caso de uso: corrutinas para simulación de eventos discretos | 495


Machine Translated by Google

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').

Ahora estudiemos el algoritmo principal de la simulación, el método Simulator.run . Es invocado por


la función principal justo después de que se instancia el Simulador , así:

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:

1. Recorra los procesos que representan taxis.

una. Prepare la rutina para cada taxi llamando a next() en él. Esto producirá la primera
Evento para cada taxi.

b. Coloque cada evento en la cola de autoeventos del Simulador.

2. Ejecute el bucle principal de la simulación mientras sim_time < end_time.

una. Compruebe si self.events está vacío; si es así, sal del bucle. b.

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.

Actualice el tiempo de simulación con el atributo de tiempo de current_event. mi. Envía la

hora a la rutina identificada por el atributo proc de la cur


alquiler_evento. La corrutina producirá el next_event. F.

Programe next_event agregándolo a la cola self.events .

La clase Simulator completa es el Ejemplo 16-23.

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 :

def __init__(self, procs_map):


self.events = cola.PriorityQueue() self.procs
= dict(procs_map)

def ejecutar(self, end_time):


"""Programar y mostrar eventos hasta que se acabe el tiempo"""
# programar el primer evento para cada cabina para
_, proc en sorted(self.procs.items()): first_event
= next(proc)

496 | Capítulo 16: Rutinas


Machine Translated by Google

self.events.put(primer_evento)

# bucle principal de la simulación


sim_time = 0
while sim_time < end_time: if
self.events.empty(): print('***
fin de eventos ***')
descanso

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

La simulación end_time es el único argumento requerido para la ejecución.

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

Zero sim_time, el reloj de simulación.

Bucle principal de la simulación: ejecutar mientras sim_time es menor que end_time.

El bucle principal también puede salir si no hay eventos pendientes en la cola.

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

Visualice el Evento, identificando el taxi y agregando sangría de acuerdo con el


identificación de taxi

Recupere la rutina para el taxi activo del diccionario self.procs .

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.

Caso de uso: corrutinas para simulación de eventos discretos | 497


Machine Translated by Google

Calcule el próximo tiempo de activación agregando sim_time y el resultado de llamar a


compute_duration(...) con la acción anterior (por ejemplo, 'recoger pasajero', 'dejar
pasajero', etc.)

Envía la hora a la rutina del taxi. La corrutina producirá el next_event o generará


StopIteration cuando finalice.

Si se genera StopIteration , elimine la rutina del diccionario self.procs .

De lo contrario, ponga el next_event en la cola.


Si el bucle sale porque pasó el tiempo de simulación, muestra el número de eventos
pendientes (que puede ser cero por coincidencia, a veces).

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.

Resumen del capítulo


Guido van Rossum escribió que hay tres estilos diferentes de código que puedes escribir usando
generadores:

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".

498 | Capítulo 16: Rutinas


Machine Translated by Google

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

Resumen del capítulo | 499


Machine Translated by Google

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.

500 | Capítulo 16: Rutinas


Machine Translated by Google

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ó:

Si diferentes personas encontrarán o no código usando yield de difícil de entender o no,


tendrá más que ver con su comprensión de los conceptos de multitarea cooperativa en
general más que con el engaño subyacente involucrado en permitir generadores
verdaderamente anidados.

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.

Lectura adicional | 501


Machine Translated by Google

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.

Tabla 16-1. Número de palabras clave en lenguajes de programación

Palabras clave Idioma Comentario

5 Smalltalk-80 Famoso por su sintaxis minimalista.

25 Vamos
El lenguaje, no el juego.

32 C Eso es ANSI C. C99 tiene 37 palabras clave, C11 tiene 44.

33 Pitón Python 2.7 tiene 31 palabras clave; Python 1.5 tenía 28.

502 | Capítulo 16: Rutinas


Machine Translated by Google

Palabras clave Idioma Comentario

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.

sesenta y cinco PHP


Desde PHP 5.3, se introdujeron siete palabras clave, incluidas goto, trait y yield.

85 C++ Según cppreference.com, C++11 agregó 10 palabras clave a las 75 existentes.

555 COBOL Yo no inventé esto. Consulte este manual de IBM ILE COBOL.

ÿ Esquema Cualquiera puede definir nuevas palabras clave.

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

Python es como Chess y Scheme es como Go (el juego).

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 manifestación más grave de este problema es la sobrecarga de def: ahora se usa


para definir funciones, generadores y rutinas, objetos que son demasiado diferentes para compartir
la misma sintaxis de declaración.19

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.

Noticias de última hora

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.

Lectura adicional | 503


Machine Translated by Google

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.

504 | Capítulo 16: Rutinas


Machine Translated by Google

CAPÍTULO 17

Concurrencia con Futuros

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.

Comenzaremos con un ejemplo motivador.

Ejemplo: descargas web en tres estilos


Para manejar la E/S de la red de manera eficiente, necesita simultaneidad, ya que implica una alta
latencia, por lo que en lugar de desperdiciar ciclos de CPU en espera, es mejor hacer otra cosa
hasta que llegue una respuesta de la red.

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

20 indicadores descargados en 7,09


s $ python3 flags_threadpool.py DE
BD CN JP ID EG NG BR RU CD IR MX US PH FR PK VN IN ET TR 20
indicadores descargados en 1,37 s $ python3 flags_threadpool.py

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

20 banderas descargadas en 1,27 s


$ python3 flags_asyncio.py RU IN
ID DE BR VN PK MX US IR ET EG NG BD FR CN JP PH CD TR 20 banderas
descargadas en 1,42 s

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.

Flags.py tardó un promedio de 7,18 s en descargar 20 imágenes.

El promedio de flags_threadpool.py fue de 1,40 s.

Para flags_asincio.py, 1,35 fue el tiempo promedio.

506 | Capítulo 17: Concurrencia con futuros


Machine Translated by Google

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.

Mientras prueba clientes HTTP simultáneos en la web pública, es posible


que, sin darse cuenta, lance un ataque de denegación de servicio (DoS) o
que se sospeche que lo está haciendo. En el caso del Ejemplo 17-1, está
bien hacerlo porque esos scripts están codificados para realizar solo 20
solicitudes. Para probar clientes HTTP no triviales, debe configurar su
propio servidor de prueba. El archivo 17-futures/ countries/ README.rst en
el repositorio de GitHub del código Fluent Python tiene instrucciones para
configurar un servidor Nginx local.

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.

Un ejemplo de secuencia de comandos de

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.

Para mayor claridad, no hay manejo de errores en el ejemplo 17-2.


Trataremos las excepciones más adelante, pero aquí queremos centrarnos
en la estructura básica del código, para que sea más fácil contrastar este
script con los concurrentes.

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

Ejemplo: Descargas web en tres estilos | 507


Machine Translated by Google

'
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 save_flag(img, nombre de archivo):


ruta = os.path.join(DEST_DIR, nombre de archivo)
con abierto (ruta, 'wb') como fp:
fp.escribir(img)

def get_flag(cc): url =


'{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower())
resp = solicitudes.get(url)
devolver contenido resp.

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)

Importar la biblioteca de solicitudes ; no es parte de la biblioteca estándar, así que por


por convención, lo importamos después de los módulos de biblioteca estándar os, time y sys,
y sepárelo de ellos con una línea en blanco.
Lista de los códigos de país ISO 3166 para los 20 países más poblados en orden
de población decreciente.

508 | Capítulo 17: Concurrencia con futuros


Machine Translated by Google

El sitio web con las imágenes de la bandera. 2

Directorio local donde se guardan las imágenes.

Simplemente guarde el img (una secuencia de bytes) en el nombre de archivo en DEST_DIR.

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.

download_many es la función clave para comparar con las implementaciones concurrentes.

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.

main registra e informa el tiempo transcurrido después de ejecutar


download_many. main debe llamarse con la función que hará las descargas; pasamos
la función download_many como argumento para que main se pueda usar como una
función de biblioteca con otras implementaciones de download_many en los siguientes ejemplos.

La biblioteca de solicitudes de Kenneth Reitz está disponible en PyPI y


es más poderosa y fácil de usar que el módulo urllib.request de la
biblioteca estándar de Python 3. De hecho, Requests se considera un
modelo de API Pythonic. También es compatible con Python 2.6 y
versiones posteriores, mientras que urllib2 de Python 2 se movió y
cambió de nombre en Python 3, por lo que es más conveniente usar
solicitudes independientemente de la versión de Python a la que se dirige.

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.

Descarga con concurrent.futures Las características

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.

Ejemplo: Descargas web en tres estilos | 509


Machine Translated by Google

ejecutado. Pero la interfaz es de muy alto nivel y no necesitamos conocer nada de


esos detalles para un caso de uso simple como nuestras descargas de banderas.

El ejemplo 17-3 muestra la forma más fácil de implementar las descargas al mismo tiempo, utilizando
el método ThreadPoolExecutor.map .

Ejemplo 17-3. flags_threadpool.py: secuencia de comandos de descarga encadenada usando futures.Threadÿ


PoolExecutor

de futuros de importación concurrentes

from flags import save_flag, get_flag, show, main

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)

Reutilice algunas funciones del módulo de banderas (Ejemplo 17-2).


Número máximo de subprocesos que se utilizarán en ThreadPoolExecutor.

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.

Cree una instancia de ThreadPoolExecutor con esa cantidad de subprocesos de trabajo; la


El método executor.__exit__ llamará a executor.shutdown(wait=True), que
se bloqueará hasta que todos los subprocesos estén terminados.

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.

510 | Capítulo 17: Concurrencia con futuros


Machine Translated by Google

Devolver el número de resultados obtenidos; si alguna de las llamadas encadenadas generara


una excepción, esa excepción se generaría aquí cuando la llamada next() implícita intentara
recuperar el valor de retorno correspondiente del iterador.

Llame a la función principal desde el módulo de banderas , pasando la versión mejorada de


download_many.

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.

¿Dónde están los futuros?

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.

Ejemplo: Descargas web en tres estilos | 511


Machine Translated by Google

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.

El uso de futures.as_completed requiere cambios solo en la función download_many .


La llamada executor.map de nivel superior se reemplaza por dos bucles for : uno para crear y
programar los futuros, el otro para recuperar sus resultados. Mientras estamos en eso, agregaremos
algunas llamadas de impresión para mostrar cada futuro antes y después de que esté listo. El ejemplo
17-4 muestra el código para una nueva función download_many . El código para download_many
creció de 5 a 17 líneas, pero ahora podemos inspeccionar los futuros misteriosos. Las funciones
restantes son las mismas que en el ejemplo 17-3.

Ejemplo 17-4. flags_threadpool_ac.py: reemplazando executor.map con executor.submit y


futures.as_completed en la función download_many
def download_many(cc_list):
cc_list = cc_list[:5] with
futures.ThreadPoolExecutor(max_workers=3) as ejecutor: to_do = [] for
cc in sorted(cc_list): future = executor.submit(download_one, cc)
to_do.append (futuro)

512 | Capítulo 17: Concurrencia con futuros


Machine Translated by Google

mensaje = 'Programado para {}: {}'


imprimir(mensaje.formato(cc, futuro))

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.

Codifique max_workers a 3 para que podamos observar futuros pendientes en la salida.

Iterar los códigos de países alfabéticamente, para dejar claro que los resultados llegan
de orden.

executor.submit programa la ejecución del invocable y devuelve un futuro


que representa esta operación pendiente.

Almacene cada futuro para que luego podamos recuperarlos con as_completed.

Muestra un mensaje con el código del país y el futuro respectivo.

as_completed produce futuros a medida que se completan.


Obtener el resultado de este futuro.

Mostrar el futuro y su resultado.

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.

Ejemplo 17-5. Salida de flags_threadpool_ac.py

$ 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'

5 banderas descargadas en 0.70s

Ejemplo: Descargas web en tres estilos | 513


Machine Translated by Google

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.

Los dos últimos futuros están pendientes, esperando subprocesos de trabajo.

El primer CN aquí es la salida de download_one en un subproceso de trabajo; el resto de la línea es la salida


de download_many.

Aquí, dos hilos emiten códigos antes de que download_many en el hilo principal pueda mostrar el resultado
del primer hilo.

Si ejecuta flags_threadpool_ac.py varias veces, verá que varía el orden


de los resultados. Aumentar el argumento max_workers a 5 aumentará
la variación en el orden de los resultados.
Disminuirlo a 1 hará que este código se ejecute secuencialmente, y el
orden de los resultados siempre será el orden de las llamadas de envío .

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?

Responderé a la segunda pregunta en “Correr en círculos y bloquear llamadas” en la página 552.

Continúe leyendo para comprender por qué GIL es casi inofensivo con el procesamiento vinculado a E/S.

514 | Capítulo 17: Concurrencia con futuros


Machine Translated by Google

Bloqueo de E/S y GIL


El intérprete de CPython no es seguro para subprocesos internamente, por lo que tiene un bloqueo de intérprete
global (GIL), que permite que solo un subproceso a la vez ejecute códigos de bytes de Python. Es por eso que un
solo proceso de Python generalmente no puede usar múltiples núcleos de CPU al mismo tiempo.3 Cuando

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

Cada función de E/S de bloqueo en la biblioteca estándar de Python libera el


GIL, lo que permite que se ejecuten otros subprocesos. La función time.sleep()
también libera el GIL. Por lo tanto, los subprocesos de Python se pueden usar
perfectamente en aplicaciones vinculadas a E/S, a pesar de la GIL.

Ahora echemos un breve vistazo a una forma sencilla de solucionar el GIL para trabajos vinculados a la CPU
utilizando concurrent.futures.

Lanzamiento de Procesos con concurrent.futures


La página de documentación de concurrent.futures tiene el subtítulo "Lanzamiento de tareas paralelas".
El paquete permite cálculos verdaderamente paralelos porque admite la distribución del trabajo entre varios procesos
de Python utilizando la clase ProcessPoolExecutor , lo que evita el GIL y aprovecha todos los núcleos de CPU
disponibles, si necesita realizar un procesamiento vinculado a la CPU.

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.

4. Diapositiva 106 de “Generadores: La última frontera”.

Bloqueo de E/S y GIL | 515


Machine Translated by Google

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

516 | Capítulo 17: Concurrencia con futuros


Machine Translated by Google

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

Trabajadores RC4 tiempo RC4 factor SHA tiempo SHA factor

1 11.48s 1.00x 22.66s 1.00x

2 8.65s 1.33x 14.90 1.52x

3 6.04s 1,90x 11.91 s 1,90x

4 5,58 s 2.06x 10.89s 2.08x

En resumen, para los algoritmos criptográficos, puede esperar duplicar el rendimiento


al generar cuatro procesos de trabajo con un ProcessPoolExecutor, si tiene cuatro CPU
núcleos

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.

Si está haciendo un trabajo intensivo de CPU en Python, debe intentar


PyPy. El ejemplo arcfour_futures.py se ejecutó de 3,8 a 5,1 veces
más rápido usando PyPy, dependiendo de la cantidad de trabajadores utilizados. yo
probado con PyPy 2.4.0, que es compatible con Python 3.2.5, por lo que
tiene concurrent.futures en la biblioteca estándar.

Ahora investiguemos el comportamiento de un grupo de subprocesos con un programa de demostración que


lanza un grupo con tres trabajadores, ejecutando cinco invocables que generan una marca de tiempo
mensajes

Experimentando con Executor.map


La forma más sencilla de ejecutar varios invocables al mismo tiempo es con la función Executor.map .
ción que vimos por primera vez en el ejemplo 17-3. El ejemplo 17-6 es un script para demostrar cómo Execu
tor.map funciona con cierto detalle. Su salida aparece en el ejemplo 17-7.

Ejemplo 17-6. demo_executor_map.py: Demostración simple del método map de


ThreadPoolExecutor

desde el tiempo de importación del sueño, strftime


de futuros de importación concurrentes

def mostrar(*argumentos):
imprimir(strftime('[%H:%M:%S]'), end=' ')
imprimir(*argumentos)

Experimentando con Executor.map | 517


Machine Translated by Google

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.

merodeo regresa m * 10 para que podamos ver cómo recopilar resultados.

Cree un ThreadPoolExecutor con tres subprocesos.

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.

Muestre inmediatamente los resultados de la invocación de executor.map: es un generador, como muestra


el resultado del Ejemplo 17-7 .

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.

El ejemplo 17-7 muestra una ejecución de muestra del ejemplo 17-6.

518 | Capítulo 17: Concurrencia con futuros


Machine Translated by Google

Ejemplo 17-7. Ejemplo de ejecución de demo_executor_map.py del Ejemplo 17-6

$ 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

[15:56:53] merodear(3): hecho.


[15:56:53] resultado 3: 30
[15:56:55] merodear(4): hecho.
[15:56:55] resultado 4: 40

Esta carrera comenzó a las 15:56:50.

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

loiter(1) y loiter(2) comienzan inmediatamente (porque el grupo de subprocesos tiene tres


trabajadores, puede ejecutar tres funciones al mismo tiempo).

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

Aquí es donde la ejecución puede bloquearse, dependiendo de los parámetros dados al


merodeo llamadas: el método __next__ del generador de resultados debe esperar hasta que el
el primer futuro está completo. En este caso, no se bloqueará porque la llamada a loi
ter(0) terminó antes de que comenzara este bucle. Tenga en cuenta que todo hasta este punto
sucedió dentro del mismo segundo: 15:56:50.

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.

Experimentando con Executor.map | 519


Machine Translated by Google

Se muestra el resultado de loiter(1) : 10. Ahora el bucle for se bloqueará esperando el


resultado de loiter(2).

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.

La combinación de executor.submit y futures.as_comple ted es


más flexible que executor.map porque puede enviar diferentes
argumentos e invocables, mientras que executor.map está
diseñado para ejecutar el mismo invocable en diferentes
argumentos. Además, el conjunto de futuros que pasa a
futures.as_completed puede provenir de más de un ejecutor, tal
vez algunos fueron creados por una instancia de ThreadPoolExecutor
mientras que otros son de un Proc essPoolExecutor.

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.

Descargas con visualización de progreso y manejo de errores


Como se mencionó, las secuencias de comandos en “Ejemplo: descargas web en tres estilos” en la
página 505 no tienen manejo de errores para que sean más fáciles de leer y para contrastar la
estructura de los tres enfoques: secuencial, subproceso y asíncrono.

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

520 | Capítulo 17: Concurrencia con futuros


Machine Translated by Google

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.

Tenga cuidado al probar clientes


simultáneos Al probar clientes HTTP simultáneos en servidores HTTP
públicos, puede generar muchas solicitudes por segundo, y así es como
se realizan los ataques de denegación de servicio (DoS). No queremos
atacar a nadie, solo aprender a construir clientes de alto rendimiento.
Acelere con cuidado a sus clientes cuando acceda a servidores públicos.
Para experimentos de alta simultaneidad, configure un servidor HTTP local para realizar pruebas.
Las instrucciones para hacerlo se encuentran en el archivo README.rst en
el directorio 17-futures/ countries/ del repositorio de código de Fluent Python .

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.

Descargas con visualización de progreso y manejo de errores | 521


Machine Translated by Google

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.

Ejemplo 17-8. Pantalla de ayuda para los scripts de la serie flags2

$ python3 flags2_threadpool.py -h uso:


flags2_threadpool.py [-h] [-a] [-e] [-l N] [-m CONCURRENT] [-s LABEL]
[-v]
[CC [CC...]]

522 | Capítulo 17: Concurrencia con futuros


Machine Translated by Google

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)

argumentos opcionales: -h,


--help -a, --all -e, --every mostrar este mensaje de ayuda y salir obtener
-l N, --limit N -m todos los indicadores disponibles (AD a ZW) obtener
CONCURRENT, -- indicadores para cada código posible (AA...ZZ) limitar a N
max_req CONCURRENT primeros códigos

máximo de solicitudes simultáneas (predeterminado=30)


-s ETIQUETA, --servidor ETIQUETA
Servidor para golpear; uno de DEMORA, ERROR, LOCAL, REMOTO
(predeterminado = LOCAL) genera información de progreso detallada
-v, --verbose

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.

Descargas con visualización de progreso y manejo de errores | 523


Machine Translated by Google

ERROR Usar http://localhost:8003/flags; en el puerto 8003 se debe instalar un proxy que


introduzca errores HTTP y retrase las respuestas. Utilicé una configuración diferente
de Vaurien para esto.

La opción LOCAL solo funciona si configura e inicia un servidor


HTTP local en el puerto 8001. Las opciones DELAY y ERROR
requieren proxies escuchando en los puertos 8002 y 8003. La
configuración de Nginx y Mozilla Vaurien para habilitar estas
opciones se explica en 17- futuros/ países / README.rst en el
repositorio de código de Fluent Python en GitHub.

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

$ python3 flags2_secuencial.py Sitio


LOCAL: http://localhost:8001/flags Búsqueda de 20
flags: de BD a VN Se utilizará 1 conexión concurrente.

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

$ python3 flags2_threadpool.py -s DELAY abc DELAY sitio:


http://localhost:8002/flags Búsqueda de 78 banderas: de AA
a CZ Se utilizarán 30 conexiones simultáneas.

--------------------
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.

524 | Capítulo 17: Concurrencia con futuros


Machine Translated by Google

Ejemplo 17-11. Ejecute flags2_asyncio.py para obtener 100 indicadores (-al 100) del servidor ERROR,
utilizando 100 solicitudes simultáneas (-m 100)

$ python3 flags2_asyncio.py -s ERROR -al 100 -m 100 ERROR sitio:


http://localhost:8003/flags Búsqueda de 100 banderas: de AD a LK
Se utilizarán 100 conexiones simultáneas.

--------------------
73 banderas descargadas.
27 errores.
Tiempo transcurrido: 0,64 s

Esa es la interfaz de usuario de los ejemplos flags2 . Veamos cómo se implementan.

Manejo de errores en los ejemplos flags2 La estrategia

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 .

Ejemplo 17-12. flags2_secuencial.py: funciones básicas encargadas de la descarga; ambos se reutilizan


en flags2_threadpool.py

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.

def descargar_uno(cc, base_url, detallado=Falso):

intente: imagen = get_flag (base_url, cc)


excepto solicitudes.excepciones.HTTPError como exc:
res = exc.response si
res.status_code == 404: estado =
HTTPStatus.not_found msg = 'no
encontrado'
más:
elevar
else:
save_flag(imagen, cc.inferior() + '.gif') estado =
HTTPStatus.ok
mensaje = 'OK'

Descargas con visualización de progreso y manejo de errores | 525


Machine Translated by Google

si detallado:
imprimir (cc, msg)

resultado devuelto (estado, cc)

get_flag no maneja errores, usa solicitudes.Respuesta.raise_for_sta


tus para generar una excepción para cualquier código HTTP que no sea 200.

download_one detecta solicitudes.excepciones.HTTPError para manejar el código HTTP


404 específicamente...

…estableciendo su estado local en HTTPStatus.not_found; HTTPStatus es una enumeración


importado de flags2_common (Ejemplo A-10).

Cualquier otra excepción de HTTPError se vuelve a generar; otras excepciones simplemente se propagarán
a la persona que llama

Si se establece la opción de línea de comandos -v/--verbose , el código de país y el estado


se mostrará el mensaje; así verá el progreso en el modo detallado.

El resultado namedtuple devuelto por download_one tendrá un campo de estado con


un valor de HTTPStatus.not_found o HTTPStatus.ok.

El ejemplo 17-13 enumera la versión secuencial de la función download_many . este codigo es


sencillo, pero vale la pena estudiarlo para contrastarlo con las versiones concurrentes que vienen
arriba. Concéntrese en cómo informa el progreso, maneja los errores y cuenta las descargas.

Ejemplo 17-13. flags2_secuencial.py: la implementación secuencial de downÿ


carga_muchos

def download_many(cc_list, base_url, detallado, max_req):


contador = colecciones.Contador()
cc_iter = ordenado(cc_list) si no es
detallado:
cc_iter = tqdm.tqdm(cc_iter) para cc
en cc_iter: prueba:

res = descargar_uno(cc, base_url, detallado)


excepto solicitudes.excepciones.HTTPError como exc:
error_msg = 'Error HTTP {res.status_code} - {res.reason}'
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
counter[status] += 1 si es
detallado y error_msg:

526 | Capítulo 17: Concurrencia con futuros


Machine Translated by Google

imprimir ('*** Error para {}: {}'. formato (cc, error_msg))

contador de retorno

Este contador contará los diferentes resultados de descarga: HTTPStatus.ok,


HTTPStatus.not_found o HTTPStatus.error. cc_iter contiene la lista de los códigos
de países recibidos como argumentos, ordenados alfabéticamente.

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.

Este bucle for itera sobre cc_iter y… …realiza la

descarga mediante llamadas sucesivas a download_one.

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 no se escapó ninguna excepción de download_one, entonces el estado se recupera de la tupla


HTTPStatus con nombre devuelta por download_one.

Si hubo un error, establezca el estado local en consecuencia.

Incremente el contador utilizando el valor de HTTPStatus Enum como clave.

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.

Ahora estudiaremos el ejemplo del grupo de subprocesos refactorizado, flags2_threadpool.py.

Uso de futures.as_completed Para integrar

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 .

Ejemplo 17-14. flags2_threadpool.py: listado completo

importar colecciones
de futuros de importación concurrentes

Descargas con visualización de progreso y manejo de errores | 527


Machine Translated by Google

solicitudes de importación
importar tqdm

desde flags2_importación común principal ,


HTTPStatus desde flags2_importación secuencial download_one

DEFAULT_CONCUR_REQ = 30
MAX_CONCUR_REQ = 1000

def download_many(cc_list, base_url, detallado, concur_req):


contador = colecciones.Contador()
con futuros.ThreadPoolExecutor(max_workers=concur_req) como ejecutor: to_do_map
= {} for cc in sorted(cc_list): future = executor.submit(download_one,

cc, base_url, verbose)


to_do_map[future] = cc done_iter =
futures.as_completed(to_do_map) si no es detallado:

done_iter = tqdm.tqdm(done_iter, total=len(cc_list)) para el futuro


en done_iter: pruebe:

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)

Importe la biblioteca de visualización de la barra de progreso.

Importe una función y una enumeración del módulo flags2_common .


Reutilice el download_one de flags2_secuencial (Ejemplo 17-12).

528 | Capítulo 17: Concurrencia con futuros


Machine Translated by Google

Si no se proporciona la opción de línea de comandos -m/--max_req , esta será la cantidad máxima


de solicitudes simultáneas, implementadas como el tamaño del grupo de subprocesos; el número
real puede ser menor, si el número de banderas para descargar es menor.

MAX_CONCUR_REQ limita la cantidad máxima de solicitudes simultáneas independientemente


de la cantidad de indicadores para descargar o la opción de línea de comando -m/--max_req ; es
una precaución de seguridad.

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á.

Guarde el futuro y el código de país en el dict.

futures.as_completed devuelve un iterador que genera futuros a medida que se realizan.

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.

Iterar sobre los futuros a medida que se completan.

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.

Descargas con visualización de progreso y manejo de errores | 529


Machine Translated by Google

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.

Alternativas de subprocesos y multiprocesamiento Python

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.

Resumen del capítulo


Comenzamos el capítulo comparando dos clientes HTTP simultáneos con uno secuencial, demostrando
ganancias de rendimiento significativas sobre el script secuencial.

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.

530 | Capítulo 17: Concurrencia con futuros


Machine Translated by Google

mit(…), e iterar sobre futuros completados con concurrent.futures.as_comple


ted(…).
A continuación, vimos por qué los subprocesos de Python son adecuados para aplicaciones
vinculadas a E/S, a pesar de GIL: todas las funciones de E/S de biblioteca estándar escritas en C
liberan GIL, por lo que mientras un subproceso determinado espera E/S, Python el planificador
puede cambiar a otro subproceso. Luego discutimos el uso de múltiples procesos con la clase
concurrent.futures.Proces sPoolExecutor , para sortear el GIL y usar múltiples núcleos de CPU
para ejecutar algoritmos criptográficos, logrando aceleraciones de más del 100 % cuando se usan cuatro trabajadores.

En la siguiente sección, observamos de cerca cómo funciona concurrent.futures.Thread


PoolExecutor , con un ejemplo didáctico que inicia tareas que no hicieron nada durante unos
segundos, excepto mostrar su estado con una marca de tiempo.

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.

Lectura adicional | 531


Machine Translated by Google

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,

8. Gracias a Lucas Brunialti por enviarme un enlace a esta charla.

532 | Capítulo 17: Concurrencia con futuros


Machine Translated by Google

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,

como señala Simionato en esa cita.

9. Diapositiva n.º 9 de "Un curso curioso sobre corrutinas y concurrencia", tutorial presentado en PyCon
2009.

Lectura adicional | 533


Machine Translated by Google

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. .

534 | Capítulo 17: Concurrencia con futuros


Machine Translated by Google

La concurrencia en Competition MRI, la

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.

En cuanto a la nueva generación de lenguajes expertos en concurrencia, Go y Elixir son probablemente


los que están mejor posicionados para comerse el almuerzo de Python. Pero ahora tenemos asyncio. Si
hordas de personas creen que Node.js con devoluciones de llamada sin procesar es una plataforma viable
para la programación simultánea, ¿qué tan difícil puede ser ganarlos para Python cuando madure el
ecosistema asyncio ? Pero ese es un tema para el próximo "Soapbox" en la página 580.

Lectura adicional | 535


Machine Translated by Google
Machine Translated by Google

CAPÍTULO 18

Concurrencia con asyncio

La concurrencia consiste en tratar con muchas cosas a la vez.

El paralelismo consiste en hacer muchas cosas a la vez.

No es lo mismo, pero está relacionado.

Una es sobre la estructura, la otra sobre la ejecución.

La concurrencia proporciona una forma de estructurar una solución para resolver un problema que puede (pero
no necesariamente) ser paralelizable.1
— Rob Pike

Co-inventor del lenguaje Go

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)".

1. Diapositiva 5 de la 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.

El proyecto Trollius, también llamado así por una flor, es un backport de


asyncio a Python 2.6 y más reciente, reemplazando yield from con yield
y llamables inteligentes llamados From y Return. Una expresión yield
from... se convierte en yield From(...); y cuando una corrutina necesita
devolver un resultado, escribes raise Return (resultado) en lugar de
devolver resultado. Trollius está dirigido por Victor Stinner, quien también
es un desarrollador central de asyncio , y quien accedió amablemente a
revisar este capítulo mientras este libro entraba en producción.

En este capítulo veremos:

• Una comparación entre un programa de subprocesos simple y el equivalente asyncio ,


mostrando la relación entre subprocesos y tareas asincrónicas • En qué se diferencia la

clase asyncio.Future de concurrent.futures.Future

• Versiones asíncronas de los ejemplos de descarga de banderas del Capítulo 17 • Cómo la

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

está preparado para tener un gran impacto en el ecosistema de Python

Comencemos con el ejemplo simple contrastando subprocesos y asyncio.

538 | Capítulo 18: Concurrencia con asyncio


Machine Translated by Google

Thread versus Coroutine: una comparación


Durante una discusión sobre subprocesos y GIL, Michele Simionato publicó un ejemplo simple
pero divertido usando multiprocesamiento para mostrar una ruleta animada hecha con los
caracteres ASCII "|/-\" en la consola mientras se ejecutan cálculos largos.

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.

Figura 18-1. Los scripts spinner_thread.py y spinner_asyncio.py producen resultados similares:


el repr de un objeto spinner y el texto Respuesta: 42. En la captura de pantalla, spinner_asyncio.py
aún se está ejecutando y el mensaje spinner \ pensando! se muestra; cuando finalice el guión,
esa línea será reemplazada por la Respuesta: 42.

Primero revisemos el script spinner_thread.py (Ejemplo 18-1).

Ejemplo 18-1. spinner_thread.py: animando un girador de texto con un hilo


importar hilos
importar itertools
importar tiempo
importar sys

Señal de clase :
ir = Verdadero

giro def (mensaje, señal):

Thread versus Coroutine: una comparación | 539


Machine Translated by Google

escribir, vaciar = sys.stdout.write, sys.stdout.flush


para char en itertools.cycle('|/-\\'): estado = char
+ + mensaje ' '

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 supervisor(): señal


= Señal()
spinner = enhebrar.Thread(objetivo=girar,
args=('¡pensando!', señal))
print('objeto spinner:', spinner) spinner.start()
resultado = función_lenta() señal.ir = False
spinner.join() devolver resultado

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.

Esta función se ejecutará en un hilo separado. El argumento de la señal es una instancia.


de la clase Señal recién definida.

Esto es en realidad un bucle infinito porque itertools.cycle produce elementos


ciclismo de la secuencia dada para siempre.
El truco para hacer una animación en modo texto: mueve el cursor hacia atrás con la tecla de retroceso
caracteres (\x08).

Si el atributo go ya no es True, salga del ciclo.

540 | Capítulo 18: Concurrencia con asyncio


Machine Translated by Google

Borre la línea de estado sobreescribiéndola con espacios y moviendo el cursor de vuelta al


principio.

Imagine que esto es un cálculo costoso.

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.

Muestre el objeto de subproceso secundario. El resultado se parece a <Thread(Thread-1,


initial)>.

Inicie el subproceso secundario.

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 .

Espere hasta que termine el hilo giratorio .

Ejecute la función de supervisor .

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.

Como se indica en el “Resumen del capítulo” en la página 498


(Capítulo 16), asyncio usa una definición más estricta de “corrutina”.
Una corrutina adecuada para usar con la API asyncio debe usar
yield from y no yield en su cuerpo. Además, una corrutina asyncio
debe ser impulsada por una persona que llama invocándola a través
de yield from o pasando la corrutina a una de las funciones asyncio
como asyncio.async(…) y otras cubiertas en este capítulo.
Finalmente, el decorador @asyncio.coroutine debe aplicarse a las
corrutinas, como se muestra en los ejemplos.

Observe el ejemplo 18-2.

Ejemplo 18-2. spinner_asyncio.py: animando un girador de texto con una rutina


importar asyncio
importar itertools
importar sys

Thread versus Coroutine: una comparación | 541


Machine Translated by Google

@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

write(' ' * len(estado) + '\x08' * len(estado))

@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.

542 | Capítulo 18: Concurrencia con asyncio


Machine Translated by Google

Use yield de asyncio.sleep(.1) en lugar de solo time.sleep(.1), para dormir sin bloquear el bucle de
eventos.

Si se genera asyncio.CancelledError después de que spin se activa , es porque se solicitó la cancelación,


así que salga del ciclo. slow_function ahora es una corrutina, y usa yield from para permitir que el ciclo

de eventos continúe mientras esta corrutina pretende hacer E/S durmiendo.

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

puede impulsar slow_function con yield from.

asyncio.async(…) programa la ejecución de la rutina de giro , envolviéndola en un objeto Task , que se


devuelve inmediatamente.

Muestre el objeto Tarea . El resultado se parece a <Tarea pendiente coro=<spin() ejecutándose en


spinner_asyncio.py:12>>.

Conduce la función_lenta(). Cuando haya terminado, obtenga el valor devuelto.


Mientras tanto, el bucle de eventos continuará ejecutándose porque slow_function finalmente usa el
rendimiento de asyncio.sleep(3) para devolver el control al bucle principal.

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.

Obtenga una referencia al bucle de eventos.

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.

Nunca use time.sleep(…) en corrutinas asyncio a menos que


desee bloquear el hilo principal, por lo tanto, congelar el ciclo de
eventos y probablemente también toda la aplicación. Si una
rutina necesita pasar algún tiempo sin hacer nada, debe producir
async cio.sleep (DELAY).

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.

Thread versus Coroutine: una comparación | 543


Machine Translated by Google

Tenga en cuenta que el número de líneas de spinner_thread.py y spinner_asyncio.py es casi el mismo.


Las funciones de supervisor son el corazón de estos ejemplos. Vamos a compararlos en detalle.
El ejemplo 18-3 enumera solo al supervisor del ejemplo de creación de subprocesos .

Ejemplo 18-3. spinner_thread.py: la función supervisora de subprocesos

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

A modo de comparación, el ejemplo 18-4 muestra la rutina del supervisor .

Ejemplo 18-4. spinner_asyncio.py: la rutina del supervisor asíncrono

@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 :

• Una asyncio.Task es aproximadamente el equivalente de un threading.Thread. Victor Stinner, revisor


técnico especial de este capítulo, señala que "una tarea es como un hilo verde en las bibliotecas que
implementan la multitarea cooperativa, como gevent".

• Una tarea impulsa una corrutina y un subproceso invoca un invocable.

• 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 .

• En el supervisor de subprocesos , slow_function es una función simple y el subproceso


la invoca directamente. En el supervisor asyncio, slow_function es una rutina
impulsada por el rendimiento.
• No existe una API para terminar un subproceso desde el exterior, porque un subproceso podría
interrumpirse en cualquier punto, dejando el sistema en un estado no válido. Para las tareas, hay

544 | Capítulo 18: Concurrencia con asyncio


Machine Translated by Google

el método de instancia Task.cancel() , que genera CancelledError dentro de la rutina


conjunta. La corrutina puede lidiar con esto capturando la excepción en el rendimiento
donde está suspendida.
• La rutina de supervisor debe ejecutarse con loop.run_until_complete en la función principal .

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 .

Ahora veremos cómo la clase asyncio.Future difiere de la clase concurrent.futures.Future que


vimos en el Capítulo 17.

asyncio.Future: sin bloqueo por diseño Las clases

asyncio.Future y concurrent.futures.Future tienen casi la misma interfaz, pero se implementan


de manera diferente y no son intercambiables. PEP-3156 — Soporte de E/S asíncrono
reiniciado: el módulo "asyncio" dice lo siguiente sobre esta desafortunada situación:

En el futuro (juego de palabras intencionado) podemos unificar asyncio.Future y


concurrent.futures.Future (por ejemplo, agregando un método __iter__ al último que funciona
con yield from).

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

Thread versus Coroutine: una comparación | 545


Machine Translated by Google

Al igual que su contraparte concurrent.futures.Future , la clase asyncio.Future proporciona


métodos .done(), .add_done_callback(…) y .results() , entre otros. Los primeros dos métodos funcionan
como se describe en "¿Dónde están los futuros?" en la página 511, pero .result() es muy diferente.

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:

• No necesita my_future.add_done_callback(...) porque simplemente puede poner cualquier


procesamiento que haría después de que el futuro esté hecho en las líneas que siguen a yield
from my_future en su rutina. Esa es la gran ventaja de tener coÿrutinas: funciones que se pueden
suspender y reanudar.

• No necesita my_future.result() porque el valor de un rendimiento de la expresión


en un futuro es el resultado (p. ej., resultado = rendimiento de mi_futuro).

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.

Rendimiento de futuros, tareas y corrutinas


En asyncio, existe una estrecha relación entre los futuros y las corrutinas porque se puede obtener el
resultado de un asyncio.Future rindiéndose a partir de él. Esto significa que res = yield from foo()
funciona si foo es una función de rutina (por lo tanto, devuelve un objeto de rutina cuando se llama) o si
foo es una función simple que devuelve una instancia de Future o Task .

546 | Capítulo 18: Concurrencia con asyncio


Machine Translated by Google

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

BaseEventLoop.create_task(…) solo está disponible en Python


3.4.2 o posterior. Si usa una versión anterior de Python 3.3 o 3.4,
debe usar asyncio.async(…) o instalar una versión más reciente
de asyncio desde PyPI.

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

>>> import asyncio


>>> def run_sync(coro_or_future): loop
... = asyncio.get_event_loop() return
... loop.run_until_complete(coro_or_future)
...
>>> a = run_sync(some_coroutine())

La relación entre rutinas, futuros y tareas se documenta en la sección 18.5.3.


Tareas y corrutinas de la documentación de asyncio , donde encontrarás esta nota:

En esta documentación, algunos métodos se documentan como corrutinas, incluso si son


funciones simples de Python que devuelven un futuro. Esto es intencional para tener la
libertad de ajustar la implementación de estas funciones en el futuro.

3. Sugerido por Petr Viktorin en un mensaje del 11 de septiembre de 2014 a la lista de ideas de Python.

Thread versus Coroutine: una comparación | 547


Machine Translated by Google

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

Descargando con asyncio y aiohttp


A partir de Python 3.4, asyncio solo admite TCP y UDP directamente. Para HTTP o cualquier otro
protocolo, necesitamos paquetes de terceros; aiohttp es el que todo el mundo parece estar usando para
clientes y servidores HTTP asyncio en este momento.

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:

1. Comenzamos el proceso en download_many alimentando el bucle de eventos con varios objetos de


coÿrutina producidos al llamar a download_one.

2. El bucle de eventos asyncio activa cada corrutina por turno.

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.

se ha completado la operación de limpieza.

5. Cuando eso sucede, el bucle principal envía un resultado a la rutina suspendida.

6. La corrutina luego avanza al siguiente rendimiento, por ejemplo, rendimiento de resp.read() en


get_flag. El bucle de eventos vuelve a hacerse cargo. Los pasos 4, 5 y 6 se repiten hasta que
finaliza el bucle de eventos.

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.

Ahora repasemos el Ejemplo 18-5 jugada por jugada.

Ejemplo 18-5. flags_asyncio.py: script de descarga asincrónica con asyncio y aiohttp

importar asyncio

importar aiohttp

548 | Capítulo 18: Concurrencia con asyncio


Machine Translated by Google

from flags import BASE_URL, save_flag, show, main

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

volver len (res)

si __nombre__ == '__principal__':
principal (descargar_muchos)

aiohttp debe estar instalado, no está en la biblioteca estándar.


Reutilice algunas funciones del módulo de banderas (Ejemplo 17-2).

Las rutinas deben estar decoradas con @asyncio.coroutine.


Las operaciones de bloqueo se implementan como corrutinas y su código se delega a
a través de yield from para que se ejecuten de forma asincrónica.

La lectura del contenido de la respuesta es una operación asincrónica independiente.

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.

Obtenga una referencia a la implementación subyacente del bucle de eventos.

Cree una lista de objetos generadores llamando a la función download_one una vez por
cada bandera a recuperar.

Descargando con asyncio y aiohttp | 549


Machine Translated by Google

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.

Cierra el bucle de eventos.

Sería bueno si las instancias de bucle de eventos fueran administradores de contexto,


por lo que podríamos usar un bloque with para asegurarnos de que el bucle esté cerrado.
Sin embargo, la situación se complica por el hecho de que el código del cliente nunca
crea el bucle de eventos directamente, sino que obtiene una referencia llamando a
asyncio.get_event_loop(). A veces, nuestro código no es "dueño" del bucle de eventos,
por lo que sería un error cerrarlo. Por ejemplo, cuando se usa un bucle de eventos de
GUI externo con un paquete como Quamash, la biblioteca Qt es responsable de cerrar
el bucle cuando se cierra la aplicació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

550 | Capítulo 18: Concurrencia con asyncio


Machine Translated by Google

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.

Por ejemplo, imagina que el cuerpo de esta rutina...

@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

…funciona como la siguiente función, excepto que nunca bloquea:

def get_flag(cc): url


= '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower()) resp =
aiohttp.request('GET', url) image = 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 (…).

Descargando con asyncio y aiohttp | 551


Machine Translated by Google

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?

Correr Dar vueltas Bloquear llamadas


Ryan Dahl, el inventor de Node.js, presenta la filosofía de su proyecto diciendo
“Estamos haciendo E/S completamente mal.4 " Él define una función de bloqueo como aquella que hace
E/S de disco o red, y argumenta que no podemos tratarlos como tratamos las funciones de no bloqueo.
ciones. Para explicar por qué, presenta los números en las dos primeras columnas de la tabla 18-1.

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

Dispositivo ciclos de CPU Escala proporcional “humana”

caché L1 3 3 segundos

caché L2 14 14 segundos

RAM 250 250 segundos

disco 41,000,000 1,3 años

la red 240,000,000 7,6 años

4. Video: Introducción a Node.js a las 4:55.

552 | Capítulo 18: Concurrencia con asyncio


Machine Translated by Google

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:

• Ejecute cada operación de bloqueo en un subproceso separado.

• Convierta cada operación de bloqueo en una llamada asíncrona sin bloqueo.

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.

Corriendo Dando vueltas Bloqueando llamadas | 553


Machine Translated by Google

flags_asyncio.py, el bucle de eventos lleva cada corrutina download_one al primer rendimiento


desde, y esto a su vez lleva a cada corrutina get_flag al primer rendimiento desde, llamando a
aiohttp.request(…). Ninguna de estas llamadas está bloqueando, por lo que todas las solicitudes se
inician en una fracción de segundo.

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.

Para obtener el máximo rendimiento, la operación save_flag debe ser


asíncrona, pero asyncio no proporciona una API de sistema de archivos
asíncrona en este momento, como lo hace Node. Si eso se convierte en
un cuello de botella en su aplicación, puede usar la función
loop.run_in_execu tor para ejecutar save_flag en un grupo de
subprocesos. El ejemplo 18-9 mostrará cómo.

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.

Mejora de la secuencia de comandos del descargador de asyncio

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

Ejemplo 18-6. Ejecutando flags2_asincio.py


$ python3 flags2_asyncio.py -s ERROR -al 100 -m 100 ERROR
sitio: http://localhost:8003/flags Búsqueda de 100 banderas: de
AD a LK Se utilizarán 100 conexiones simultáneas.

--------------------
73 banderas descargadas.
27 errores.
Tiempo transcurrido: 0,64 s

554 | Capítulo 18: Concurrencia con asyncio


Machine Translated by Google

Actúe con responsabilidad al probar clientes


simultáneos Incluso si el tiempo total de descarga no es diferente entre
los clientes HTTP con subprocesos y asyncio , asyncio puede enviar
solicitudes más rápido, por lo que es aún más probable que el servidor
sospeche un ataque de DOS. Para ejercitar realmente estos clientes
simultáneos a toda velocidad, configure un servidor HTTP local para realizar
pruebas, como se explica en README.rst dentro del directorio 17-futures/
countries/ del repositorio de código de Fluent Python .

Ahora veamos cómo se implementa flags2_asyncio.py .

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

Mejora de la secuencia de comandos del descargador de asyncio | 555


Machine Translated by Google

desde flags2_common import main, HTTPStatus, Result, save_flag

# 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

class FetchError(Excepción): def


__init__(self, código_país):
self.código_país = código_país

@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):

intente: con (rendimiento del semáforo):


imagen = rendimiento de get_flag (url_base, cc) excepto
web.HTTPNotFound: estado = HTTPStatus.not_found msg = 'no
encontrado' excepto Excepción como exc: aumentar
FetchError (cc) de exc else: save_flag (imagen, cc.inferior() +
'.gif') estado = HTTPStatus.ok mensaje = ' OK'

si detallado y msg:
imprimir (cc, msg)

resultado devuelto (estado, cc)

Esta excepción personalizada se usará para envolver otras excepciones de red o


HTTP y llevar el código_país para el informe de errores.

556 | Capítulo 18: Concurrencia con asyncio


Machine Translated by Google

get_flag devolverá los bytes de la imagen descargada, generará


web.HTTPNotFound si el estado de la respuesta HTTP es 404 o generará un
aiohttp.HttpProcessingError para otros códigos de estado HTTP.
El argumento semáforo es una instancia de asyncio.Semaphore, un dispositivo de sincronización que limita el
número de solicitudes simultáneas.

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

de rutina que puede estar esperando el mismo objeto de semáforo .

Si no se encontró la bandera, simplemente configure el estado para el Resultado en consecuencia.

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.

Esta llamada de función en realidad guarda la imagen de la bandera en el disco.

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.

Mejora de la secuencia de comandos del descargador de asyncio | 557


Machine Translated by Google

Llamar a .acquire() no se bloquea cuando el contador es mayor que cero, pero si el


el contador es cero, .acquire() bloqueará la corrutina de llamada hasta que otra corrutina
llama a .release() en el mismo semáforo, incrementando así el contador. En
Ejemplo 18-7, no llamo a .acquire() o .release(), pero uso el semáforo como contexto
administrador en este bloque de código dentro de download_one:

con (rendimiento del semáforo):


imagen = rendimiento de get_flag (base_url, cc)

Ese fragmento garantiza que no más de instancias concur_req de get_flags coro-


Las rutinas se iniciarán en cualquier momento.

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

Ejemplo 18-8. flags2_asincio.py: Script continúa del Ejemplo 18-7

@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:

res = rendimiento del futuro


excepto FetchError como exc:
código_país = exc.código_país intente:

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:

558 | Capítulo 18: Concurrencia con asyncio


Machine Translated by Google

estado = res.estado

contador[estado] += 1

contador de retorno

def download_many(cc_list, base_url, detallado, concur_req):


loop = asyncio.get_event_loop() coro =
downloader_coro(cc_list, base_url, verbose, concur_req) cuenta = loop.run_until_complete(coro)
loop.close()

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 .

Obtenga un iterador que devolverá los futuros a medida que finalicen.

Envuelva el iterador en la función tqdm para mostrar el progreso.

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

Cada excepción en download_one está envuelta en un FetchError con la excepción original


encadenada.

Obtenga el código de país donde ocurrió el error de la excepción FetchError .

Intente recuperar el mensaje de error de la excepción original (__cause__).

Si el mensaje de error no se encuentra en la excepción original, utilice el nombre de la clase de


excepción encadenada como mensaje de error.

Conteo de resultados.

Devuelve el contador, como se hace en los otros scripts.

Mejora de la secuencia de comandos del descargador de asyncio | 559


Machine Translated by Google

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 .

Esto concluye la discusión de un ejemplo de asyncio funcionalmente equivalente al


flags2_threadpool.py que vimos anteriormente. A continuación, implementaremos mejoras en
flags2_asyncio.py que nos permitirán explorar asyncio más a fondo.

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.

Uso de un ejecutor para evitar bloquear el bucle de eventos En la

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.

560 | Capítulo 18: Concurrencia con asyncio


Machine Translated by Google

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

intente: con (rendimiento del semáforo):


imagen = rendimiento de get_flag (base_url, cc)
excepto web.HTTPNotFound: estado = HTTPStatus.not_found
msg = 'no encontrado' excepto Excepción como exc:
generar FetchError (cc) de exc else: loop =
asyncio.get_event_loop() loop.run_in_executor(Ninguno,

save_flag, imagen, cc.inferior() + '.gif')


estado = HTTPStatus.ok
mensaje = ' OK'

si detallado y msg:
imprimir (cc, msg)

resultado devuelto (estado, cc)

Obtenga una referencia al objeto de bucle de eventos.

El primer argumento de run_in_executor es una instancia de ejecutor; si es Ninguno, se utiliza el ejecutor


del grupo de subprocesos predeterminado del bucle de eventos.

Los argumentos restantes son el invocable y sus argumentos posicionales.

Cuando probé el Ejemplo 18-9, no hubo cambios notables en el


rendimiento al usar run_in_executor para guardar los archivos de
imagen porque no son grandes (13 KB cada uno, en promedio).
Pero verá un efecto si edita la función save_flag en
flags2_common.py para guardar 10 veces más bytes en cada
archivo, simplemente codificando fp.write(img*10) en lugar de
fp.write(img ). Con un tamaño de descarga promedio de 130 KB, la
ventaja de usar run_in_ex ecutor se hace evidente. Si está
descargando imágenes de megapíxeles, la aceleración será significativa.

Mejora de la secuencia de comandos del descargador de asyncio | 561


Machine Translated by Google

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.

De devoluciones de llamadas a futuros y rutinas


La programación orientada a eventos con rutinas requiere algo de esfuerzo para dominarla, por lo que es bueno tener
claro cómo mejora el estilo clásico de devolución de llamada. Este es el tema de esta secció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

api_call1(solicitud1, función (respuesta1) { // etapa


1 var solicitud2 = paso1(respuesta1);

api_call2(solicitud2, función (respuesta2) { // etapa


2 var solicitud3 = paso2 (respuesta2);

api_call3(solicitud3, función (respuesta3) { // etapa


3 paso3(respuesta3);

}); });
});

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.

El ejemplo 18-11 muestra cómo se ve el infierno de devolución de llamada en Python.

Ejemplo 18-11. Infierno de devolución de llamada en Python: devoluciones de llamada encadenadas

def etapa1(respuesta1):
solicitud2 = paso1(respuesta1)
api_call2(solicitud2, etapa2)

562 | Capítulo 18: Concurrencia con asyncio


Machine Translated by Google

def etapa2(respuesta2):
solicitud3 = paso2 (respuesta2)
api_call3(solicitud3, etapa3)

def etapa3(respuesta3):
paso3(respuesta3)

api_call1 (solicitud1, etapa1)

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)

De devoluciones de llamadas a futuros y rutinas | 563


Machine Translated by Google

loop.create_task(tres_etapas(solicitud1)) # debe programar explícitamente la ejecución

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.

El siguiente ejemplo pone en práctica esta teoría.

Realización de solicitudes múltiples para cada descarga Suponga

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

564 | Capítulo 18: Concurrencia con asyncio


Machine Translated by Google

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

elif res.status == 404:


generar web.HTTPNotFound()
más: generar
aiohttp.errors.HttpProcessingError ( código =
res.status, mensaje = res.reason, encabezados
= res.headers)

@asyncio.coroutine
def get_country(base_url, cc):
url = '{}/{cc}/metadata.json'.format(base_url, cc=cc.inferior())

De devoluciones de llamadas a futuros y rutinas | 565


Machine Translated by Google

metadatos = rendimiento de http_get(url)


devuelve metadatos['país']

@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):

intente: con (rendimiento del


semáforo): imagen = rendimiento de get_flag
(base_url, cc) con (rendimiento del semáforo): país =
rendimiento de get_country (base_url, cc)
excepto web.HTTPNotFound:
estado = HTTPStatus.not_found
msg = 'no encontrado' excepto
Excepción como exc: generar
FetchError(cc) de exc otra cosa:

country = country.replace(' ', '_') filename =


'{}-{}.gif'.format(country, cc) loop =
asyncio.get_event_loop() loop.run_in_executor(Ninguno,
save_flag, image, filename) estado = HTTPStatus.ok

mensaje = 'OK'

si detallado y msg:
imprimir (cc, msg)

resultado devuelto (estado, cc)

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.

566 | Capítulo 18: Concurrencia con asyncio


Machine Translated by Google

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.

Ahora pasaremos de scripts de clientes a servidores de escritura con asyncio.

Escribir servidores asyncio


El ejemplo clásico de juguete de un servidor TCP es un servidor de eco. Construiremos juguetes un poco
más interesantes: buscadores de caracteres Unicode, primero usando TCP simple, luego usando HTTP.
Estos servidores permitirán a los clientes consultar caracteres Unicode en función de las palabras de sus
nombres canónicos, utilizando el módulo de datos unicode que analizamos en “La base de datos
Unicode” en la página 127. Una sesión de Telnet con el servidor de búsqueda de caracteres TCP,
buscando piezas de ajedrez y los caracteres con la palabra “sol” se muestran en la Figura 18-2.

Escritura de servidores asyncio | 567


Machine Translated by Google

Figura 18-2. Una sesión de Telnet con el servidor tcp_charfinder.py: consultando "ajedrez negro" y "sol".

Ahora, vamos a las implementaciones.

Un servidor TCP asyncio La

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

en un archivo local charfinder_index.pickle . Si aparecen varias palabras en la consulta, charfind er


calcula la intersección de los conjuntos recuperados del índice.

568 | Capítulo 18: Concurrencia con asyncio


Machine Translated by Google

Ahora nos centraremos en el script tcp_charfinder.py que responde a las consultas en


Figura 18-2. Como tengo mucho que decir sobre este código, lo he dividido en dos partes:
Ejemplo 18-14 y Ejemplo 18-15.

Ejemplo 18-14. tcp_charfinder.py: un servidor TCP simple usando asyncio.start_server; código


para este módulo continúa en el Ejemplo 18-15
sistema de importación

importar asyncio

desde charfinder importar UnicodeNameIndex

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:

si ord(consulta[:1]) < 32:


descanso
líneas = lista(index.find_description_strs(consulta)) si líneas:

escritor.writelines(línea.encode() + CRLF para línea en líneas)


escritor.write(index.status(consulta, len(líneas)).encode() + CRLF)

rendimiento de escritor.drain()
print('Resultados enviados {} '. format(len(líneas)))

print('Cerrar el socket del cliente')


escritor.cerrar()

UnicodeNameIndex es la clase que crea el índice de nombres y proporciona


métodos de consulta.

Escritura de servidores asyncio | 569


Machine Translated by Google

Cuando se crea una instancia, UnicodeNameIndex usa charfinder_index.pickle, si está disponible, o lo


crea, por lo que la primera ejecución puede tardar unos segundos más en iniciarse.8 Esta es la rutina

que debemos pasar a asyncio_startserver; los argumentos recibidos son asyncio.StreamReader y


asyncio.StreamWriter.

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.

Registre la consulta en la consola del servidor.

Salga del ciclo si se recibió un carácter de control o nulo.

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.

Escriba una línea de estado como 627 coincidencias para 'dígito'.

Vacíe el búfer de salida.

Registre la respuesta en la consola del servidor.

Registre el final de la sesión en la consola del servidor.


Cierre StreamWriter.

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.

570 | Capítulo 18: Concurrencia con asyncio


Machine Translated by Google

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.

Ejemplo 18-15. tcp_charfinder.py (continuación del Ejemplo 18-14): la función principal


configura y desarma el bucle de eventos y el servidor de socket

def main(dirección='127.0.0.1', puerto=2323): puerto


= int(puerto)
bucle = asincio.get_event_loop()
server_coro = asyncio.start_server(handle_queries, dirección, puerto,
loop=loop)
servidor = loop.run_until_complete(server_coro)

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:])

La función principal se puede llamar sin argumentos.

Cuando se completa, el objeto coroutine devuelto por asyncio.start_server


devuelve una instancia de asyncio.Server, un servidor de socket TCP.

Conduzca server_coro para abrir el servidor.

Obtenga la dirección y el puerto del primer socket del servidor y...

…mostrar en la consola del servidor. Esta es la primera salida generada por este script.
en la consola del servidor.

Escritura de servidores asyncio | 571


Machine Translated by Google

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.

Terminar el bucle de eventos.

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

Esta es la salida de main.

Primera iteración del ciclo while en handle_queries.

Segunda iteración del ciclo while .

El usuario presionó CTRL-C; el servidor recibe un carácter de control y se acerca a la sesión.

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

572 | Capítulo 18: Concurrencia con asyncio


Machine Translated by Google

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.

La siguiente sección presenta un servidor buscador de caracteres HTTP.

Un servidor web aiohttp La

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

Escritura de servidores asyncio | 573


Machine Translated by Google

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.

Comenzaremos analizando la parte más interesante de http_charfinder.py: la mitad inferior


donde el bucle de eventos y el servidor HTTP se configuran y derriban. Vea el Ejemplo 18-17.

Ejemplo 18-17. http_charfinder.py: las funciones principal y de inicio


@asincio.coroutine
def init(bucle, dirección, puerto):
app = web.Application(loop=loop)
app.router.add_route('GET', '/', home) handler =
app.make_handler() server = rendimiento de
loop.create_server(handler,
dirección, puerto)
devolver servidor.sockets[0].getsockname()

def principal(dirección="127.0.0.1", puerto=8888):


puerto = int(puerto)
bucle = asincio.get_event_loop()
host = loop.run_until_complete(init(bucle, dirección, puerto)) print('Serviendo en {}.
Presione CTRL-C para detener.'.format(host))
probar:

loop.run_forever() excepto
KeyboardInterrupt: # CTRL+C presionado
pasar
print('Servidor apagándose.')
bucle.cerrar()

si __nombre__ == '__principal__':
principal(*sys.argv[1:])

La corrutina init produce un servidor para que el ciclo de eventos lo maneje.

La clase aiohttp.web.Application representa una aplicación web...

…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).

El método app.make_handler devuelve un aiohttp.web.RequestHandler


instancia para manejar solicitudes HTTP de acuerdo con las rutas configuradas en la aplicación
objeto.

create_server muestra el servidor, utilizando handler como controlador de protocolo y


vinculándolo a la dirección y al puerto.

574 | Capítulo 18: Concurrencia con asyncio


Machine Translated by Google

Devuelve la dirección y el puerto del primer socket del servidor.

Ejecute init para iniciar el servidor y obtener su dirección y puerto.

Ejecute el bucle de eventos; main se bloqueará aquí mientras el bucle de eventos esté bajo control.

Cierra el bucle de eventos.

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:

server_coro = asyncio.start_server(handle_queries, address, port, loop=loop)


server = loop.run_until_complete(server_coro)

En el ejemplo de HTTP, la función init crea el servidor así:

servidor = rendimiento de loop.create_server (controlador,


dirección, puerto)

Pero init en sí mismo es una rutina, y lo que hace que se ejecute es la función principal , con esta línea:

host = loop.run_until_complete(init(bucle, dirección, puerto))

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.

Ejemplo 18-18. http_charfinder.py: la función de inicio

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)

Escritura de servidores asyncio | 575


Machine Translated by Google

msg = index.status(consulta, len(descripciones)) else:


descripciones = []

res = ''
msg = 'Ingrese palabras que describan caracteres.'

html = template.format(consulta=consulta, resultado=res,


mensaje=mensaje)
print('Enviando {} resultados'.format(len(descripciones))) return
web.Response(content_type=CONTENT_TYPE, text=html)

Un controlador de ruta recibe una instancia de aiohttp.web.Request .

Obtenga la cadena de consulta despojada de espacios en blanco iniciales y finales.

Consulta de registro en la consola del servidor.

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.

Renderice la página HTML.

Registre la respuesta en la consola del servidor.

Cree la respuesta y devuélvala.

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.

Clientes más inteligentes para una mejor concurrencia La

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

576 | Capítulo 18: Concurrencia con asyncio


Machine Translated by Google

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.

Resumen del capítulo


Este capítulo presentó una forma completamente nueva de codificar la concurrencia en Python, aprovechando el
rendimiento de, las corrutinas, los futuros y el ciclo de eventos asyncio . Los primeros ejemplos simples, los scripts
giratorios, se diseñaron para demostrar una comparación lado a lado de los enfoques de subprocesamiento y asyncio
para la concurrencia.

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.

Resumen del capítulo | 577


Machine Translated by Google

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.

578 | Capítulo 18: Concurrencia con asyncio


Machine Translated by Google

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

2. rendimiento de f en una corrutina (reanuda la corrutina cuando se completa el futuro,


ya sea con el resultado o la excepción según corresponda)

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.

Lectura adicional | 579


Machine Translated by Google

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.

580 | Capítulo 18: Concurrencia con asyncio


Machine Translated by Google

El Zen de Python se aplica perfectamente:

Debe haber una, y preferiblemente solo una, forma obvia de hacerlo.

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.

Lectura adicional | 581


Machine Translated by Google

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:

One Loop para gobernarlos a todos, One Loop para encontrarlos,

Un Lazo para traerlos a todos y atarlos en la vida.

12. Véase el mensaje de Guido del 29 de enero de 2015, seguido inmediatamente por una respuesta de Glyph.

582 | Capítulo 18: Concurrencia con asyncio


Machine Translated by Google

PARTE VI

Metaprogramación
Machine Translated by Google
Machine Translated by Google

CAPÍTULO 19

Propiedades y atributos dinámicos

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.

1. Alex Martelli, Python en pocas palabras, 2E (O'Reilly), pág. 101.

2. Bertrand Meyer, Construcción de software orientada a objetos, 2E, pág. 57.

585
Machine Translated by Google

Gestión de datos con atributos dinámicos


En los siguientes ejemplos, aprovecharemos los atributos dinámicos para trabajar con una fuente de
datos JSON publicada por O'Reilly para la conferencia OSCON 2014. El ejemplo 19-1 muestra cuatro
registros de esa fuente de datos.3

Ejemplo 19-1. Registros de muestra de osconfeed.json; algunos contenidos de campo abreviados

{ "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 .

586 | Capítulo 19: Propiedades y atributos dinámicos


Machine Translated by Google

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.

Ejemplo 19-2. osconfeed.py: descargando osconfeed.json (las pruebas de documentación están en el


Ejemplo 19-3)

from urllib.request import urlopen


import warnings import os import json

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 abierto (JSON) como fp:


devolver json.load (fp)

Emite una advertencia si se realizará una nueva descarga.

con el uso de dos administradores de contexto (permitidos desde Python 2.7 y 3.1) para leer el
archivo remoto y guardarlo.

La función json.load analiza un archivo JSON y devuelve objetos Python nativos.


En este feed, tenemos los tipos: dict, list, str e int.

Con el código del ejemplo 19-2, podemos inspeccionar cualquier campo de los datos. Vea el Ejemplo 19-3.

Ejemplo 19-3. osconfeed.py: doctests para el Ejemplo 19-2

>>> feed = load()


>>> sorted(feed['Schedule'].keys())
['conferences', 'evenues', 'speakers', 'venues'] >>> for
key, value in sorted (feed['Horario'].items()): print('{:3}
... {}'.format(len(valor), clave))
...

Gestión de datos con atributos dinámicos | 587


Machine Translated by Google

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.

Ejemplo 19-2. osconfeed.py: descargando osconfeed.json (las pruebas de documentación están en el


Ejemplo 19-3)

from urllib.request import urlopen


import warnings import os import json

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 abierto (JSON) como fp:


devolver json.load (fp)

Emite una advertencia si se realizará una nueva descarga.

con el uso de dos administradores de contexto (permitidos desde Python 2.7 y 3.1) para leer el
archivo remoto y guardarlo.

La función json.load analiza un archivo JSON y devuelve objetos Python nativos.


En este feed, tenemos los tipos: dict, list, str e int.

Con el código del ejemplo 19-2, podemos inspeccionar cualquier campo de los datos. Vea el Ejemplo 19-3.

Ejemplo 19-3. osconfeed.py: doctests para el Ejemplo 19-2

>>> feed = load()


>>> sorted(feed['Schedule'].keys())
['conferences', 'evenues', 'speakers', 'venues'] >>> for
key, value in sorted (feed['Horario'].items()): print('{:3}
... {}'.format(len(valor), clave))
...

Gestión de datos con atributos dinámicos | 587


Machine Translated by Google

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".

Mostrar recuentos de registros para cada colección.

Navegue a través de los dictados anidados y las listas para obtener el nombre del último orador.

Obtenga el número de serie de ese mismo altavoz.

Cada evento tiene una lista de 'oradores' con 0 o más números de serie de oradores.

Explorar datos similares a JSON con atributos dinámicos El ejemplo 19-2

es bastante simple, pero la fuente de sintaxis ['Schedule']['events'][40] ['name'] es engorrosa. En JavaScript,


puede obtener el mismo valor escribiendo feed.Schedule.events[40].name. Es fácil implementar una clase
similar a un dictado que hace lo mismo en Python: hay muchas implementaciones en la Web.4 Implementé
mi propio FrozenJSON, que es más simple que la mayoría de las recetas porque admite solo lectura: es solo
para explorar los datos . Sin embargo, también es recursivo y se ocupa automáticamente de listas y
asignaciones anidadas.

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

>>> from osconfeed import load


>>> raw_feed = load() >>> feed =
FrozenJSON(raw_feed) >>>
len(feed.Schedule.speakers) 357 >>>
sorted(feed.Schedule.keys())
[ 'conferencias', 'eventos', 'conferencistas',
'lugares'] >>> for key, value in sorted(feed.Schedule.items()):
print('{:3} {}'.format(len( valor), clave))
...

4. Uno mencionado a menudo es AttrDict; otro, que permite la creación rápida de mapeos anidados, es adictivo.

588 | Capítulo 19: Propiedades y atributos dinámicos


Machine Translated by Google

...
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

el len() de cada uno de ellos.

Una lista, como feed.Schedule.speakers, sigue siendo una lista, pero los elementos que contiene se convierten a

FrozenJSON si son asignaciones.

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.

Gestión de datos con atributos dinámicos | 589


Machine Translated by Google

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

de colecciones importar abc

clase FrozenJSON:
"""Una fachada de solo lectura para navegar por un objeto similar a JSON
usando notación de atributos
"""

def __init__(auto, mapeo):


self.__data = dict(asignación)

def __getattr__(self, nombre): if


hasattr(self.__data, nombre):
devuelve getattr(self.__data, nombre) otra
cosa:
devuelve FrozenJSON.build(self.__data[nombre])

@métodoclase
def build(cls, obj): if
isinstance(obj, abc.Mapping): return cls(obj)

elif es una instancia (obj, abc.MutableSequence):


devuelve [cls.build (elemento) para el elemento en obj]
más:
devolver 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 .

590 | Capítulo 19: Propiedades y atributos dinámicos


Machine Translated by Google

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 obj es un mapeo, crea un FrozenJSON con él.

Si es una MutableSequence, debe ser una lista,6 por lo que construimos una lista pasando cada elemento
en obj recursivamente a .build().

Si no es un dictado o una lista, devuelva el elemento tal como está.

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.

El problema del nombre de atributo no válido

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:

>>> graduado = FrozenJSON({'nombre': 'Jim Bo', 'clase': 1982})

No podrá leer grad.class porque class es una palabra reservada en Python:

>>> grad.class
Archivo "<stdin>", línea 1
grad.class
^

SyntaxError: sintaxis inválida

Siempre puedes hacer esto, por supuesto:

>>> getattr(graduado, 'clase') 1982

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.

Gestión de datos con atributos dinámicos | 591


Machine Translated by Google

>>> grad.clase_ 1982

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

def __init__(self, mapeo): self.__data


= {} for clave, valor en
mapping.items(): if palabra clave.iskeyword(clave):
clave += '_' self.__data[clave] = valor

La función keyword.iskeyword(…) es exactamente lo que necesitamos; para usarlo, se debe importar


el módulo de palabras clave , que no se muestra en este fragmento.

Puede surgir un problema similar si una clave en el JSON no es un identificador de Python válido:

>>> x = FrozenJSON({'2be':'or not'}) >>> x.2be


Archivo "<stdin>", línea 1 x.2be

SyntaxError: sintaxis inválida

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.

Creación flexible de objetos con __new__ A menudo nos

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

592 | Capítulo 19: Propiedades y atributos dinámicos


Machine Translated by Google

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:

# pseudocódigo para la construcción de objetos


def object_maker(the_class, some_arg):
nuevo_objeto = la_clase.__nuevo__(algún_argumento)
if isinstance(nuevo_objeto, la_clase):
la_clase.__init__(nuevo_objeto, algún_argumento)
devuelve nuevo_objeto

# las siguientes declaraciones son aproximadamente equivalentes


x = Foo('bar') x = object_maker(Foo, 'bar')

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
"""

def __new__(cls, arg): if


isinstance(arg, abc.Mapping): return
super().__new__(cls) elif
isinstance(arg, abc.MutableSequence): return [cls(item)
for item in arg] else: devolver argumento

def __init__(self, mapeo): self.__data


= {} for key, value in
mapping.items(): if iskeyword(key): key += '_'

Gestión de datos con atributos dinámicos | 593


Machine Translated by Google

self.__data[clave] = valor

def __getattr__(yo, nombre):


if hasattr(self.__data, nombre):
return getattr(self.__data, name) else:
return FrozenJSON(self.__data[name])

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.

El comportamiento predeterminado es delegar al __nuevo__ de una superclase. En este caso, estamos


llamando a __new__ desde la clase base del objeto , pasando FrozenJSON como único argumento.

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.

Reestructuración de OSCON Feed con shelve El nombre gracioso

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:

594 | Capítulo 19: Propiedades y atributos dinámicos


Machine Translated by Google

• shelve.Shelf subclases abc.MutableMapping, por lo que proporciona el método esencial


probabilidades que esperamos de un tipo de mapeo

• Además, shelve.Shelf proporciona algunos otros métodos de administración de E/S, como


sincronizar y cerrar; también es un administrador de contexto.

• Las claves y los valores se guardan cada vez que se asigna un nuevo valor a una clave.

• Las claves deben ser cadenas.

• 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'].

Ejemplo 19-8. Probando la funcionalidad provista por schedule1.py (Ejemplo 19-9)

>>> estantería de importación


>>> db = shelve.open(DB_NAME)
>>> si CONFERENCIA no está en
... db: load_db(db)
...
>>> hablante = db['hablante.3471'] >>>
tipo(hablante) <clase 'horario1.Registro'>

>>> hablante.nombre, hablante.twitter


('Anna Martelli Ravenscroft', ' annaraven ')
>>> db.cerrar()

shelve.open abre un archivo de base de datos existente o recién creado.

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

Si la base de datos está vacía, llame a load_db(db) para cargarla.

7. También podría hacer len(db), pero eso sería costoso en una gran base de datos dbm.

Gestión de datos con atributos dinámicos | 595


Machine Translated by Google

Obtener un registro de orador .

Es una instancia de la clase Record definida en el Ejemplo 19-9.

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

El código para schedule1.py está en el ejemplo 19-9.

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)

Cargue el módulo osconfeed.py del Ejemplo 19-2.

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.

Iterar sobre las colecciones (por ejemplo, 'conferencias', 'eventos', etc.).

record_type se establece en el nombre de la colección sin la 's' final (es decir, 'eventos'
se convierte en 'evento').

Cree la clave a partir de record_type y el campo 'serial' .

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.

596 | Capítulo 19: Propiedades y atributos dinámicos


Machine Translated by Google

Actualice el campo 'serial' con la clave completa.

Cree la instancia de Record y guárdela en la base de datos bajo la clave.

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

No voy a repetir los detalles que discutimos anteriormente en “El problema


del nombre de atributo no válido” en la página 591, pero según el contexto
de la aplicación, es posible que la clase Record deba tratar con claves que
no son nombres de atributo válidos.

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.

La biblioteca estándar de Python proporciona al menos dos clases similares


a nuestro Registro, donde cada instancia tiene un conjunto arbitrario de
atributos creados a partir de argumentos de palabras clave para el
constructor: procesamiento múltiple . Espacio de nombres ( documentación,
código fuente) y análisis de argumentos. Espacio de nombres ( documentación,
código fuente). Implementé Record para resaltar la esencia de la idea:
__init__ actualizando la instancia __dict__.

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 ".

Gestión de datos con atributos dinámicos | 597


Machine Translated by Google

Recuperación de registros vinculados con propiedades

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.

Ejemplo 19-10. Extracto de los doctests de schedule2.py

>>> 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.

El método de clase DbRecord.fetch recupera registros de cualquier tipo.

Tenga en cuenta que event es una instancia de la clase Event , que amplía DbRecord.

Acceder a event.venue devuelve una instancia de DbRecord .

Ahora es fácil encontrar el nombre de un evento.lugar. Esta desreferenciación automática es el


objetivo de este ejemplo.

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.

598 | Capítulo 19: Propiedades y atributos dinámicos


Machine Translated by Google

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

declaraciones de schedule2.py se muestran en el ejemplo 19-11.

Ejemplo 19-11. schedule2.py: importaciones, constantes y la clase Record mejorada

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)

def __eq__(uno mismo, otro):

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.

Gestión de datos con atributos dinámicos | 599


Machine Translated by Google

si es una instancia (otro, registro):


return self.__dict__ == otro.__dict__
más:
volver No implementado

inspeccionar se utilizará en la función load_db (Ejemplo 19-14).

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.

Un método __eq__ siempre es útil para las pruebas.

En Python 2, solo las clases de "nuevo estilo" admiten propiedades. Escribir


una nueva clase de estilo en Python 2 debe subclasificar directamente o de forma individual
directamente del objeto. El registro del ejemplo 19-11 es la clase base de un
jerarquía que usará propiedades, por lo que en Python 2 su declaración
comenzaría con: 12

registro de clase (objeto):


# etc...

Las siguientes clases definidas en schedule2.py son un tipo de excepción personalizado y DbRecord. Ver
Ejemplo 19-12.

Ejemplo 19-12. Schedule2.py: clase MissingDatabaseError y 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):
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.

600 | Capítulo 19: Propiedades y atributos dinámicos


Machine Translated by Google

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 .

DbRecord extiende Record.


El atributo de clase __db contendrá una referencia a la base de datos shelve.Shelf abierta .

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.

Esto recupera el registro con la clave de identificación de la base de datos.

Si obtenemos un TypeError y db es None, genere una excepción personalizada que explique que la base
de datos debe estar configurada.

De lo contrario, vuelva a generar la excepción porque no sabemos cómo manejarla.

Si el registro tiene un atributo de serie , utilícelo en la representación de cadena.

De lo contrario, utilice por defecto el __repr__ heredado.

Ahora llegamos al meollo del ejemplo: la clase Event , listada en el Ejemplo 19-13.

Ejemplo 19-13. schedule2.py: la clase de evento

Evento de clase (DbRecord):

@property
def sede(self): clave
= 'lugar.{}'.format(self.lugar_serial)

Gestión de datos con atributos dinámicos | 601


Machine Translated by Google

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

def __repr__(uno mismo):


if hasattr(self, 'nombre'): cls_name
= self.__class__.__name__
devuelve '<{} {!r}>'.format(cls_name, self.name)
más:
devolver super().__repr__()

El evento extiende DbRecord.

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

La propiedad de altavoces comprueba si el registro tiene un atributo _speaker_objs .

Si no es así, el atributo 'altavoces' se recupera directamente de la instancia


__dict__ para evitar una recursión infinita, porque el nombre público de esta propiedad
es también altavoces.

Obtenga una referencia al método de clase de búsqueda (la razón de esto se explicará
dentro de poco).

self._speaker_objs se carga con una lista de registros de hablantes , usando fetch.


Esa lista se devuelve.

Si el registro tiene un atributo de nombre , utilícelo en la representación de cadena.

De lo contrario, utilice por defecto el __repr__ heredado.

En la propiedad del lugar del ejemplo 19-13, la última línea devuelve


self.__class__.fetch(clave). ¿Por qué no escribir eso simplemente como self.fetch(key)? los
fórmula más simple funciona con el conjunto de datos específico de la fuente OSCON porque no hay
registro de eventos con una tecla 'buscar' . Si incluso un solo registro de evento tuviera una clave llamada
'buscar', luego, dentro de esa instancia de Evento específica , la referencia self.fetch volvería a
obtener el valor de ese campo, en lugar del método de clase de obtención del que hereda Event
DbRecord. Este es un error sutil, y podría colarse fácilmente a través de las pruebas y explotar
solo en producción cuando el lugar o los registros del orador están vinculados a ese evento específico
se recuperan los registros.

602 | Capítulo 19: Propiedades y atributos dinámicos


Machine Translated by Google

Al crear nombres de atributos de instancias a partir de datos, siempre hay


formas el riesgo de errores debido al sombreado de los atributos de clase (como
métodos) o pérdida de datos debido a la sobrescritura accidental de
atributos de instancia. Esta advertencia es probablemente la razón principal por la cual,
por defecto, los dictados de Python no son como los objetos de JavaScript en el primer
lugar.

Si la clase Record se comportara más como un mapeo, implementando un __geti dinámico


tem__ en lugar de un __getattr__ dinámico, no habría riesgo de errores por exceso .
escribir o sombrear. Un mapeo personalizado es probablemente la forma Pythonic de implementar
Registro. Pero si tomara ese camino, no estaríamos reflexionando sobre los trucos y trampas de la dinámica.
programación de atributos.

La pieza final de este ejemplo es la función load_db revisada en el Ejemplo 19-14.

Ejemplo 19-14. schedule2.py: la función load_db

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.

Si el objeto recién recuperado es una clase y es una subclase de DbRecord...

…vincularle el nombre de la fábrica . Esto significa que la fábrica puede ser cualquier subclase de

DbRecord, dependiendo del record_type.


De lo contrario, vincule el nombre de la fábrica a DbRecord.

El bucle for que crea la clave y guarda los registros es el mismo que antes,
excepto eso…

Gestión de datos con atributos dinámicos | 603


Machine Translated by Google

…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.

Uso de una propiedad para la validación de atributos

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.

LineItem Take #1: Clase para un artículo en un pedido

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.

Ejemplo 19-15. bulkfood_v1.py: la clase LineItem más simple


elemento de línea de clase :

def __init__(self, descripción, peso, precio): self.description


= descripción self.weight = peso self.price = precio

def subtotal(self):
return self.weight * self.price

Eso es agradable y simple. Quizás demasiado simple. El ejemplo 19-16 muestra un problema.

Ejemplo 19-16. Un peso negativo da como resultado un subtotal negativo

>>> pasas = LineItem(' Pasas doradas', 10, 6.95) >>>


pasas.subtotal() 69.5

>>> pasas.peso = -20 # basura en... >>>


pasas.subtotal() # basura fuera... -139.0

604 | Capítulo 19: Propiedades y atributos dinámicos


Machine Translated by Google

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.

LineItem Take #2: una propiedad de validación

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

El ejemplo 19-17 enumera el código para una propiedad de ponderación de lectura/escritura .

Ejemplo 19-17. bulkfood_v2.py: un elemento de línea con una propiedad de peso


elemento de línea de clase :

def __init__(self, descripción, peso, precio): self.description


= descripción self.weight = peso self.price = precio

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

Uso de una propiedad para la validación de atributos | 605


Machine Translated by Google

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 valor real se almacena en un atributo privado __weight.

El getter decorado tiene un atributo .setter , que también es un decorador; esto une al getter y al setter.

Si el valor es mayor que cero, establecemos el __peso privado.

De lo contrario, se genera ValueError .

Tenga en cuenta que ahora no se puede crear un elemento de línea con un peso no válido:

>>> nueces = LineItem('nueces', 0, 10.00)


Rastreo (llamadas recientes más última):
...
ValueError: el valor debe ser > 0

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.

Una mirada adecuada a 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.

Esta es la firma completa del constructor de la propiedad :

606 | Capítulo 19: Propiedades y atributos dinámicos


Machine Translated by Google

propiedad(fget=Ninguno, fset=Ninguno, fdel=Ninguno, doc=Ninguno)

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 :

def __init__(self, descripción, peso, precio): self.description =


descripción self.weight = peso self.price = precio

def subtotal(self): return


self.weight * self.price

def get_weight(self): return


self.__weight

def set_weight(self, valor): si valor >


0:
self.__weight = valor else:
raise ValueError('el valor debe
ser > 0')

peso = propiedad (obtener_peso, establecer_peso)

Un captador simple.

Un setter llano.

Cree la propiedad y asígnela a un atributo de clase pública.

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.

Una mirada adecuada a las propiedades | 607


Machine Translated by Google

Las propiedades anulan los atributos de la instancia

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.

Ejemplo 19-19. Atributo de instancia sombras clase atributo de datos

>>> clase Clase: #


... data = 'los datos de la clase attr'
... @propiedad
... def prop(uno mismo):
... devolver 'el valor prop'
...
>>> obj = Clase()
>>> vars(obj) # {}

>>> obj.data # 'los


datos de la clase attr'
>>> obj.datos = 'barra' #
>>> vars(obj) # {'datos':
'barra'}
>>> obj.datos #
'barra'
>>> Class.data # 'el
atributo de datos de clase'

Defina Clase con dos atributos de clase: el atributo de datos de datos y la prop
propiedad.

vars devuelve el __dict__ de obj, mostrando que no tiene atributos de instancia.

La lectura de obj.data recupera el valor de Class.data.

Escribir en obj.data crea un atributo de instancia.

Inspeccione la instancia para ver el atributo de la instancia.

Ahora, la lectura de obj.data recupera el valor del atributo de la instancia. Cuando


leído de la instancia de obj , los datos de la instancia sombrean los datos de la clase .
El atributo Class.data está intacto.

Ahora, intentemos anular el atributo prop en la instancia de obj . Retomando lo anterior


sesión de consola, tenemos el Ejemplo 19-20.

608 | Capítulo 19: Propiedades y atributos dinámicos


Machine Translated by Google

Ejemplo 19-20. El atributo de instancia no oculta la propiedad de clase (continuación de


Ejemplo 19-19)

>>> 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'}

>>> obj.prop # 'el


valor de prop'
>>> Clase.prop = 'baz' # >>>
obj.prop # 'foo'

Leer prop directamente de Class recupera el objeto de propiedad en sí mismo, sin


ejecutando su método getter.

La lectura de obj.prop ejecuta el captador de propiedades.

Intentar establecer un atributo prop de instancia falla.

Poner 'prop' directamente en el obj.__dict__ funciona.

Podemos ver que obj ahora tiene dos atributos de instancia: attr y prop.

Sin embargo, la lectura de obj.prop aún ejecuta el captador de propiedades. la propiedad no es


sombreado por un atributo de instancia.

Sobrescribir Class.prop destruye el objeto de propiedad.

Ahora obj.prop recupera el atributo de la instancia. Class.prop no es una propiedad


más, por lo que ya no anula obj.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"'

>>> del Class.data #

Una mirada adecuada a las propiedades | 609


Machine Translated by Google

>>> obj.datos #
'barra'

obj.data recupera el atributo de datos de la instancia.


Class.data recupera el atributo de datos de clase.

Sobrescriba Class.data con una nueva propiedad. obj.data

ahora está sombreado por la propiedad Class.data .

Eliminar la propiedad.

obj.data ahora vuelve a leer el atributo de datos de la instancia .

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 :

peso = propiedad(get_weight, set_weight, doc='peso en kilogramos')

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.

610 | Capítulo 19: Propiedades y atributos dinámicos


Machine Translated by Google

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.

Ejemplo 19-22. Documentación para una propiedad.


clase Foo:

@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.

Codificación de una fábrica de propiedades

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.

Ejemplo 19-23. bulkfood_v2prop.py: la fábrica de propiedades de cantidad en uso


class LineItem:
peso = cantidad('peso') precio =
cantidad('precio')

Codificación de una fábrica de propiedades | 611


Machine Translated by Google

def __init__(self, descripción, peso, precio): self.description


= descripción self.weight = peso self.price = 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.

Esta segunda convocatoria construye otra propiedad personalizada, el precio.

Aquí la propiedad ya está activa, asegurándose de que se rechace un peso negativo o 0 .

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.

Mejorar la propiedad de cantidad para que el usuario no necesite volver


a escribir el nombre del atributo es un problema de metaprogramación no
trivial. Veremos una solución en el Capítulo 20, pero las soluciones reales
tendrán que esperar hasta el Capítulo 21, porque requieren un decorador
de clases o una metaclase.

El ejemplo 19-24 enumera la implementación de la fábrica de propiedad de cantidad.14

Ejemplo 19-24. bulkfood_v2prop.py: la fábrica de propiedades de cantidad

def cantidad(nombre_de_almacenamiento):

def qty_getter(instancia):
devolver instancia.__dict__[nombre_de_almacenamiento]

def qty_setter(instancia, valor): si valor


> 0:
instancia.__dict__[nombre_de_almacenamiento] = valor

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

612 | Capítulo 19: Propiedades y atributos dinámicos


Machine Translated by Google

else:
aumentar ValueError('el valor debe ser > 0')

propiedad devuelta (qty_getter, qty_setter)

El argumento nombre_de_almacenamiento determina dónde se almacenan los datos de cada


propiedad; para el peso, el nombre de almacenamiento será 'peso'.

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.

El valor se almacena directamente en la instancia.__dict__, omitiendo nuevamente la propiedad.

Cree un objeto de propiedad personalizado y devuélvalo.

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.

Ejemplo 19-25. bulkfood_v2prop.py: la fábrica de propiedades de cantidad

>>> nuez moscada = LineItem('Nuez moscada de las Molucas',


8, 13.95) >>> nuez moscada.peso, nuez moscada.precio (8,
13.95) >>> sorted(vars(nuez moscada).items()) [('descripción' ,
'nuez moscada de las Molucas'), ('precio', 13,95), ('peso', 8)]

Leer el peso y el precio a través de las propiedades que ocultan los atributos de la instancia del
mismo nombre.

Codificación de una fábrica de propiedades | 613


Machine Translated by Google

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.

Ahora terminemos la discusión de las propiedades con el tema de la eliminación de atributos.

Gestión de la eliminación de atributos

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”

clase Caballero Negro:

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!",

614 | Capítulo 19: Propiedades y atributos dinámicos


Machine Translated by Google

"Está bien, lo llamaremos empate".]

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

Los documentos en blackknight.py están en el Ejemplo 19-27.

Ejemplo 19-27. blackknight.py: pruebas documentales para el Ejemplo 19-26 (el Caballero Negro nunca
reconoce la derrota)

>>> caballero = BlackKnight () >>>


caballero.miembro siguiente miembro
es:
'un brazo'
>>> del caballero.miembro
BLACK KNIGHT (pierde un brazo)
-- No es más que un rasguño.
>>> del knight.member BLACK
KNIGHT (pierde otro brazo)
-- Es sólo una herida superficial. >>>
del knight.member BLACK KNIGHT
(pierde una pierna)
-- ¡Soy invencible!
>>> del knight.member BLACK
KNIGHT (pierde otra pierna)
-- Muy bien, lo llamaremos empate.

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 :

miembro = propiedad(miembro_captador, fdel=miembro_eliminador)

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.

Gestión de eliminación de atributos | 615


Machine Translated by Google

Atributos y funciones esenciales para el manejo de atributos


A lo largo de este capítulo, e incluso antes en el libro, hemos utilizado algunas de las funciones integradas
y métodos especiales que proporciona Python para tratar con atributos dinámicos.
Esta sección brinda una descripción general de ellos en un solo lugar, porque su documentación está
dispersa en los documentos oficiales.

Atributos especiales que afectan el manejo de atributos


El comportamiento de muchas de las funciones y métodos especiales enumerados en las siguientes
secciones depende de tres atributos especiales:

__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.

Funciones integradas para el manejo de atributos


Estas cinco funciones integradas realizan lectura, escritura e introspección de atributos de objetos:

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í.

616 | Capítulo 19: Propiedades y atributos dinámicos


Machine Translated by Google

__name__ tampoco están listados por dir . Si no se proporciona el argumento de objeto opcional , dir
enumera los nombres en el ámbito actual.

getattr(objeto, nombre[, predeterminado])


Obtiene el atributo identificado por la cadena de nombre del objeto. Esto puede obtener un atributo de
la clase del objeto o de una superclase. Si no existe dicho atributo, getattr genera AttributeError o
devuelve el valor predeterminado , si se proporciona.

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”.

setattr(objeto, nombre, valor)


Asigna el valor al atributo nombrado del objeto, si el objeto lo permite. Esto puede crear un nuevo
atributo o sobrescribir uno existente.

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.

Métodos especiales para el manejo de atributos Cuando

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.

Atributos y funciones esenciales para el manejo de atributos | 617


Machine Translated by Google

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

__setattr__(yo, nombre, valor)


Siempre llamado cuando hay un intento de establecer el atributo nombrado. La notación
de puntos y el setattr integrado activan este método; por ejemplo, tanto obj.attr = 42
como setattr(obj, 'attr', 42) activan Class.__setattr__(obj, 'attr', 42).

En la práctica, debido a que se llaman incondicionalmente y afectan


prácticamente a todos los accesos a los atributos, los métodos especiales
__getattribute__ y __setattr__ son más difíciles de usar correctamente que
__getattr__, que solo maneja nombres de atributos que no existen.
Usar propiedades o descriptores es menos propenso a errores que definir
estos métodos especiales.

Esto concluye nuestra inmersión en propiedades, métodos especiales y otras técnicas para
codificar atributos dinámicos.

618 | Capítulo 19: Propiedades y atributos dinámicos


Machine Translated by Google

Resumen del capítulo


Comenzamos nuestra cobertura de atributos dinámicos mostrando ejemplos prácticos de clases simples
para facilitar el manejo de una fuente de datos JSON. El primer ejemplo fue la clase FrozenJSON que
convirtió dictados y listas anidadas en instancias anidadas de FrozenJSON y listas de ellas. El código

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ÿ

Resumen del capítulo | 619


Machine Translated by Google

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.

620 | Capítulo 19: Propiedades y atributos dinámicos


Machine Translated by Google

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".

17. Alex Martelli, Python en pocas palabras, 2E (O'Reilly), pág. 101.

Lectura adicional | 621


Machine Translated by Google

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

__nuevo__ es mejor que nuevo

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.

622 | Capítulo 19: Propiedades y atributos dinámicos


Machine Translated by Google

biblioteca usó la misma convención. Te estoy mirando, collections.OrderedDict


y collections.defaultdict.

Lectura adicional | 623


Machine Translated by Google
Machine Translated by Google

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.

Ejemplo de descriptor: validación de atributos


Como vimos en “Codificación de una fábrica de propiedades” en la página 611, una fábrica de propiedades es
una forma de evitar la codificación repetitiva de getters y setters mediante la aplicación de patrones de
programación funcional. Una fábrica de propiedades es una función de orden superior que crea un conjunto parametrizado de

1. Raymond Hettinger, Descriptor HowTo Guide.

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.

Continuaremos la serie de ejemplos de LineItem donde la dejamos, en “Codificación de una fábrica de


propiedades” en la página 611, refactorizando la fábrica de propiedades de cantidad en una clase de
descriptor de cantidad .

LineItem Take #3: Un descriptor simple Una clase

que implementa un método __get__ , __set__ o __delete__ es un descriptor.


Utiliza un descriptor declarando instancias de él como atributos de clase de otra clase.

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.

De ahora en adelante, usaré las siguientes definiciones:

Clase de descriptor Una clase que implementa el protocolo


de descriptor. Esa es la cantidad en la figura 20-1.

Clase administrada
La clase en la que las instancias del descriptor se declaran como atributos de clase: elemento
LineI en la figura 20-1.

626 | Capítulo 20: Descriptores de atributos


Machine Translated by Google

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.

Ejemplo de descriptor: Validación de atributos | 627


Machine Translated by Google

Introducción a la notación Mills & Gizmos

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.

Ejemplo 20-1. bulkfood_v3.py: los descriptores de cantidad administran atributos en LineItem

cantidad de clase :

def __init__(self, nombre_de_almacenamiento):


self.nombre_almacenamiento = nombre_almacenamiento

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.

628 | Capítulo 20: Descriptores de atributos


Machine Translated by Google

def __set__(self, instancia, valor): si valor


> 0:
instancia.__dict__[self.storage_name] = valor más:
aumentar ValueError('el valor debe ser > 0')

class LineItem:
peso = Cantidad('peso') precio =
Cantidad('precio')

def __init__(self, descripción, peso, precio): self.description


= descripción self.weight = peso self.price = precio

def subtotal(self):
return self.weight * self.price

Descriptor es una función basada en protocolos; no se necesitan subclases para implementar


una.

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

asignación al atributo administrado.


Aquí, self es la instancia del descriptor (es decir, LineItem.weight o LineI tem.price), la instancia
es la instancia administrada (una instancia de LineItem ) y el valor es el valor que se asigna.

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.

La primera instancia del descriptor está vinculada al atributo de peso .

La segunda instancia del descriptor está vinculada al atributo de precio .

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

Ejemplo de descriptor: Validación de atributos | 629


Machine Translated by Google

>>> trufa = LineItem(' Trufa blanca', 100, 0)


Rastreo (llamadas recientes más última):
...
ValueError: el valor debe ser > 0

Al codificar un método __set__ , debe tener en cuenta lo que significan


los argumentos self e instancia : self es la instancia descriptora e instancia
es la instancia administrada. Los descriptores que administran los atributos
de las instancias deben almacenar valores en las instancias administradas.
Es por eso que Python proporciona el argumento de la instancia a los
métodos del descriptor.

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

la tentadora pero mala alternativa sería:

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

# métodos restantes como antes

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

630 | Capítulo 20: Descriptores de atributos


Machine Translated by Google

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.

LineItem Take #4: Nombres de atributos de almacenamiento automático Para

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.

Para generar el nombre_de_almacenamiento, comenzamos con un prefijo '_Cantidad#' y concatenamos un


número entero: el valor actual de un atributo de clase Cantidad.__contador que incrementaremos cada vez que
se adjunte una nueva instancia de descriptor de cantidad a una clase. El uso del carácter hash en el prefijo
garantiza que el nombre_de_almacenamiento no entre en conflicto con los atributos creados por el usuario
usando la notación de puntos, porque nutmeg._Quantity#0 no es una sintaxis de Python válida. Pero siempre
podemos obtener y establecer atributos con tales identificadores "no válidos" utilizando las funciones integradas
getattr y setattr , o introduciendo la instancia __dict__.
El ejemplo 20-2 muestra la nueva implementación.

Ejemplo 20-2. bulkfood_v4.py: cada descriptor de cantidad obtiene un nombre de almacenamiento único

Clase Cantidad:
__contador = 0

def __init__(self): cls


= self.__class__ prefijo
= cls.__name__ index =
cls.__counter

Ejemplo de descriptor: Validación de atributos | 631


Machine Translated by Google

self.storage_name = '_{}#{}'.format(prefijo, índice)


cls.__contador += 1

def __get__(self, instancia, propietario):


return getattr(instancia, self.storage_name)

def __set__(self, instancia, valor): si valor


> 0:
setattr(instancia, self.storage_name, value) else:
raise ValueError('el valor debe ser > 0')

clase LineItem:
peso = Cantidad()
precio = Cantidad()

def __init__(self, descripción, peso, precio): self.description


= descripción self.weight = peso self.price = precio

def subtotal(self):
return self.weight * self.price

__contador es un atributo de clase de Cantidad, que cuenta el número de instancias de


Cantidad .

cls es una referencia a la clase Cantidad .

El nombre_de_almacenamiento para cada instancia del descriptor es único porque se crea a


partir del nombre de la clase del descriptor y el valor del contador actual (p. ej., _Cantidad #0).
Incrementar __contador.

Necesitamos implementar __get__ porque el nombre del atributo administrado no es el


mismo que el nombre_de_almacenamiento. El argumento del propietario se explicará en breve.

Utilice la función integrada getattr para recuperar el valor de la instancia.


Use el setattr incorporado para almacenar el valor en la instancia.

Ahora no necesitamos pasar el nombre del atributo administrado al constructor Cantidad .


Ese era el objetivo de esta versión.

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.

632 | Capítulo 20: Descriptores de atributos


Machine Translated by Google

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:

>>> from bulkfood_v4 import LineItem >>>


cocos = LineItem('coco brasileño', 20, 17.95 ) >>> cocos.peso ,
cocos.precio (20, 17.95) >>> getattr(pasas, '_Cantidad#0' ),
getattr(pasas, '_Cantidad#1') (20, 17.95)

Si quisiéramos seguir la convención que usa Python para manipular


nombres (p. ej., _LineItem__quantity0) , tendríamos que saber el nombre
de la clase administrada (es decir, LineItem), pero el cuerpo de una
definición de clase se ejecuta antes de que se construya la clase en sí . por
el intérprete, por lo que no tenemos esa información cuando se crea cada
instancia del descriptor. Sin embargo, en este caso, no es necesario incluir
el nombre de la clase administrada para evitar la sobrescritura accidental
en las subclases: la clase de descriptor __counter se incrementará cada
vez que se instancia un nuevo descriptor, lo que garantiza que cada nombre
de almacenamiento será único en todas las instancias. clases administradas.

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:

>>> from bulkfood_v4 import LineItem >>>


LineItem.weight Traceback (última llamada
más reciente):
...
Archivo ".../descriptors/bulkfood_v4.py", línea 54, en __get__
devuelve getattr(instancia, self.storage_name)
AttributeError: el objeto 'NoneType' no tiene el atributo '_Quantity#0'

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ÿ

Ejemplo de descriptor: Validación de atributos | 633


Machine Translated by Google

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

def __init__(self): cls


= self.__class__ prefix =
cls.__name__ index =
cls.__counter
self.storage_name = '_{}#{}'.format(prefijo, índice) cls.__counter
+= 1

def __get__(self, instancia, propietario): si


la instancia es None: return self

else:
devuelve getattr(instancia, self.storage_name)

def __set__(self, instancia, valor): si valor


> 0:
setattr(instancia, self.storage_name, value) else:
raise ValueError('el valor debe ser > 0')

Si la llamada no fue a través de una instancia, devuelve el propio descriptor.

De lo contrario, devuelva el valor del atributo gestionado, como de costumbre.

Probando el Ejemplo 20-3, esto es lo que vemos:

>>> from bulkfood_v4b import LineItem


>>> LineItem.price <bulkfood_v4b.Quantity
object at 0x100721be0> >>> br_nuts = LineItem('Nueces
de Brasil ', 10, 34.95) >>> br_nuts.price 34.95

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.

634 | Capítulo 20: Descriptores de atributos


Machine Translated by Google

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

importar model_v4c como modelo

clase LineItem:
peso = modelo.Cantidad() precio
= modelo.Cantidad()

def __init__(self, descripción, peso, precio): self.description =


descripción self.weight = peso self.price = precio

def subtotal(self): return


self.weight * self.price

Importe el módulo model_v4c , dándole un nombre más amigable.

Poner modelo.Cantidad a usar.

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.

Como se ha implementado hasta ahora, el descriptor de cantidad funciona bastante bien.


Su único inconveniente real es el uso de nombres de almacenamiento generados como
_Quantity#0, lo que dificulta la depuración para los usuarios. Pero la asignación
automática de nombres de almacenamiento que se asemejan a los nombres de atributos
administrados requiere un decorador de clases o una metaclase, temas que remitiremos
al Capítulo 21.

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.

Fábrica de propiedades frente a clase de descriptor

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

Ejemplo de descriptor: Validación de atributos | 635


Machine Translated by Google

excepto AttributeError:
cantidad.contador = 0

nombre_de_almacenamiento = '_{}:{}'.format('cantidad', cantidad.contador)

def qty_getter(instancia):
devuelve getattr(instancia, nombre_de_almacenamiento)

def qty_setter(instancia, valor): si valor


> 0:
setattr(instancia, nombre_de_almacenamiento, valor)
de lo contrario: aumentar ValueError('el valor debe ser
> 0')

propiedad devuelta (qty_getter, qty_setter)

Sin argumento 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í.

Si cantidad.contador no está definido, configúrelo en 0.

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__.

¿Asi que cual prefieres? ¿ Ejemplo 20-2 o Ejemplo 20-5?

Prefiero el enfoque de clase de descriptor principalmente por dos razones:

• 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.

636 | Capítulo 20: Descriptores de atributos


Machine Translated by Google

Toma de línea de pedido n.º 5: un nuevo tipo de descriptor a

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.

Luego, reescribiremos la cantidad e implementaremos NonBlank heredando de Validated y simplemente


codificando los métodos de validación . La figura 20-5 muestra la configuración.

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.

Ejemplo de descriptor: Validación de atributos | 637


Machine Translated by Google

En este caso, la operación abstracta es la validación. El ejemplo 20-6 enumera la implementación


de las clases en la figura 20-5.

Ejemplo 20-6. model_v5.py: las clases descriptoras refactorizadas

importar abc

clase
AutoAlmacenamiento: __contador = 0

def __init__(self): cls =


self.__class__ prefix =
cls.__name__ index =
cls.__counter self.storage_name
= '_{}#{}'.format(prefijo, índice) cls.__counter += 1

def __get__(self, instancia, propietario): si la


instancia es None: return self else: return
getattr(instancia, self.storage_name)

def __set__(self, instancia, valor): setattr(instancia,


self.storage_name, valor)

clase validada (abc.ABC, AutoStorage):

def __set__(auto, instancia, valor):


valor = self.validate(instancia, valor)
super().__set__(instancia, valor)

@abc.abstractmethod def
validar(auto, instancia, valor):
"""devolver valor validado o generar ValueError"""

cantidad de clase (validada): """un


número mayor que cero"""

def validar(auto, instancia, valor): si el valor <= 0:

aumentar ValueError('el valor debe ser > 0')


valor devuelto

clase no en blanco (validado):


"""una cadena con al menos un carácter que no sea un espacio"""

def validar(auto, instancia, valor):

638 | Capítulo 20: Descriptores de atributos


Machine Translated by Google

value = value.strip() if
len(value) == 0: raise
ValueError('el valor no puede estar vacío o en blanco')
valor devuelto

AutoStorage proporciona la mayor parte de la funcionalidad del descriptor de cantidad


anterior ... ...excepto la validación.

Validado es abstracto pero también hereda de AutoStorage.

__set__ delega la validación a un método de


validación ... ...luego usa el valor devuelto para invocar __set__ en una superclase,
que realiza el almacenamiento real.
En esta clase, validar es un método abstracto.

Cantidad y No en blanco heredan de Validado.

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.

Ejemplo 20-7. bulkfood_v5.py: LineItem utilizando descriptores de cantidad y no en blanco


importar model_v5 como modelo

clase LineItem:
descripción = modelo.NonBlank() peso
= modelo.Cantidad() precio =
modelo.Cantidad()

def __init__(self, descripción, peso, precio): self.description =


descripción self.weight = peso self.price = precio

def subtotal(self): return


self.weight * self.price

Importe el módulo model_v5 , dándole un nombre más amigable.

Ponga model.NonBlank en uso. El resto del código no se modifica.

Ejemplo de descriptor: Validación de atributos | 639


Machine Translated by Google

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.

Descriptores anulados frente a no anulados


Recuerde que existe una asimetría importante en la forma en que Python maneja los atributos.
La lectura de un atributo a través de una instancia normalmente devuelve el atributo definido en la
instancia, pero si no existe tal atributo en la instancia, se recuperará un atributo de clase.
Por otro lado, la asignación a un atributo en una instancia normalmente crea el atributo en la instancia,
sin afectar en absoluto a la clase.

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.

Cada método __get__ y __set__ en el ejemplo 20-8 llama a


print_args para que sus invocaciones se muestren de forma legible.
Comprender print_args y las funciones auxiliares cls_name y
display no es importante, así que no se distraiga con ellas.

Ejemplo 20-8. descriptorkinds.py: clases simples para estudiar comportamientos de anulación de


descriptores

### funciones auxiliares solo para visualización ###

def cls_name(obj_or_cls):
cls = tipo(obj_or_cls) si cls
es tipo: cls = obj_or_cls
return
cls.__name__.split('.')[-1]

def mostrar(obj): cls


= tipo(obj) si cls
es tipo: return
'<clase {}>'.format(obj.__name__) elif cls in
[tipo(Ninguno), int]: return repr(obj)

más:
devuelve '<{} objeto>'.format(cls_name(obj))

def print_args(nombre, *args):


pseudo_args = ', '.join(display(x) for x in args)

640 | Capítulo 20: Descriptores de atributos


Machine Translated by Google

print('-> {}.__{}__({})'.format(cls_name(args[0]), nombre, pseudo_args))

### clases esenciales para este ejemplo ###

Anulación de clase :
"""también conocido como descriptor de datos o descriptor forzado"""

def __get__(self, instancia, propietario):


print_args('get', self, instancia, propietario)

def __set__(self, instancia, valor):


print_args('set', self, instancia, valor)

clase OverridingNoGet:
"""un descriptor anulado sin ``__get__``"""

def __set__(self, instancia, valor):


print_args('set', self, instancia, valor)

class NonOverriding:
"""también conocido como descriptor no data o shadowable"""

def __get__(self, instancia, propietario):


print_args('get', self, instancia, propietario)

Clase
administrada: over =
Overriding( ) over_no_get =
OverridingNoGet( ) non_over = NonOverriding()

def spam(auto):
print('-> Managed.spam({})'.format(display(self)))

Una clase típica de descriptor principal con __get__ y __set__.

La función print_args es llamada por cada método descriptor en este ejemplo.

Un descriptor principal sin un método __get__ .

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.

En las siguientes secciones, examinaremos el comportamiento de las lecturas y escrituras de atributos


en la clase administrada y una instancia de la misma, pasando por cada uno de los diferentes descriptores
definidos.

Descriptores anulados frente a no anulados | 641


Machine Translated by Google

Descriptor de anulación Un

descriptor que implementa el método __set__ se denomina descriptor de anulación porque,


aunque es un atributo de clase, un descriptor que implementa __set__ anulará los intentos de
asignar atributos de instancia. Así es como se implementó el Ejemplo 20-2 .
Las propiedades también anulan los descriptores: si no proporciona una función de
establecimiento, el __set__ predeterminado de la clase de propiedad generará AttributeError
para indicar que el atributo es de solo lectura. Dado el código del ejemplo 20-8, los experimentos
con un descriptor principal se pueden ver en el ejemplo 20-9.

Ejemplo 20-9. Comportamiento de un descriptor overriding: obj.over es una instancia de


Overriding (Ejemplo 20-8)
>>> obj = Administrado()
>>> obj.over ->
Anular.__get__(< Objeto anulado>, <Objeto administrado >, <Clase
administrada>)
>>> Managed.over
-> Overriding.__get__(< Objeto de reemplazo>, Ninguno, <clase administrado>)
>>> obj.over = 7 -> Overriding.__set__(< Objeto de reemplazo>, <Objeto
administrado >, 7) >>> obj.over -> Overriding.__get__(< Objeto de reemplazo>,
<Objeto administrado >, <clase administrada>) >>> obj.__dict__['over'] = 8
>>> vars(obj) {'over ': 8} >>> obj.over -> Overriding.__get__(< Objeto de
anulación>, <Objeto administrado >, <clase administrada>)

Cree un objeto administrado para la

prueba. obj.over activa el método __get__ del descriptor , pasando la instancia


administrada obj como segundo argumento.

Managed.over activa el método __get__ del descriptor , pasando None como el


segundo argumento (instancia).
La asignación a obj.over activa el método __set__ del descriptor , pasando el valor 7
como último argumento.

La lectura de obj.over todavía invoca el método __get__ del descriptor.

Omitiendo el descriptor, estableciendo un valor directamente en el obj.__dict__.

Verifique que el valor esté en obj.__dict__, debajo de la tecla over .


Sin embargo, incluso con un atributo de instancia denominado over, el descriptor
Managed.over anula los intentos de leer obj.over.

642 | Capítulo 20: Descriptores de atributos


Machine Translated by Google

Sobreescribir descriptor sin __get__ Por lo

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.

Ejemplo 20-10. Sobrescribir descriptor sin get: obj.over_no_get es una instancia de


OverridingNoGet (Ejemplo 20-8)
>>> obj.over_no_get
<__main__.OverridingNoGet object at 0x665bcc> >>>
Managed.over_no_get <__main__.OverridingNoGet object
at 0x665bcc> >>> obj.over_no_get = 7 ->
OverridingNoGet.__set__(<OverridingNoGet object>,
<Managed object >, 7) >>> obj.over_no_get <__main__.OverridingNoGet objeto en 0x665bcc>
>>> obj.__dict__['over_no_get'] = 9 >>> obj.over_no_get

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__ .

Debido a que nuestro __set__ no realiza cambios, leer obj.over_no_get nuevamente


recupera la instancia del descriptor de la clase administrada.
Pasando por la instancia __dict__ para establecer un atributo de instancia llamado
over_no_get.

Ahora ese atributo de instancia over_no_get sombrea el descriptor, pero solo para
lectura.

Descriptores anulados frente a no anulados | 643


Machine Translated by Google

Intentar asignar un valor a obj.over_no_get todavía pasa por el descriptor


establecer.

Pero para la lectura, ese descriptor está sombreado siempre que haya un atributo de
instancia del mismo nombre.

Descriptor no anulado Si un

descriptor no implementa __set__, entonces es un descriptor no anulado. Establecer un atributo de


instancia con el mismo nombre sombreará el descriptor, haciéndolo ineficaz para manejar ese
atributo en esa instancia específica. Los métodos se implementan como descriptores no anuladores.
El ejemplo 20-11 muestra la operación de un descriptor no predominante.

Ejemplo 20-11. Comportamiento de un descriptor nonoverriding: obj.non_over es una instancia de


NonOverriding (Ejemplo 20-8)
>>> obj = Administrado()
>>> obj.non_over ->
NonOverriding.__get__(<NonOverriding object>, <Managed object>, <class
Managed>) >>> obj.non_over = 7 >>> obj.non_over 7 >>> Managed.non_over
-> NonOverriding.__get__(<NonOverriding object>, None, <class Managed>)
>>> del obj.non_over >>> obj.non_over -> NonOverriding.__get__(<NonOverriding
object>, <Managed objeto>, <clase gestionada>)

obj.non_over activa el método __get__ del descriptor , pasando obj como segundo argumento.

Managed.non_over es un descriptor no anulado, por lo que no hay __set__ que interfiera


con esta asignación.
El obj ahora tiene un atributo de instancia llamado non_over, que sombrea el atributo del
descriptor del mismo nombre en la clase Administrada .

El descriptor Managed.non_over todavía está allí y detecta este acceso a través de la clase.

Si se elimina el atributo de instancia non_over ...

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.

644 | Capítulo 20: Descriptores de atributos


Machine Translated by Google

Los colaboradores y autores de Python usan diferentes términos cuando


discuten estos conceptos. Los descriptores anulados también se denominan
descriptores de datos o descriptores forzados. Los descriptores no anulados
también se conocen como descriptores sin datos o descriptores sombreables.

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.

Sobrescribir un descriptor en la clase

Independientemente de si un descriptor se sobrescribe o no, se puede sobrescribir mediante la


asignación a la clase. Esta es una técnica de parcheo de monos, pero en el Ejemplo 20-12 los
descriptores se reemplazan por números enteros, lo que rompería efectivamente cualquier clase que
dependiera de los descriptores para una operación adecuada.

Ejemplo 20-12. Cualquier descriptor se puede sobrescribir en la propia clase.

>>> obj = Administrado()


>>> Managed.over = 1
>>> Managed.over_no_get = 2 >>>
Managed.non_over = 3 >>>
obj.over, obj.over_no_get, obj.non_over (1, 2, 3)

Cree una nueva instancia para realizar pruebas posteriores.

Sobrescriba los atributos del descriptor en la clase.

Los descriptores realmente se han ido.

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.

Para controlar la configuración de los atributos en una clase, debe adjuntar


descriptores a la clase de la clase, en otras palabras, la metaclase. De forma
predeterminada, la metaclase de las clases definidas por el usuario es tipo y
no puede agregar atributos al tipo. Pero en el Capítulo 21, crearemos
nuestras propias metaclases.

Descriptores anulados frente a no anulados | 645


Machine Translated by Google

Centrémonos ahora en cómo se utilizan los descriptores para implementar métodos en Python.

Los métodos son descriptores


Una función dentro de una clase se convierte en un método vinculado porque todas las funciones definidas por

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.

Ejemplo 20-13. Un método es un descriptor no predominante

>>> obj = Managed() >>>


obj.spam <método
enlazado Managed.spam of <descriptorkinds.Managed object at 0x74c80c>> >>> Managed.spam
<función Managed.spam at 0x734734> >>> obj.spam = 7 >>> obj.correo no deseado

La lectura de obj.spam recupera un objeto de método enlazado.

Pero la lectura de Managed.spam recupera una función.

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.

Ejemplo 20-14. method_is_descriptor.py: una clase de texto, derivada de UserString

importar colecciones

clase Texto(colecciones.CadenaUsuario):

def __repr__(self):
devuelve 'Texto({!r})'.format(self.data)

646 | Capítulo 20: Descriptores de atributos


Machine Translated by Google

def inversa(auto):
devolver uno mismo[::-1]

Ahora investiguemos el método Text.reverse . Vea el Ejemplo 20-15.

Ejemplo 20-15. Experimentos con un método

>>> palabra = Texto('adelante')


>>> palabra
Texto('adelante')
>>> palabra.reverse()
Texto('dibujo')
>>> Texto.reverse(Texto('hacia atrás'))
Texto('drawkcab')
>>> type(Text.reverse), type(word.reverse) (<clase 'función'>,
<clase 'método'>)
>>> list(map(Text.reverse, ['pagado', (10, 20, 30), Text('estresado')])) ['pañal', (30, 20, 10), Text('postres
')]
>>> Text.reverse.__get__(word) <método
enlazado Text.reverse of Text('forward')>
>>> Texto.reverso.__get__(Ninguno, Texto) <función
Texto.reverso en 0x101244e18>
>>> palabra.reversa
<método enlazado Text.reverse of Text('forward')>
>>> palabra.reversa.__self__
Text('adelante')
>>> palabra.reversa.__func__ es Texto.reversa
Verdadero

La repr de una instancia de Text parece una llamada al constructor de Text que haría
una instancia igual.

El método inverso devuelve el texto escrito al revés.


Un método llamado en la clase funciona como una función.

Tenga en cuenta los diferentes tipos: una función y un método.

Text.reverse opera como una función, incluso trabajando con objetos que no son
instancias de Texto.

Cualquier función es un descriptor no predominante. Llamando a su __get__ con una instancia


recupera un método vinculado a esa instancia.

Llamar a __get__ de la función con None como argumento de instancia recupera el


función en sí.

La expresión word.reverse en realidad invoca Text.reverse.__get__(palabra),


devolviendo el método enlazado.

El objeto del método enlazado tiene un atributo __self__ que contiene una referencia al
instancia en la que se llamó al método.

métodos son descriptores | 647


Machine Translated by Google

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.

Sugerencias de uso de descriptores

La siguiente lista aborda algunas consecuencias prácticas de las características del descriptor recién descritas:

Use la propiedad para mantenerlo simple


La propiedad incorporada en realidad crea descriptores anulados que implementan tanto __set__ como __get__,
incluso si no define un método setter. El __set__ predeterminado de una propiedad genera AttributeError: no se
puede establecer el atributo, por lo que una propiedad es la forma más fácil de crear un atributo de solo lectura,
evitando el problema descrito
Siguiente.

Los descriptores de solo lectura requieren __set__


Si usa una clase de descriptor para implementar un atributo de solo lectura, debe recordar codificar tanto __get__
como __set__; de lo contrario, establecer un atributo del mismo nombre en una instancia sombreará el descriptor.
El método __set__ de un atributo de solo lectura debería generar AttributeError con un mensaje adecuado.5

Los descriptores de validación pueden funcionar solo con __set__


En un descriptor diseñado solo para validación, el método __set__ debe verificar el argumento de valor que
obtiene y, si es válido, configurarlo directamente en la instancia __dict__ usando el nombre de la instancia del
descriptor como clave. De esa forma, leer el atributo con el mismo nombre de la instancia será lo más rápido
posible, ya que no requerirá un __get__. Consulte el código del ejemplo 20-1.

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.

648 | Capítulo 20: Descriptores de atributos


Machine Translated by Google

El almacenamiento en caché se puede hacer de


manera eficiente solo con __get__ Si codifica solo el método __get__ , tiene un descriptor
no anulado. Estos son útiles para realizar cálculos costosos y luego almacenar en caché el
resultado estableciendo un atributo con el mismo nombre en la instancia. El atributo de
instancia del mismo nombre sombreará el descriptor, por lo que el acceso posterior a ese
atributo lo obtendrá directamente de la instancia __dict__ y ya no activará el descriptor __get__ .

Los métodos no especiales pueden ser sombreados por atributos


de instancia Debido a que las funciones y los métodos solo implementan __get__, no
manejan los intentos de establecer atributos de instancia con el mismo nombre, por lo que
una asignación simple como my_obj.the_method = 7 significa que el acceso adicional a
the_method a través de ese instancia recuperará el número 7, sin afectar a la clase ni a otras instancias.
Sin embargo, este problema no interfiere con los métodos especiales. El intérprete solo
busca métodos especiales en la propia clase, en otras palabras, repr(x) se ejecuta como
x.__class__.__repr__(x), por lo que un atributo __repr__ definido en x no tiene efecto sobre
repr(x). Por la misma razón, la existencia de un atributo llamado __getattr__ en una instancia
no subvertirá el algoritmo habitual de acceso a atributos.

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.

La clase FrozenJSON del ejemplo 19-6 está a salvo de los métodos de


sombreado de atributos de instancia porque sus únicos métodos son los
métodos especiales y el método de la clase de compilación . Los métodos
de clase son seguros siempre que se acceda a ellos a través de la clase,
como hice con FrozenJSON.build en el ejemplo 19-6, que luego se
reemplazó por __new__ en el ejemplo 19-7. La clase Record (Ejemplos
19-9 y 19-11) y las subclases también son seguras: solo usan métodos
especiales, métodos de clase, métodos estáticos y propiedades. Las
propiedades son descriptores de datos, por lo que no pueden ser anuladas por atributos de instancia.

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.

Consejos de uso de descriptores | 649


Machine Translated by Google

Cadena de documentación del descriptor y eliminación anulada

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.

650 | Capítulo 20: Descriptores de atributos


Machine Translated by Google

clase de descriptor. La codificación de una clase de descriptor tonta con __delete__ se deja como un ejercicio para el
lector pausado.

Resumen del capítulo


El primer ejemplo de este capítulo fue una continuación de los ejemplos de LineItem del Capítulo 19. En el Ejemplo
20-1, reemplazamos propiedades con descriptores. Vimos que un descriptor es una clase que proporciona instancias
que se implementan como atributos en la clase administrada. Discutir este mecanismo requirió una terminología
especial, introduciendo términos como instancia administrada y atributo de almacenamiento.

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.

A continuación, estudiamos una categoría particular de descriptores no predominantes: los métodos.


Las pruebas de la consola revelaron cómo una función adjunta a una clase se convierte en un método cuando se
accede a través de una instancia, aprovechando el protocolo del descriptor.

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.

Resumen del capítulo | 651


Machine Translated by Google

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

El problema con uno mismo

“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'.

652 | Capítulo 20: Descriptores de atributos


Machine Translated by Google

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.

Lectura adicional | 653


Machine Translated by Google
Machine Translated by Google

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.

Este es un tema apasionante, y es fácil dejarse llevar. Así que


debo comenzar este capítulo con la siguiente advertencia: si no
está creando un marco, no debe escribir metaclases, a menos
que lo haga por diversión o para practicar los conceptos.

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

Comenzaremos revisando cómo crear una clase en tiempo de ejecución.

Una fábrica de clase

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:

>>> rex = Perro('Rex', 30, 'Bob')


>>> rey

<__main__.Objeto de perro en 0x2865bac>

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.

Ejemplo 21-1. Probando record_factory, una fábrica de clases simple

>>> Perro = record_factory('Perro', 'nombre peso dueño') >>> rex


= Perro('Rex', 30, 'Bob')
>>> rey

Perro(nombre='Rex', peso=30, dueño='Bob') >>>


= rexpeso ('Rex', 30)
nombre, peso, _ >>> nombre,
>>> "El perro de {2} pesa {1}kg".formato(*rex)

"El perro de Bob pesa 30kg"


>>> rex.peso = 32
>>> rey

Perro(nombre='Rex', peso=32, dueño='Bob')


>>> Perro.__mro__
(<clase 'fábricas.Perro'>, <clase 'objeto'>)

La firma de fábrica es similar a la de namedtuple: nombre de clase, seguido de nombres de atributos


en una sola cadena, separados por espacios o comas.

Buena repr.

656 | Capítulo 21: Metaprogramación de clases


Machine Translated by Google

Las instancias son iterables, por lo que se pueden desempaquetar convenientemente en la

asignación... ...o al pasar a funciones como formato.


Una instancia de registro es mutable.

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.

Ejemplo 21-2. record_factory.py: una fábrica de clases simple


def record_factory(cls_name, field_names):

intente: field_names = field_names.replace(',', ' ').split()


excepto AttributeError: # no .replace o .split
pasar # asumir que ya es una secuencia de identificadores
nombres_de_campo = tupla(nombres_de_campo)

def __init__(self, *args, **kwargs):


attrs = dict(zip(self.__slots__, args))
attrs.update(kwargs) for name, value in
attrs.items(): setattr(self, name, value)

def __iter__(self): for


name in self.__slots__:
rendimiento getattr(yo, nombre)

def __repr__(self):
valores = ', '.join('{}={!r}'.format(*i) for i in zip(self.__slots__,
self)) return '{}
({})' .format(self.__class__.__name__, valores)

cls_attrs = dict(__ranuras__ = nombres_de_campo ,


__init__ = __init__,
__iter__ = __iter__,
__repr__ = __repr__)

tipo de retorno (cls_name, (objeto,), cls_attrs)

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__.

Esta función se convertirá en el método __init__ en la nueva clase. Acepta argumentos


posicionales y/o de palabras clave.

2. Gracias a mi amigo JS Bueno por sugerir esta solución.

Una fábrica de clase | 657


Machine Translated by Google

Implemente un __iter__, por lo que las instancias de clase serán iterables; producir los valores
de campo en el orden dado por __slots__.

Producir la buena repr, iterando sobre __slots__ y self.

Ensamblar diccionario de atributos de clase.

Compile y devuelva la nueva clase, llamando al constructor de tipos .

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:

MiClase = tipo('MiClase', (MiSuperClase, MiMixin), {'x': 42,


'x2': lambda self: self.x * 2})

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:

clase MiClase(MiSuperClase, MiMixin): x =


42

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 .

658 | Capítulo 21: Metaprogramación de clases


Machine Translated by Google

Es una buena práctica evitar exec o eval para la metaprogramación en


Python. Estas funciones presentan serios riesgos de seguridad si se
alimentan con cadenas (incluso fragmentos) de fuentes no confiables.
Python ofrece suficientes herramientas de introspección para hacer que
exec y eval sean innecesarios la mayor parte del tiempo. Sin embargo, los
desarrolladores principales de Python eligieron usar exec al implementar
namedtuple. El enfoque elegido hace que el código generado para la clase
esté disponible en el atributo ._source .

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".

Un decorador de clase para personalizar descriptores


Cuando dejamos el ejemplo de LineItem en “LineItem Take #5: A New Descriptor Type” en la página
637, el problema de los nombres de almacenamiento descriptivos aún estaba pendiente: el valor de
atributos como el peso se almacenaba en un atributo de instancia llamado _Quantity#0, lo que hizo
que la depuración fuera un poco difícil. Puede recuperar el nombre de almacenamiento de un
descriptor en el Ejemplo 20-7 con las siguientes líneas:

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

Un decorador de clase para personalizar descriptores | 659


Machine Translated by Google

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.

Ejemplo 21-3. bulkfood_v6.py: LineItem usando descriptores de cantidad y no en blanco

importar model_v6 como modelo

@model.entity
class LineItem:
descripción = modelo.NonBlank()
peso = modelo.Cantidad() precio =
modelo.Cantidad()

def __init__(self, descripción, peso, precio): self.description


= descripción self.weight = peso self.price = precio

def subtotal(self):
return self.weight * self.price

El único cambio en esta clase es el decorador agregado.

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

Ejemplo 21-4. model_v6.py: un decorador de clase

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

El decorador obtiene la clase como argumento.

Iterar sobre dict que contiene los atributos de clase.

Si el atributo es uno de nuestros descriptores validados ...

660 | Capítulo 21: Metaprogramación de clases


Machine Translated by Google

…establezca el nombre_de_almacenamiento para usar el nombre de la clase del descriptor y el


nombre del atributo administrado (p. ej., _NonBlank#description).
Devuelve la clase modificada.

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 = LineItem(' Pasas doradas', 10, 6.95) >>>


dir(pasas)[:3]
['_NonBlank#description', '_Quantity#price', '_Quantity#weight']
>>> LineItem.description.storage_name
'_NonBlank#description' >>> pasas.description
'Golden pasas' >>> getattr(pasas,
'_NonBlank#description')

'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.

Qué sucede cuando: tiempo de importación versus tiempo de ejecución

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.

Qué sucede cuando: tiempo de importación versus tiempo de ejecución | 661


Machine Translated by Google

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:

la declaración de importación puede desencadenar todo tipo de Comportamiento de “tiempo de ejecución”.

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.

Los ejercicios de tiempo de evaluación

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

El módulo evaltime.py se importa de forma interactiva en la consola de Python:

>>> importar tiempo de evaluación

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.

662 | Capítulo 21: Metaprogramación de clases


Machine Translated by Google

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

print('<[1]> inicio del módulo evaltime')

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

clase Clase Cuatro (Clase Tres):


print('<[9]> cuerpo ClassFour')

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

Qué sucede cuando: tiempo de importación versus tiempo de ejecución | 663


Machine Translated by Google

cuatro.método_y()

print('<[14]> fin del módulo evaltime')

Ejemplo 21-7. evalsupport.py: módulo importado por evaltime.py


print('<[100]> inicio del módulo evalsupport')

def deco_alpha(cls):
print('<[200]> deco_alpha')

def interior_1(self):
print('<[300]> deco_alpha:inner_1')

cls.method_y = inner_1 return


cls

# BEGIN META_ALEPH
clase MetaAleph(tipo):
print('<[400]> cuerpo MetaAleph')

def __init__(cls, nombre, bases, dic):


imprimir('<[500]> MetaAleph.__init__')

def interior_2(self):
print('<[600]> MetaAleph.__init__:interior_2')

cls.method_z = interior_2 #
FIN META_ALEPH

print('<[700]> fin del módulo de soporte de evaluación')

Solución para el escenario #1

El ejemplo 21-8 es el resultado de importar el módulo evaltime.py en la consola de Python.

Ejemplo 21-8. Escenario n.º 1: importar evaltime en la consola de Python


>>> import evaltime
<[100]> inicio del módulo evalsupport
<[400]> cuerpo de MetaAleph
<[700]> final del módulo evalsupport <[1]>
inicio del módulo evaltime
<[2]> Cuerpo Clase Uno
<[6]> Cuerpo ClaseDos
<[7]> Cuerpo ClassThree
<[200]> deco_alpha
<[9]> cuerpo ClassFour
<[14]> final del módulo evaltime

664 | Capítulo 21: Metaprogramación de clases


Machine Translated by Google

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.

El cuerpo de la función MetaAleph sí se ejecuta.

El cuerpo de cada clase se ejecuta... ...incluidas

las clases anidadas.

La función de decorador se ejecuta después de evaluar el cuerpo del ClassThree decorado .

En este escenario, se importa evaltime , por lo que el bloque if __name__ == '__main__': nunca
se ejecuta.

Notas sobre el escenario #1:

1. Este escenario se activa mediante una simple declaración de importación evaltime .

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 .

Ahora veamos qué sucede en el escenario #2.

Solución para el escenario #2

El ejemplo 21-9 es el resultado de ejecutar python evaltime.py.

Ejemplo 21-9. Escenario #2: ejecutar evaltime.py desde el shell


$ python3 evaltime.py
<[100]> inicio del módulo evalsupport
<[400]> cuerpo MetaAleph <[700]> fin
del módulo evalsupport <[1]> inicio del
módulo evaltime <[2]> cuerpo ClassOne
<[6]> cuerpo ClassTwo <[7]> ClassThree
body <[200]> deco_alpha <[9]> ClassFour
body <[11]> ClassOne tests <[3]>
ClassOne.__init__ <[5]>
ClassOne.method_x <[12]> ClassThree
...............................
tests <[300]> deco_alfa:interior_1

...............................

Qué sucede cuando: tiempo de importación versus tiempo de ejecución | 665


Machine Translated by Google

<[13]> Pruebas de ...............................


ClassFour <[10]>
ClassFour.method_y <[14]>
final del módulo evaltime <[4]> ClassOne.__del__

Mismo resultado que el Ejemplo 21-8 hasta ahora.

Comportamiento estándar de una clase.

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

una jerarquía de clases completa, y no una clase a la vez.

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.

666 | Capítulo 21: Metaprogramación de clases


Machine Translated by Google

Figura 21-1. Una metaclase es una clase que construye clases.

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:

>>> 'correo no deseado'.__clase__


<clase 'cadena'>
>>> str.__class__
<clase 'tipo'> >>>
from bulkfood_v6 import LineItem >>>
LineItem.__class__ <clase 'tipo'> >>>
type.__class__ <clase 'tipo'>

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.

Metaclases 101 | 667


Machine Translated by Google

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:

>>> importar colecciones


>>> colecciones.Iterable.__class__ <clase
'abc.ABCMeta'> >>> import abc >>>
abc.ABCMeta.__class__ <clase 'tipo'>
>>> abc.ABCMeta.__mro__ (< clase
'abc.ABCMeta'>, <clase 'tipo'>, <clase
'objeto'>)

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.

668 | Capítulo 21: Metaprogramación de clases


Machine Translated by Google

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.

El ejercicio del tiempo de evaluación de la metaclase

Esta es una variación de “Ejercicios de tiempo de evaluación” en la página 662. El módulo


evalsupport.py es el mismo que el del ejemplo 21-7, pero el script principal ahora es evalÿtime_meta.py,
que se muestra en el ejemplo 21-10.

Ejemplo 21-10. evaltime_meta.py: ClassFive es una instancia de la metaclase MetaAleph

desde evalsupport importar deco_alpha


desde evalsupport importar MetaAleph

print('<[1]> inicio del módulo evaltime_meta')

@deco_alpha
clase ClassThree():
print('<[2]> cuerpo ClassThree')

def método_y(self):
print('<[3]> ClassThree.method_y')

Metaclases 101 | 669


Machine Translated by Google

clase Clase Cuatro (Clase Tres):


print('<[4]> cuerpo ClassFour')

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

print('<[15]> final del módulo evaltime_meta')

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.

Las soluciones y el análisis son los siguientes.

670 | Capítulo 21: Metaprogramación de clases


Machine Translated by Google

Solución para el escenario #3

El ejemplo 21-11 muestra el resultado de importar evaltime_meta.py en la consola de Python.

Ejemplo 21-11. Escenario #3: importar evaltime_meta en la consola de Python

>>> import evaltime_meta


<[100]> evalsupport module start
<[400]> MetaAleph body <[700]>
evalsupport module end <[1]>
evaltime_meta module start <[2]>
ClassThree body <[200]> deco_alpha
< [4]> ClassFour body <[6]> ClassFive
body <[500]> MetaAleph.__init__ <[9]>
ClassSix body <[500]> MetaAleph.__init__
<[15]> evaltime_meta module end

La diferencia clave con el escenario n.º 1 es que se invoca el método MetaAleph.__init__ para
inicializar el ClassFive recién creado.

Y MetaAleph.__init__ también inicializa ClassSix, que es una subclase de Class Five.

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

Ese es el objeto de clase que se está inicializando (por ejemplo, ClassFive)

nombre, bases, dic


Los mismos argumentos pasados a type para construir una clase

Ejemplo 21-12. evalsupport.py: definición de la metaclase MetaAleph del ejemplo 21-7

clase MetaAleph(tipo):
print('<[400]> cuerpo MetaAleph')

def __init__(cls, nombre, bases, dic):


imprimir('<[500]> MetaAleph.__init__')

def interior_2(self):
print('<[600]> MetaAleph.__init__:interior_2')

cls.método_z = interior_2

Metaclases 101 | 671


Machine Translated by Google

Al codificar una metaclase, es convencional reemplazar self


con cls. Por ejemplo, en el método __init__ de la metaclase,
usar cls como el nombre del primer argumento deja claro que
la instancia en construcción es una clase.

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

Solución para el escenario #4

El ejemplo 21-13 muestra el resultado de ejecutar python evaltime.py desde la línea de comandos.

Ejemplo 21-13. Escenario #4: ejecutar evaltime_meta.py desde el shell

$ 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

...............................

Cuando el decorador se aplica a ClassThree, su método_y se reemplaza por el método


interior_1 ...

Pero esto no tiene efecto en el ClassFour no decorado, aunque ClassFour es


una subclase de ClassThree.

672 | Capítulo 21: Metaprogramación de clases


Machine Translated by Google

El método __init__ de MetaAleph reemplaza ClassFive.method_z con su función inner_2 .

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.

Se puede realizar una mayor personalización de la clase implementando


__new__ en una metaclase. Pero la mayoría de las veces, implementar
__init__ es suficiente.

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.

Una metaclase para personalizar descriptores


Volvamos a los ejemplos de elementos de línea. Sería bueno si el usuario no tuviera que estar al tanto de
los decoradores o las metaclases, y pudiera simplemente heredar de una clase proporcionada por nuestra
biblioteca, como en el Ejemplo 21-14.

Ejemplo 21-14. bulkfood_v7.py: heredando de model.Entity puede funcionar, si una metaclase está detrás
de escena

importar model_v7 como modelo

clase LineItem(modelo.Entidad):
descripción = modelo.NonBlank()
peso = modelo.Cantidad() precio =
modelo.Cantidad()

def __init__(self, descripción, peso, precio): self.description


= descripción self.weight = peso self.price = precio

def subtotal(self):
return self.weight * self.price

LineItem es una subclase de model.Entity.

Una metaclase para personalizar descriptores | 673


Machine Translated by Google

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 .

Ejemplo 21-15. model_v7.py: la metaclase EntityMeta y una instancia de la misma, Entity

clase EntityMeta(tipo):
"""Metaclase para entidades de negocio con campos validados"""

def __init__(cls, nombre, bases, attr_dict):


super().__init__(nombre, bases, attr_dict) for
key, attr in attr_dict.items(): if isinstance(attr,
Validated):
type_name = type(attr).__name__
attr.storage_name = '_{}#{}'.format(type_name, key)

clase Entidad(metaclase=EntidadMeta):
"""Entidad comercial con campos validados"""

Llame a __init__ en la superclase (escriba en este caso).

Misma lógica que el decorador @entity del ejemplo 21-4.

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.

674 | Capítulo 21: Metaprogramación de clases


Machine Translated by Google

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.

El método especial Metaclass __prepare__


En algunas aplicaciones es interesante poder saber el orden en que se definen los atributos de una
clase. Por ejemplo, una biblioteca para leer/escribir archivos CSV controlados por clases definidas por
el usuario puede desear asignar el orden de los campos declarados en la clase al orden de las
columnas en el archivo CSV.

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.

El método especial Metaclass __prepare__ | 675


Machine Translated by Google

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

def __init__(cls, nombre, bases, attr_dict):


super().__init__(name, bases, attr_dict)
cls._field_names = [] for key, attr in attr_dict.items():
if isinstance(attr, Validated):

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.

Cree un atributo _field_names en la clase en construcción.

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.

Agregue el nombre de cada campo Validado encontrado a _field_names.

El método de la clase field_names simplemente genera los nombres de los campos en el orden
en que se agregaron.

676 | Capítulo 21: Metaprogramación de clases


Machine Translated by Google

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

>>> para nombre en LineItem.field_names():


... print(nombre)
...
descripción
peso precio

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

• Aplicar decoradores a muchos métodos a la vez •

Serialización de objetos o conversión de datos • Mapeo

relacional de objetos • Persistencia basada en objetos •

Traducción dinámica de estructuras de clases de otros

lenguajes

Ahora tendremos una breve descripción general de los métodos definidos en el modelo de datos de Python para todas
las clases.

Clases como objetos


Cada clase tiene una serie de atributos definidos en el modelo de datos de Python, documentado en
“4.13. Atributos especiales” del capítulo “Tipos incorporados” en la Referencia de la biblioteca.
Tres de esos atributos ya los hemos visto varias veces en el libro: __mro__, __class__ y __name__.
Otros atributos de clase son:

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.

clases como objetos | 677


Machine Translated by Google

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

Ninguno de los atributos mencionados en esta sección está listado por


la función dir(…) .

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.

Resumen del capítulo


La metaprogramación de clases consiste en crear o personalizar clases dinámicamente. Las clases en Python
son objetos de primera clase, por lo que comenzamos el capítulo mostrando cómo se puede crear una clase
mediante una función que invoca la metaclase incorporada de tipo .

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

678 | Capítulo 21: Metaprogramación de clases


Machine Translated by Google

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 segunda metaclase que implementamos agregó una pequeña característica a model.EntityMeta: un


método __prepare__ para proporcionar un OrderedDict para servir como mapeo de nombres a atributos.
Esto preserva el orden en el que esos atributos están vinculados en el cuerpo de la clase en construcción,
de modo que los métodos de la metaclase como __new__ e __init__ puedan usar esa información. En el
ejemplo, implementamos un atributo de clase _field_names , que hizo posible un Entity.field_names()
para que los usuarios pudieran recuperar los descriptores validados en el mismo orden en que aparecen
en el código fuente.

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:

Y, no defina ABC personalizados (o metaclases) en el código de producción... si siente la necesidad de


hacerlo, apuesto a que es probable que sea un caso de "todos los problemas parecen un clavo"-síndrome
para alguien que simplemente obtuvo un nuevo y brillante martillo: usted (y los futuros mantenedores de
su código) estarán mucho más felices si se quedan con un código directo y simple, evitando tales
profundidades.
— Alex Martelli

Sabias palabras de un hombre que no solo es un maestro de la metaprogramación de Python, sino


también un ingeniero de software consumado que trabaja en algunas de las implementaciones de Python
de misión crítica más grandes del mundo.

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

Lectura adicional | 679


Machine Translated by Google

Python 3.3 y están diseñados para ayudar con la metaprogramación de clases:


tipos.nueva_clase(…) y tipos.prepare_clase(…).

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:

Este libro contribuyó al diseño de metaclases en Python 2.2


Lástima que esté agotado; Sigo refiriéndome a él como el mejor tutorial que conozco sobre
el difícil tema de la herencia múltiple cooperativa, compatible con Python a través de la
función super() . 6 .

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.

680 | Capítulo 21: Metaprogramación de clases


Machine Translated by Google

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

— Brian Harvey y Matthew Wright Prefacio a


Simply Scheme Las descripciones

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.

Lectura adicional | 681


Machine Translated by Google

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í.

682 | Capítulo 21: Metaprogramación de clases


Machine Translated by Google

Epílogo

Python es un lenguaje para adultos que consienten.

—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.

Capítulo 3: en Prueba de rendimiento del operador


El ejemplo A-1 es el código que usé para producir los tiempos en la Tabla 3-6 usando el módulo timeit . La
secuencia de comandos se ocupa principalmente de configurar las muestras de pajar y agujas y de formatear
la salida.

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

importación del sistema de importación

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=' ')

agujas = array.array('d') with


open('not_selected.arr', 'rb') as fp: agujas.fromfile(fp,
500) agujas.extend(seleccionado[::{tamaño}//
500]) if {verbose}: print(' agujas: %10d' %
len(agujas), end=' ')

'''

'''
PRUEBA =
encontrado = 0 para
n en agujas: si n en
pajar: encontrado
+= 1 si {verbose}: print('
encontrado: %10d' % encontrado)
'''

def test(container_type, detallado):


MAX_EXPONENT =
7 para n en el rango (3, MAX_EXPONENT + 1):
tamaño = 10**n
configuración = SETUP.format(container_type=container_type,
tamaño=tamaño,
detallado=detallado) prueba =
PRUEBA.formato(detallado=detallado) tt = timeit.repeat(stmt=test,
setup=setup, repeat=5, number=1) print('|{:{}d} |{:f}'.format(tamaño, MAX_EXPONENT + 1, min(tt)))

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)

El script container_perftest_datagen.py (Ejemplo A-2) genera el accesorio de datos para


el script en el Ejemplo A-1.

688 | Apéndice A: Scripts de soporte


Machine Translated by Google

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
"""

Generar datos para la prueba de rendimiento del contenedor


"""

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

muestra = {1/random.random() for i in range(SAMPLE_LEN)} print('muestra


inicial: %d elementos' % len(muestra))

# muestra completa, en caso de que se descartaran números aleatorios duplicados


mientras len(sample) < SAMPLE_LEN:
muestra.añadir(1/aleatorio.aleatorio())

print('muestra completa: %d elementos' % len(muestra))

muestra = array.array('d', muestra)


random.shuffle(muestra)

not_selected = sample[:NEEDLES_LEN // 2] print('no


seleccionado: %d muestras' % len(not_selected)) print(' escribiendo
not_selected.arr') with open('not_selected.arr', 'wb') as fp :
not_selected.tofile(fp)

seleccionado = muestra[AGUJAS_LEN // 2:]


print('seleccionado: %d muestras' % len(seleccionado)) print('
escribiendo seleccionado.arr') with open('seleccionado.arr',
'wb') as fp: seleccionado.aarchivo(fp)

Capítulo 3: Comparar los patrones de bits de los hashes


El ejemplo A-3 es un script simple para mostrar visualmente cuán diferentes son los patrones de bits
para los hash de números de punto flotante similares (por ejemplo, 1,0001, 1,0002, etc.). Su salida
aparece en el Ejemplo 3-16.

Ejemplo A-3. hashdiff.py: muestra la diferencia de los patrones de bits de los valores hash

sistema de importación

MAX_BITS = len(formato(sys.maxsize, 'b'))

Capítulo 3: Comparar los patrones de bits de hashes | 689


Machine Translated by Google

print('%s-bit Python build' % (MAX_BITS + 1))

def hash_diff(o1, o2):


h1 = '{:>0{}b}'.format(hash(o1), MAX_BITS) h2 = '{:>0{}
b}'.format(hash(o2), MAX_BITS) diff = ''.join ('!' if b1 !=
b2 else ' ' for b1, b2 in zip(h1, h2)) cuenta = '!= {}'.format(diff.count('!')) ancho =
max(len( repr(o1)), len(repr(o2)), 8) '-' * (ancho * 2 + MAX_BITS) sep = return '{!r:
{ancho}} {}\n{:{ancho}} { } {}\n{!r:{ancho}} {}\n{}'.format( o1, h1, ' ' * ancho, diff, contar,
o2, h2, sep, ancho=ancho)

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

Capítulo 9: Uso de RAM con y sin __slots__


El script memtest.py se usó para una demostración en “Ahorro de espacio con el atributo de clase
__slots__” en la página 264: Ejemplo 9-12.

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)

fmt = 'Tipo de Vector2d seleccionado: {.__name__}.{.__name__}'


print(fmt.format(módulo, módulo.Vector2d))

mem_init = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
print('Creando {:,} instancias de Vector2d'.format(NUM_VECTORS))

vectores = [module.Vector2d(3.0, 4.0) for i in range(NUM_VECTORS)]

mem_final = recurso.getrusage (recurso.RUSAGE_SELF).ru_maxrss

690 | Apéndice A: Scripts de soporte


Machine Translated by Google

print('Uso de RAM inicial: {:14,}'.format(mem_init)) print(' Uso de


RAM final: {:14,}'.format(mem_final))

Capítulo 14: Script de conversión de base de datos isis2json.py


El ejemplo A-5 es la secuencia de comandos isis2json.py analizada en “Estudio de caso: Generadores en una
utilidad de conversión de base de datos” en la página 437 (Capítulo 14). Utiliza funciones de generador para
convertir de forma perezosa las bases de datos CDS/ISIS a JSON para cargarlas en CouchDB o MongoDB.

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.

Ejemplo A-5. isis2json.py: dependencias y documentación disponible en el repositorio de GitHub fluentpython/


isis2json

# este script funciona con Python o Jython (versiones >=2.5 y <3)

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'

def iter_iso_records(iso_file_name, isis_json_type):


desde iso2709 importar IsoFile
desde subcampo importar expandir

iso = IsoFile(iso_file_name) para


grabar en iso:
campos = {}
para campo en record.directory:
field_key = str(int(field.tag)) # eliminar los ceros iniciales field_occurrences
= fields.setdefault(field_key, []) content = field.value.decode(INPUT_ENCODING,
'replace')

Capítulo 14: Script de conversión de base de datos isis2json.py | 691


Machine Translated by Google

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

def iter_mst_records(master_file_name, isis_json_type):

pruebe: desde bruma.master import MasterFactory, Record


excepto ImportError: print('IMPORT ERROR: Jython 2.5
'
and Bruma.jar 'son necesarios para leer archivos .mst')

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
== ' *':

content.insert(0, subfield.getContent()) else:


content.append(SUBFIELD_DELIMITER + subfield_key
+

692 | Apéndice A: Scripts de soporte


Machine Translated by Google

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

def write_json(input_gen, file_name , output, qty, skip, id_tag, gen_uuid, mongo,


mfn , isis_json_type, prefix, constant): start = skip end = start
+ qty if id_tag: id_tag = str(id_tag) ids = set() más:

''
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:

msg = 'múltiples etiquetas de identificación #%s encontradas en el


registro %s' si ISIS_MFN_KEY en el registro:
msg = msg + (' (mfn=%s)' % record[ISIS_MFN_KEY]) raise
TypeError(msg % (id_tag, i)) else: # ok, tenemos uno y solo un
campo de identificación
if isis_json_type == 1: id =
ocurrencias[0] elif
isis_json_type == 2:
id = ocurrencias[0][0][1]
elif isis_json_type == 3:
id = ocurrencias[0]['_'] si id en
ids:
msg = 'identificación duplicada %s en la etiqueta #%s, registro
%s' si ISIS_MFN_KEY en el registro:
mensaje = mensaje + (' ( mfn =%s)' % registro[ISIS_MFN_KEY])
aumentar TypeError(mensaje % (id, id_tag, i))

Capítulo 14: Script de conversión de base de datos isis2json.py | 693


Machine Translated by Google

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:

clave_constante, valor_constante = constante.split(':')


record[clave_constante] = valor_constante
output.write(json.dumps(record).encode('utf-8')) output.write('\n')
si no es mongo: output.write(']\n')

def main(): #
crear el analizador
analizador = argparse.ArgumentParser(
description='Convertir un archivo ISIS .mst o .iso en una matriz JSON')

# agregue los argumentos

parser.add_argument( 'file_name', metavar='INPUT.(mst|


iso)', help='.mst o .iso file to read')
parser.add_argument( '-o', '--out ',
type=argparse.FileType('w'), default=sys.stdout, metavar='OUTPUT.json', help='el
archivo donde debe escribirse la salida JSON' ' (predeterminado: escribir en stdout)')
parser.add_argument( '-c', '--couch', action='store_true', help='matriz de salida dentro
de un elemento "docs" en un documento JSON'

' para inserción masiva en CouchDB a través de POST a db/_bulk_docs')


parser.add_argument( '-m', '--mongo', action='store_true', help='salida de registros
individuales como diccionarios JSON separados, uno'

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

694 | Apéndice A: Scripts de soporte


Machine Translated by Google

'-s', '--skip', type=int, default=0, help='records


to skip from start of .mst (default=0)') parser.add_argument( '-i', '--
id ', type=int, metavar='TAG_NUMBER', default=0, help='generar un
"_id" a partir del número de campo TAG único dado' ' para cada
registro') parser.add_argument( '-u', '-- uuid', action='store_true', help='generar
un "_id" con un UUID aleatorio para cada registro') parser.add_argument( '-
p', '--prefix', type=str, metavar='PREFIX' , default='', help='concatenar prefijo a
cada etiqueta de campo numérico' ' (ej. 99 se convierte en "v99")')
parser.add_argument( '-n', '--mfn', action='store_true', help='generar un "_id"
a partir del MFN de cada registro'

' (disponible solo para entrada .mst)')


parser.add_argument( '-k', '--constant', type=str,
metavar='TAG:VALUE', default='', help='Incluir una etiqueta constante :valor
en cada registro (ej. -k tipo:AS)')

'''
# 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'

' (predeterminado = 1, use -r 0 para repetir hasta el final de la entrada)')


'''
# analiza la línea de comando
args = parser.parse_args() if
args.file_name.lower().endswith('.mst'): input_gen_func
= iter_mst_records else:

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" : ')

write_json(input_gen, args.file_name, args.out, args.qty,


args.skip, args.id, args.uuid, args.mongo, args.mfn, args.type,
args.prefix, args.constant) si args.couch: args.out.write('}\n')
args .fuera.cerrar()

si __nombre__ == '__principal__':
principal()

La función generadora iter_iso_records lee el archivo .iso y genera registros.

Capítulo 14: Script de conversión de base de datos isis2json.py | 695


Machine Translated by Google

La función del generador iter_mst_records lee el archivo .mst y genera registros.

write_json itera sobre el generador input_gen y genera el archivo .json .

La función principal lee los argumentos de la línea de comandos y

luego… …selecciona iter_iso_records o… …iter_mst_records

dependiendo de la extensión del archivo de entrada.

Un objeto generador se construye a partir de la función generadora seleccionada.

write_json se llama con el generador como primer argumento.

Capítulo 16: Simulación de eventos discretos de flota de taxis


El ejemplo A-6 es la lista completa de taxi_sim.py discutida en “La simulación de la flota de taxis” en la
página 490.

Ejemplo A-6. taxi_sim.py: el simulador de flota de taxis


"""
simulador de taxi
==============

Conducir un taxi desde la consola:

>>> from taxi_sim import taxi_process >>> taxi =


taxi_process(ident=13, viajes=2, start_time=0) >>> next(taxi)

Event(time=0, proc=13, action='salir del garaje') >>>


taxi.send(_.time + 7)
Event(time=7, proc=13, action='recoger pasajero') >>>
taxi.send(_.time + 23)
Event(time=30, proc=13, action='dejar pasajero') >>> taxi.send(_.time
+ 5)
Event(time=35, proc=13, action='recoger pasajero') >>>
taxi.send(_.time + 48)
Event(time=83, proc=13, action='dejar pasajero') >>> taxi.send(_.time
+ 1)
Evento(tiempo=84, proc=13, acción='ir a casa') >>>
taxi.send(_.tiempo + 10)
Rastreo (llamadas recientes más última):
Archivo "<stdin>", línea 1, en <módulo>
Detener iteración

Ejecución de muestra con dos autos, semilla aleatoria 10. Esta es una prueba de documento válida:

>>> main(num_taxis=2, seed=10) taxi:


0 Event(time=0, proc=0, action='dejar garaje') taxi: 0 Event(time=5,
proc=0, action='pick subir pasajero') taxi: 1 taxi: 1
Evento (tiempo = 5, proceso = 1, acción = 'salir del garaje')
Evento(tiempo=10, proc=1, acción='recoger pasajero')

696 | Apéndice A: Scripts de soporte


Machine Translated by Google

taxi: 1 Evento (tiempo = 15, proc = 1, acción = 'dejar pasajero')


taxi: 0 Evento (tiempo = 17, proc = 0, acción = 'dejar pasajero')
taxi: 1 Evento(tiempo=24, proc=1, acción='recoger pasajero')
taxi: 0 Evento(hora=26, proc=0, acción='recoger pasajero') taxi: 0 Evento(hora=30,
proc=0, acción='dejar pasajero') taxi: 0 Evento(hora=34 , proc=0, action='ir a casa') taxi: 1

Evento (hora = 46, proc = 1, acción = 'dejar al pasajero')


taxi: 1 Evento(tiempo=48, proc=1, acción='recoger pasajero')
taxi: 1 Evento (tiempo = 110, proc = 1, acción = 'dejar pasajero')
taxi: 1 Evento(hora=139, proc=1, acción='recoger pasajero')
taxi: 1 Evento (hora = 140, proc = 1, acción = 'dejar al pasajero')
taxi: 1 *** Evento (tiempo = 150, proc = 1, acción = 'ir a casa')
fin de los eventos ***

Vea una ejecución de muestra más larga al final de este módulo.

"""

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

Evento = colecciones.namedtuple('Evento', 'acción de proceso de tiempo')

# 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')

yield Event(time, ident, 'going home') # fin del proceso


de taxi
# FIN DE TAXI_PROCESO

# BEGIN TAXI_SIMULATOR
clase Simulador:

def __init__(self, procs_map): self.events =


cola.PriorityQueue() self.procs = dict(procs_map)

Capítulo 16: Simulación de eventos discretos de flota de taxis | 697


Machine Translated by Google

def ejecutar(self, end_time):


"""Programar y mostrar eventos hasta que se acabe el tiempo""" #
programar el primer evento para cada cabina para
_, proc en sorted(self.procs.items()): first_event =
next(proc) self.events.put(first_event)

# ciclo principal de la simulación sim_time


= 0 while sim_time < end_time: if
self.events.empty(): print('*** fin de eventos
***') break

evento_actual = self.events.get() tiempo_sim,


id_proc , acción_anterior = 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:
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':

# nuevo estado es intervalo


de viaje = VIAJE_DURACIÓN elif
acción_anterior == 'ir a casa':
intervalo = 1 más:

aumentar ValueError('Acción_anterior desconocida : %s' % acción_anterior )


return int(random.expovariable(1/intervalo)) + 1

def main(end_time=DEFAULT_END_TIME, num_taxis=DEFAULT_NUMBER_OF_TAXIS,


seed=Ninguno): """Inicializar generador aleatorio, construir procesos y ejecutar
simulación""" si seed no es Ninguno: random.seed(seed) # obtener resultados reproducibles

698 | Apéndice A: Scripts de soporte


Machine Translated by Google

taxis = {i: proceso_taxi(i, (i+1)*2, i*INTERVALO_SALIDA)


para i en el rango (num_taxis)}
sim = Simulador (taxis)
sim.run(end_time)

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

Capítulo 16: Simulación de eventos discretos de flota de taxis | 699


Machine Translated by Google

taxi: 2 Evento(tiempo=72, proc=2, acción='recoger pasajero')


taxi: 0 Evento (tiempo = 76, proc = 0, acción = 'ir a casa')
taxi: 1 Evento (tiempo = 80, proc = 1, acción = 'dejar pasajero')
taxi: 1 Evento(tiempo=88, proc=1, acción='recoger pasajero')
taxi: 2 Evento (tiempo = 95, proc = 2, acción = 'dejar pasajero')
taxi: 2 Evento(tiempo=97, proc=2, acción='recoger pasajero')
taxi: 2 Evento (tiempo = 98, proc = 2, acción = 'dejar pasajero')
taxi: 1 Evento (tiempo = 106, proc = 1, acción = 'dejar pasajero')
taxi: 2 Evento (tiempo = 109, proceso = 2, acción = 'ir a casa')
taxi: 1 *** Evento (tiempo = 110, proceso = 1, acción = 'ir a casa')
fin de los eventos ***
# FIN DE TAXI_SAMPLE_RUN

"""

Capítulo 17: Ejemplos criptográficos


Estos scripts se usaron para mostrar el uso de futures.ProcessPoolExecutor para ejecutar tareas
intensivas de CPU.

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.

Ejemplo A-7. arcfour_futures.py: ejemplo de futures.ProcessPoolExecutor

sistema de importación

tiempo de importación

de futuros de importación concurrentes


de randrange de importación aleatoria
de arcfour importar arcfour

EMPLEOS = 12
TAMAÑO = 2**18

CLAVE = b"Fue brillante, y las toves resbaladizas\ngiraron "


ESTADO = '{} trabajadores, tiempo transcurrido: {:.2f}s'

def arcfour_test(tamaño, clave):


in_text = bytearray(randrange(256) for i in range(size))
texto_cifrado = arcfour(clave, en_texto)
out_text = arcfour(key, cypher_text)
afirmar in_text == out_text, 'Error en arcfour_test'
tamaño de retorno

def main(trabajadores=Ninguno):
si los trabajadores:

trabajadores = int(trabajadores)
t0 = tiempo.tiempo()

con futures.ProcessPoolExecutor(workers) como ejecutor:

700 | Apéndice A: Scripts de soporte


Machine Translated by Google

trabajadores_actuales = executor._max_workers to_do


= [ ] for i in range(JOBS, 0, -1):

tamaño = TAMAÑO + int(TAMAÑO / TRABAJOS * (i - TRABAJOS/


2)) trabajo = ejecutor.submit(arcfour_test, tamaño, CLAVE)
to_do.append(trabajo)

para el futuro en futures.as_completed(to_do):


res = futuro.resultado()
print('{:.1f} KB'.format(res/2**10))

print(ESTADO.formato(trabajadores_actuales, tiempo.tiempo() - t0))

if __name__ == '__main__': if
len(sys.argv) == 2: trabajadores
= int(sys.argv[1]) else: trabajadores
= Ninguno

principal (trabajadores)

El ejemplo A-8 implementa el algoritmo de cifrado RC4 en Python puro.

Ejemplo A-8. arcfour.py: algoritmo compatible con RC4


"""Algoritmo compatible con RC4"""

def arcfour(clave, in_bytes, bucles=20):

kbox = bytearray(256) # crear cuadro clave para i, auto


en enumerar(clave): # copiar clave y vector kbox[i] = auto

j = len(clave)
para i en rango(j, 256): # repetir hasta completar kbox[i] = kbox[ij]

# [1] inicializar sbox sbox =


bytearray(rango(256))

# repetir el bucle de mezcla de sbox, como se recomienda en CipherSaber-2 #


http:// ciphersaber.gurus.com/ faq.html#cs2 j = 0 para k en el rango (bucles):

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

para coche en in_bytes: i =


(i + 1) % 256

Capítulo 17: Ejemplos criptográficos | 701


Machine Translated by Google

# [2] barajar sbox j = (j +


sbox[i]) % 256 sbox[i], sbox[j] =
sbox[j], sbox[i] # [3] calcular t t = (sbox[i] + sbox[j])
% 256 k = sbox[t] ^ k out_bytes.append(coche)

coche = coche

devolver out_bytes

prueba de definición ():

from time import time clear =


bytearray(b'1234567890' * 100000) t0 = time() cipher =
arcfour(b'key', clear) print('elapsed time: %.2fs' % (time() -
t0)) resultado = arcfour(b'clave', cifrado) afirmar resultado
== borrar, '%r != %r' % (resultado, borrar) imprimir('tiempo
transcurrido: %.2fs' % (tiempo() - t0)) imprimir('OK')

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.

Ejemplo A-9. sha_futures.py: ejemplo de futures.ProcessPoolExecutor


importar
tiempo de
importación del
sistema importar hashlib de futuros de
importación concurrentes de randrange de importación aleatoria

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:

702 | Apéndice A: Scripts de soporte


Machine Translated by Google

trabajadores = int(trabajadores)
t0 = tiempo.tiempo()

with futures.ProcessPoolExecutor(workers) as ejecutor: actual_workers =


executor._max_workers to_do = (executor.submit(sha, SIZE) for i in
range(JOBS)) for future in futures.as_completed(to_do): res = future.result( )
imprimir (res)

print(ESTADO.formato(trabajadores_actuales, tiempo.tiempo() - t0))

if __name__ == '__main__': if
len(sys.argv) == 2: trabajadores
= int(sys.argv[1]) else: trabajadores
= Ninguno

principal (trabajadores)

Capítulo 17: Ejemplos de clientes HTTP flags2


Todos los ejemplos de flags2 de “Descargas con visualización de progreso y manejo de
errores” en la página 520 usan funciones del módulo flags2_common.py (Ejemplo A-10).

Ejemplo A-10. flags2_common.py


"""Utilidades para el segundo conjunto de ejemplos de banderas.
"""

import o
import time
import sys
import string
import argparse
from collections import namedtuple from
enum import Enum

Resultado = tupla nombrada('Resultado', 'datos de estado')

HTTPStatus = Enum('Estado', 'ok not_found error')

'
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',

Capítulo 17: Ejemplos de clientes HTTP flags2 | 703


Machine Translated by Google

'ERROR': 'http://localhost:8003/flags',
}
SERVIDOR_DEFAULT = 'LOCAL'

DEST_DIR = 'descargas/'
COUNTRY_CODES_FILE = 'códigos_país.txt'

def save_flag(img, nombre de


archivo): ruta = os.path.join(DEST_DIR, nombre
de archivo) with open(ruta, 'wb') as fp:
fp.write(img)

def initial_report(cc_list, real_req, server_label):


if len(cc_list) <= 10:
cc_msg = ', '.join(cc_list) else:

cc_msg = 'de {} a {}'.format(cc_list[0], cc_list[-1]) print('{} sitio:


{}'.format(server_label, SERVERS[server_label])) msg = 'Buscando {} flag{}: {}'
plural = 's' if len(cc_list) != 1 else '' print(msg.format(len(cc_list), plural, cc_msg))
plural = 's' if actual_req != 1 else '' msg = 'Se usará {} conexión concurrente{}.'
imprimir(mensaje.formato(actual_req, plural))

def final_report(cc_list, contador, start_time): transcurrido


= time.time() - start_time print('-' * 20) msg = '{} flag{}
descargado.' plural = 's' if contador[HTTPStatus.ok] !
= 1 else '' print(msg.format(contador[HTTPStatus.ok],
plural)) if contador[HTTPStatus.not_found]:
print(contador[HTTPStatus.not_found] , 'no encontrado.') si
contador[HTTPStatus.error]:

plural = 's' if contador[HTTPStatus.error] != 1 else '' print('{}


error{}.'.format(contador[HTTPStatus.error], plural)) print('Tiempo transcurrido:
{:. 2f}s'.formato(transcurrido))

def expand_cc_args(every_cc, all_cc, cc_args, limit): códigos =


set()
A_Z = string.ascii_uppercase si
cada_cc:
codes.update(a+b para a en A_Z para b en A_Z) elif
all_cc:
con open(COUNTRY_CODES_FILE) como
fp: text = fp.read() codes.update(text.split())
else:

704 | Apéndice A: Scripts de soporte


Machine Translated by Google

for cc in (c.upper() for c in cc_args): if len(cc) ==


1 and cc in A_Z:
codes.update(cc+c para c en A_Z) elif
len(cc) == 2 y todo(c en A_Z para c en cc):
codes.add(cc)
else: msg = 'cada
argumento CC debe ser de A a Z o de AA a ZZ.' aumentar
ValueError('*** Error de uso: '+mensaje)
volver ordenado(códigos)[:límite]

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)

excepto ValueError como exc:

Capítulo 17: Ejemplos de clientes HTTP flags2 | 705


Machine Translated by Google

imprimir(exc.args[0])
parser.print_usage() sys.exit(1)

si no es cc_list: cc_list
= sorted(POP20_CC) devolver
argumentos , cc_list

def main(download_many, default_concur_req, max_concur_req): args, cc_list =


process_args(default_concur_req) real_req = min(args.max_req, max_concur_req,
len(cc_list)) initial_report(cc_list, real_req, args.server) base_url = SERVIDORES[args.server ]
t0 = tiempo.tiempo() contador = download_many(cc_list, base_url, args.verbose, real_req)
afirmar suma(counter.values()) == len(cc_list), \

'algunas descargas no están contabilizadas'


informe_final(lista_cc, contador, t0)

El script flags2_secuencial.py (Ejemplo A-11) es la línea de base para la comparación


con las implementaciones simultáneas. flags2_threadpool.py (Ejemplo 17-14) también
usa las funciones get_flag y download_one de flags2_secuencial.py.

Ejemplo A-11. flags2_secuencial.py


"""Descargar banderas de países (con manejo de errores).

Versión secuencial

Ejemplo de ejecución::

$ python3 flags2_secuencial.py -s DELAY b DELAY sitio: http://


localhost:8002/ flags Búsqueda de 26 banderas: de BA a BZ
Se utilizará 1 conexión simultánea.

--------------------

17 banderas descargadas. 9
no encontrado.
Tiempo transcurrido: 13,36 s

"""

importar colecciones

solicitudes de
importación importar tqdm

desde flags2_common import main, save_flag, HTTPStatus, Result

DEFAULT_CONCUR_REQ = 1

706 | Apéndice A: Scripts de soporte


Machine Translated by Google

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.

def descargar_uno(cc, base_url, detallado=Falso):

intente: imagen = get_flag (base_url, cc)


excepto solicitudes.excepciones.HTTPError como exc:
res = exc.response si
res.status_code == 404: estado =
HTTPStatus.not_found msg = 'no
encontrado' else: aumentar

else:
save_flag(image, cc.lower() + '.gif') status =
HTTPStatus.ok msg = 'OK'

si detallado:
imprimir (cc, msg)

resultado devuelto (estado, cc)


# FIN DE FLAGS2_BASIC_HTTP_FUNCTIONS

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

excepto solicitudes.excepciones.HTTPError como exc:


error_msg = 'Error HTTP {res.status_code} - {res.reason}' 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

Capítulo 17: Ejemplos de clientes HTTP flags2 | 707


Machine Translated by Google

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)

Capítulo 19: Scripts y pruebas del programa OSCON


El Ejemplo A-12 es el script de prueba para el módulo schedule1.py (Ejemplo 19-9). Utiliza la
biblioteca py.test y el corredor de prueba.

Ejemplo A-12. test_schedule1.py

importar archivar
importar pytest

importar horario1 como horario

@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_class(): rec =


horario.Record (spam=99, huevos=12) afirmar
rec.spam == 99 afirmar rec.eggs == 12

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

708 | Apéndice A: Scripts de soporte


Machine Translated by Google

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.

Ejemplo A-13. horario2.py


"""

schedule2.py: atravesando datos de programación OSCON

>>> import shelve >>>


db = shelve.open(DB_NAME) >>> si
CONFERENCE no está en db: load_db(db)

# COMENZAR HORARIO2_DEMO

>>> DbRecord.set_db(db) >>>


evento = DbRecord.fetch('evento.33950')
>>> evento
<Evento 'Habrá *Habrá* Errores'> >>>
evento.lugar <DbRecord
serial='lugar.1449'>
>>> evento.lugar.nombre
'Portland 251'
>>> para spkr en event.speakers:
... print('{0.serial}: {0.name}'.format(spkr))
...
locutor.3471: Anna Martelli Ravenscroft locutor.5199:
Alex Martelli

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

def __eq__(uno mismo, otro):

Capítulo 19: Scripts y pruebas de programación OSCON | 709


Machine Translated by Google

si es instancia(otro, Registro): return


self.__dict__ == otro.__dict__ else:

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:

msg = "base de datos no configurada; llamar a '{}.set_db(my_db)'"


aumentar MissingDatabaseError(msg.format(cls.__name__)) else: #
aumentar

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)

710 | Apéndice A: Scripts de soporte


Machine Translated by Google

@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

El Ejemplo A-14 se usó para probar el Ejemplo A-13 con py.test.

Ejemplo A-14. test_schedule2.py


importar
archivar importar pytest

importar horario2 como horario

@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

Capítulo 19: Scripts y pruebas de programación OSCON | 711


Machine Translated by Google

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_missing_db_exception(): with


pytest.raises(schedule.MissingDatabaseError):
schedule.DbRecord.fetch('venue.1585')

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

712 | Apéndice A: Scripts de soporte


Machine Translated by Google

anna_y_alex = [db['speaker.3471'], db['speaker.5199']] afirmar


evento.speakers == anna_and_alex

def test_event_no_speakers(db):
horario.Evento.set_db( db) event
= db['event.36848'] afirmar
len(event.speakers) == 0

Capítulo 19: Scripts y pruebas de programación OSCON | 713


Machine Translated by Google
Machine Translated by Google

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.

Consulte también el glosario oficial de Python.

ABC (lenguaje de programación) alias


Un lenguaje de programación creado por Leo Geurts, Asignación de dos o más nombres al mismo objeto.
Lambert Meertens y Steven Pemberton. Guido van Por ejemplo, en a = []; b = a las variables a y b son
Rossum, quien desarrolló Python, trabajó como alias para la misma lista
programador implementando el entorno ABC en la objeto. El alias ocurre naturalmente todo el tiempo en
década de 1980. La estructuración de bloques por cualquier idioma donde las variables almacenan
sangría, tuplas y diccionarios incorporados, el referencias a objetos. Para evitar confusiones,
desempaquetado de tuplas, la semántica del bucle simplemente olvide la idea de que las variables son
for y el manejo uniforme de todos los tipos de cajas que contienen objetos (un objeto no puede
secuencias son algunas de las características estar en dos cajas al mismo tiempo). Es mejor pensar
distintivas de Python que provienen de ABC. en ellos como etiquetas adheridas a objetos (un
objeto puede tener más de una etiqueta).

Clase base abstracta (ABC)

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

BDFL cadena de bytes

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

las cadenas Unicode . En Python 3, no tiene sentido insistir en este


secuencia
término, y traté de usar secuencia de bytes cada vez que necesitaba
binaria Término genérico para tipos de secuencia
hablar en general sobre... secuencias de bytes.
con elementos de byte. Los tipos de secuencias
binarias integradas son byte, bytearray y memory
view.
objeto similar a
lista de materiales

bytes Una secuencia genérica de bytes. Los tipos similares a bytes


Marca de orden de bytes, una secuencia de bytes que puede estar
más comunes son bytes, bytear ray y memoryview , pero otros
presente al comienzo de un archivo codificado en UTF-16. Un BOM
es el carácter U+FEFF objetos que admiten el protocolo de búfer CPython de bajo nivel

también califican, si sus elementos son bytes individuales.


(ESPACIO SIN INTERRUPCIÓN DE ANCHO CERO) codificado

para producir b'\xfe\xff' en una CPU big endian, o b'\xff\xfe' en una

pequeña endian. Debido a que no existe el carácter U+FFFE en


Unicode, la presencia de estos bytes revela sin ambigüedades el objeto invocable

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,

funciones integradas, métodos integrados, métodos de instancia,


funciones generadoras, clases e instancias de clases que

implementan el método especial __call__ .


método vinculado

Un método al que se accede a través de una instancia se vincula a

esa instancia. Cualquier método es en realidad un descriptor y,

cuando se accede a él, se devuelve envuelto en un objeto que El caso de Carmel

vincula el método a la instancia. La convención de escribir identificadores uniendo palabras con

iniciales en mayúsculas (por ejemplo, ConnectionRefusedError).

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

valor de self. CamelCase, pero la biblioteca estándar de Python no sigue este

Por ejemplo, dada la asignación my_method = my_obj.method, el consejo. Véase serpiente_caso.

método enlazado se puede llamar más adelante como my_method().

Contrasta con el método no ligado.

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.

funciones que no necesitan ser importadas, documentadas en el

Capítulo 2, "Funciones integradas", de The Python Standard Library


clase
Reference. Pero los módulos integrados como sys, math, re, etc.
Una construcción de programa que define un nuevo tipo, con
también contienen funciones integradas.
atributos de datos y métodos que especifican posibles operaciones

en ellos. Ver tipo.

punto de código

Un número entero en el rango de 0 a 0x10FFFF utilizado para

identificar una entrada en el carácter Unicode.

716 | Jerga de Python


Machine Translated by Google
olor a 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

devuelve un "punto de código Unicode". punto de código.”


envase

Un objeto que contiene referencias a otros objetos. La mayoría


olor a código
de los tipos de colección en Python son contenedores, pero
Un patrón de codificación que sugiere que puede haber algún algunos no lo son. Contraste con la secuencia plana, que son

problema con el diseño de un programa. Por ejemplo, el uso colecciones pero no contenedores.
excesivo de verificaciones de instancias de instancias contra

clases concretas es un olor a código, ya que hace que el


administrador de
programa sea más difícil de extender para tratar con nuevos
contexto Un objeto que implementa los métodos especiales
tipos en el futuro.
tura __en ter__ y __exit__ , para usar en un bloque with .

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

elementos a los que se puede acceder individualmente.


CPython
Algunas colecciones pueden contener objetos de tipos
El intérprete estándar de Python, implementado en C. Este
arbitrarios (ver contenedor) y otras solo objetos de un solo
término solo se usa cuando se analiza el comportamiento
tipo atómico (ver secuencia plana). list y bytes son colecciones,
específico de la implementación, o cuando se habla de los
pero list es un contenedor y bytes es una secuencia plana.
múltiples intérpretes de Python disponibles, como PyPy.

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

Un objeto invocable A que devuelve otro objeto invocable B y


ejemplos, incluyendo " Ensayos considerados dañinos se invoca en el código usando la sintaxis @A justo antes de la
considerados dañinos" por Eric A. definición de un C invocable . Al leer dicho código, el intérprete
Meyer. de Python invoca A(C) y vincula el B resultante a la variable

constructor previamente asignada a C, reemplazando efectivamente la

definición de C con B. Si el objetivo invocable C es un


Informalmente, el método de instancia __init__ de una clase
se llama su constructor, porque su semántica es similar a la

de un constructor de Java. Sin embargo, un nombre apropiado

para __in

Jerga de Python | 717


Machine Translated by Google

copia profunda

función, entonces A es un decorador de funciones; si C es una bajo


clase, entonces A es un decorador de clase. Atajo para pronunciar los nombres de métodos y atributos
especiales que se escriben con
copia
guiones bajos dobles iniciales y finales (es decir, __len__ se
profunda Una copia de un objeto en la que todos los objetos que
lee como “dunder len”).
son atributos del objeto también se copian. Contraste con copia
superficial. metodo dunder

Ver dunder y métodos especiales.

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,

nombre) y detectando AttributeError".

docstring

Abreviatura de cadena de documentación. Cuando la primera


declaración en un módulo, clase o función es un literal de

cadena, se toma como la cadena de documentación del objeto ansioso

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.

doctest Fallar rapido

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

pato Una forma de polimorfismo donde las funciones operan falso


en cualquier objeto que implementa los métodos apropiados, Cualquier valor x para el cual bool(x) devuelve Falso; Python
independientemente de sus clases o declaraciones de interfaz utiliza implícitamente bool para evaluar objetos en contextos
explícitas. booleanos, como la expresión que controla un bucle if o while .

Lo contrario de veraz.

718 | Jerga de Python


Machine Translated by Google

objeto similar a un archivo

objeto en una colección. El término se usa a veces para describir una

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

puede entenderse como una versión perezosa de una lista por

comprensión. Véase perezoso.


función de primera clase

Cualquier función que sea un objeto de primera clase en el

lenguaje (es decir, puede crearse en tiempo de ejecución, función genérica

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

funciones de primera clase. objetos. A partir de Python 3.4, el decorador functools.singledispatch


es la forma estándar de crear funciones genéricas. Esto se

conoce como multimétodos.


secuencia
plana Un tipo de secuencia que almacena
en otros idiomas.
físicamente los valores de sus elementos y no
referencias a otros objetos. Los tipos integrados libro gof
str, bytes, bytearray, memoryview y array.array Alias for Design Patterns: Elements of Reusable Object-Oriented

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

contenedoras. Ver contenedor. John Vlissides .

función

Estrictamente, un objeto resultante de la evaluación de un


hashable
bloque de definición o una expresión lambda . De manera

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

Consulte también objeto invocable. elementos también lo es.

genex

Abreviatura de generador de expresión. función de orden


superior Una función que toma otra función como argumento,
generador
como sorted, map y filter, o una función que devuelve una
Un iterador creado con una función generadora o una expresión
función como resultado, como lo hacen los decoradores de
generadora que puede producir valores sin iterar necesariamente
Python.
sobre una colección; el ejemplo canónico es un generador para

producir la serie de Fibonacci que, por ser infinita, nunca


encajaría

Jerga de Python | 719


Machine Translated by Google
modismo

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.

cuando las clases y funciones se definen y se convierten en Contraste ansioso.


objetos vivos. Aquí también es cuando se ejecutan los
listcomp
decoradores.
Abreviatura de comprensión de listas.

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.

Las secuencias siempre son iterables; otros objetos que

implementan un método __getitem__ también pueden ser método mágico

iterables. Igual que el método especial.

desempaquetado atributo administrado

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,

funciona como un atributo de instancia (es decir, generalmente


iterador
tiene un valor por instancia, guardado en un atributo de
Cualquier objeto que implemente el método sin argumentos almacenamiento). Ver descriptor.
__next__ , que devuelve el siguiente elemento de una serie o
genera StopIteration cuando no hay más elementos. Los
clase
iteradores de Python también implementan el método __iter__
administrada Una clase que usa un objeto descriptor para
por lo que también son iterables. Los iteradores clásicos, según
administrar uno de sus atributos. Ver descriptor.
el patrón de diseño original, devuelven elementos de una

colección. Un generador también es un iterador, pero es más instancia


flexible. Ver generador. administrada Una instancia de una clase administrada. Ver

atributo gestionado y descriptor.

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,

720 | Jerga de Python


Machine Translated by Google
metaprogramación

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á

el descriptor en esa instancia. También llamado descriptor sin

datos o descriptor sombreado. Contraste con el descriptor


metaprogramación
predominante.
La práctica de escribir programas que utilizan información de
tiempo de ejecución sobre sí mismos para
ORM
cambiar su comportamiento. Por ejemplo, un ORM puede hacer

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

operaciones de la base de datos. SQLAlchemy es un popular

ORM independiente de Python; los frameworks Django y Web2py


monkey patching
tienen sus propios ORM integrados.
Cambiar dinámicamente un módulo, clase o función en tiempo de

ejecución, generalmente para agregar funciones o corregir errores.

Debido a que se realiza en la memoria y no cambiando el código

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.

temporales y no una técnica recomendada para la integración de

código.

asignación paralela

Asignación a varias variables de elementos en un iterable, usando


mezclando clase
sintaxis como a, b = [c, d], también conocida como asignación de

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.

árbol de clases de herencia múltiple. Nunca se debe crear una


instancia de una clase mixin, y una subclase concreta de una
Las
clase mixin también debe ser una subclase de otra clase no mixin.
funciones de parámetro se declaran con 0 o más "parámetros

formales", que son variables locales independientes. Cuando se


método de mezcla llama a la función, los argumentos o "parámetros reales" pasados

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

función y parámetro para un parámetro formal en la declaración


mutador
de función. Sin embargo, eso no siempre es factible porque los
Véase accesorio.
términos parámetro y argumento se usan indistintamente en toda

cambio de la documentación y la API de Python. Ver argumento

nombre El cambio de nombre automático de atributos privados de

__x a _MyClass__x, realizado por el intérprete de Python en

tiempo de ejecución.
mento

descriptor no predominante primo (verbo)

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

Jerga de Python | 721


Machine Translated by Google
PyPI

se vuelve listo para recibir valores en sucesivas llamadas permitiendo el acceso a elementos a través de índices enteros

coro.send(valor) . basados en 0 (p. ej., s[0]). La secuencia de palabras ha sido parte

de la jerga de Python desde el principio, pero solo con Python 2.6


PyPI
se formalizó como una clase abstracta en collections.abc.Sequence.
El Python Package Index, donde hay disponibles más de 60.000

paquetes, también conocido como Cheese shop (ver Cheese

shop). PyPI se pronuncia como "pie-P-eye" para evitar confusiones

con PyPy. publicación por entregas

Convertir un objeto de su estructura en memoria a un formato

binario u orientado a texto para su almacenamiento o transmisión,


PyPy
de manera que permita la futura reconstrucción de un clon del
Una implementación alternativa del lenguaje de programación
objeto en el mismo sistema o en uno diferente. El módulo pickle
Python que utiliza una cadena de herramientas que compila un
admite la serialización de objetos Python arbitrarios a un formato
subconjunto de Python en código de máquina, por lo que el código
binario.
fuente del intérprete está escrito en Python. PyPy también incluye

un JIT para generar código de máquina para programas de usuario

sobre la marcha, como lo hace Java VM. A partir de noviembre de

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.

Consulte también creación de alias.

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

competentes. Ver modismo. El objeto Ninguno es un singleton en 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;

en particular, my_sequence[:] crea una copia superficial de toda

referente la secuencia.

Pero un objeto de vista de memoria se puede dividir para producir


El objeto que es el objetivo de una referencia.
Este término se usa con mayor frecuencia para hablar de una nueva vista de memoria que comparte datos con el objeto

referencias débiles. original.

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.

PEP-8 llama a este estilo "minúsculas con palabras separadas por

guiones bajos" y lo recomienda para nombrar funciones, métodos,

argumentos y variables. Para paquetes, PEP-8 recomienda


secuencia
concatenar
Nombre genérico para cualquier estructura de datos iterables con

un tamaño conocido (por ejemplo, len(s)) y

722 | Jerga de Python


Machine Translated by Google
método especial

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

método independiente debe pasar explícitamente una instancia

de la clase como primer argumento. Esa instancia se asignará al

argumento self en el método.

atributo de Ver método enlazado.


almacenamiento Un atributo en una instancia administrada que

se utiliza para almacenar el valor de un atributo administrado por principio de acceso

un descriptor. Véase también atributo gestionado. uniforme Bertrand Meyer, creador del Lenguaje Eiffel, escribió:

“Todos los servicios ofrecidos por un módulo deben estar


referencia fuerte
disponibles a través de una notación uniforme, que no traicione
Una referencia que mantiene vivo un objeto en Python. Contraste
si se implementan mediante almacenamiento o computación”.
con referencia débil.
Las propiedades y los descriptores permiten la implementación

desempaquetado del principio de acceso uniforme en Python. La falta de un nuevo

de tupla Asignación de elementos de un objeto iterable a una operador, haciendo que las llamadas a funciones y la creación

de instancias de objetos se vean iguales, es otra forma de este


tupla de variables (p. ej., primero, segundo, tercero == mi_lista).
Este es el término habitual utilizado por Pythonistas, pero el principio: la persona que llama no necesita saber si

desempaquetado iterable está ganando terreno.

veraz el objeto invocado es una clase, una función o cualquier otro


Cualquier valor x para el cual bool(x) devuelve True; Python utiliza invocable.
implícitamente bool para evaluar objetos en contextos booleanos,
definido por
como la expresión que controla un bucle if o while .
el usuario Casi siempre en los documentos de Python, la palabra

usuario se refiere a usted y a mí, programadores que usan el


Lo contrario de falso.
lenguaje Python, a diferencia de los desarrolladores que
escribe implementan un intérprete de Python. Entonces, el término "clase
Cada categoría específica de datos del programa, definida por definida por el usuario" significa una clase escrita en Python, a
un conjunto de posibles valores y operaciones sobre ellos. diferencia de las clases integradas escritas en C, como str.
Algunos tipos de Python están cerca de los tipos de datos de

máquina (p. ej., float y bytes) , mientras que otros son extensiones

(p. ej., int

Jerga de Python | 723


Machine Translated by Google
vista

vista sido reconocido por la BDFL como influyente en la decisión


Las vistas de Python 3 son estructuras de datos especiales de romper hacia atrás

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.

donde esos métodos devuelven listas. Todas las vistas de


dictado son iterables y admiten el operador in . Además, si referencia débil
los elementos a los que hace referencia la vista son todos
Un tipo especial de referencia de objeto que no aumenta el
modificables, entonces la vista también implementa la interfaz
recuento de referencias de objetos de referencia . Las
collections.abc.Set . referencias débiles se crean con una de las funciones y
estructuras de datos en el módulo de referencia débil .

Este es el caso de todas las vistas devueltas por el


YAGNI
método .keys() y de las vistas devueltas por .items() cuando

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

Una clase que no hereda de una superclase pero que se

registra mediante TheSuper Class.register(TheSubClass). Zen of Python

Consulte la documentación para abc.ABCMeta.register. Type importa esto en cualquier consola de Python desde la
versión 2.2.

verruga

Un defecto del lenguaje. El famoso post de Andrew Kuchling


"Python warts" tiene

724 | Jerga de Python


Machine Translated by Google

Índice

== operador, 223, 244, 288, 384 >


simbolos
operador, 384 >= operador, 384 @
!= operador, 384 !r
operador, 383 @abstractclassmethod,
campo de conversión, 11
329 @abstractmethod, 326
# operador, 14 % operador,
@abstractproperty, 329
11 %r marcador de
@abstractstaticmethod, 329
posición, 11 () (invocación
@asyncio.coroutine decorador, 541,
de función), 371 () (paréntesis), 22,
543 @ classmethod decorador, 592
144 () llamar operador, 144
@contextmanager decorador, 454 @property
operador, 10 , 12, 29, 36, 148, 380
* decorador, 604 [:] operador, 225 [] (corchetes),
** (doble asterisco), 148 *= operador,
6, 22, 35, 371 \ (barra invertida), 22
38, 388 *args, 29 *extra, 60 + operador,
9, 12, 36, 372, 375–380 += operador , 38,
388 +directiva ELIPSIS, 7 +x, 373 .
(acceso de atributos), 371
método .add_done_callback(), 512 ^
operator, 258, 288 _
método .append, 55 método .done(), 512
(guión bajo), 28, 264 __
método .frombytes, 251 método .pop, 55
(guión bajo doble), 3, 4 __add__,
método .result(), 512 suma de vectores
38, 308, 375, 392 __bool__, 12
2D, 9 404 errores ( No encontrado), 525 <
__builtins__, 63 __bytes__, 248
operador, 384 <= operador, 384
__call__, 145 __class__, 616
__delattr__, 615, 618 __borrar__,
625 __eliminar__, 235 __dict__,
63, 147, 616 __doc__, 140, 146

Nos gustaría escuchar sus sugerencias para mejorar nuestros índices. Envíe un correo electrónico a [email protected].

725
Machine Translated by Google

__entrar__, 450 declaración, 328


__eq__, 385 definición y uso, 324–335
__salir__, 450 definición de término, 715 uso
__flotante__, 259 explícito de interfaces, 359
__formato__, 248, 253, 294, 303 escritura de ganso y, 341 en
__getattribute__, 618 __getattr__, biblioteca estándar, 321 paquete
285, 618 __getitem__, 4, 70, 283, de números, 323 creación de
308, 310 __6__2, 58 __hash , 2, 88 subclase, 329 prueba de subclase,
__hash , 7__2,88 __iadd__, 38, 392 335 proceso de subclasificación,
__imatmul__, 383 __init__, 416, 592, 319 detalles de sintaxis, 328
669 __int__, 259 __invert__, 372 creación de subclases virtuales,
__iter__ , 404, 406, 409 __len__, 4, 332
14 , 283 __matmul__ , 383 __missing__ , lenguaje ABC, 19, 60, 715
72 ____mro . __new__, 592, 622 función abs, 10 valores
__next__, 406, 411 __ne__, 385 absolutos, 10 métodos de
__pos__, 372 __prepare__, 675 acceso, 585, 621, 715 funciones de
__radd__, 378 __reprep__, 11 acumulación, 434
__rmatmul__, 383 __rmul__, 380 Patrón adaptador,
__ruml__, 12 __self, 618 __rmul__ , adición 356
318 ,, 618 , 610 __StotT 264, 616, 690 Vector 2D, 9
__str__, 11 __subclasshook__, 339 {} vectores, 375–380
(llaves), 22 ~ operador, 372 – operador, clases agregadas, 360
372 … (puntos suspensivos), 35, 277 paquete aiohttp, 548, 573
algoritmos de búsqueda
binaria, 44 algoritmo C3,
355 criptográfico, 517,
700 para tablas hash, 88
RC4, 700 algoritmo de
clasificación Timsort, 62
Algoritmo de colación Unicode
(UCA), 126 creación de alias, 221, 715 y operador,
372 funciones anónimas, 143, 164, 192 módulo
arcfour.py, 700 listas de argumentos, 143 argumentos
definición de término, 715 yo explícito, 630, 652
congelación con functools.parital, 159 agarrar exceso
arbitrario, 29 instancia, 630 palabra clave -solo, 148
operadores aritméticos, 12, 13, 156, 372 generador
de progresión aritmética, 420 biblioteca array.array,
251 ventajas de matrices, 48 construcción con
expresiones generadoras, 25

A
ABC (Abstract Base Class)
ventajas de, 316 uso
apropiado de, 308, 317, 341 como
mixins, 360

726 | Índice
Machine Translated by Google

creación, guardado y carga, 48 manejo atributos

con vistas de memoria, 51 manejo con asignación arbitraria, 147


NumPy, 52 vs. listas, 49 función asciize, definición de término, 715
123 asignación aumentada, 13, 38–42, eliminación, 614 dinámico,
388–392 desestructuración, 721 de variables, 585–604 acceso dinámico, 284
220 sobrescritura de descriptores con, 645 manejo de, 616 instancia, 649
paralelo, 28, 721 a segmentos, 36 listado, 147 administrado, 627,
operaciones asíncronas, 552, 580 paquete 720 nombres, 591 de funciones
asyncio, 538–577 API proporcionadas por, definidas por el usuario, 147
57 operaciones asíncronas, 552 anulación, 267 privado y
asyncio.as_completed, 555 asyncio.Future protegido, 262, 273, 308
class, 545 asyncio.Task objetos, 547 público, 272, 308 especial, 616
asyncio. esperar (…), 550 evitar el bloqueo de almacenamiento, 627, 631, 723
bucles de eventos, 560 beneficios de, 581 validación con descriptores, 625–640 validación
corrutinas en, 541 corrutinas frente a futuros, con propiedades, 604 asignación aumentada,
546 desarrollo de, 538 mejora del script de 13, 38–42, 388–392
descarga, 554 descarga con el paquete
aiohttp, 548 servidor TCP, 568 time.sleep
(… ), 543 frente al módulo Threading, 539
escribiendo servidores asincrónicos, 567–
577 rendimiento de la construcción y, 546,
551 acceso a atributos (.), 371 descriptores
B
de atributos, 625–651
barra invertida (\), 22
BDFL (Dictador benevolente de por vida), 716
BIF (ver funciones integradas)
ordenación de bytes big-endian, 110
algoritmo de búsqueda binaria, 44
construcción de secuencias binarias,
101 tipos integrados para, 99
definición de término, 716
visualizaciones utilizadas, 100
método de clase fromhex, 101
compartir memoria, 101

validación de atributos, 625–640 compatibilidad con el método str,


módulo 100 bisect
docstrings, 650 métodos como, 646
anulación de eliminación, 650
inserción con bisect.insort, 47 funciones
anulación frente a no anulación, 640–
principales, 44 búsqueda con función
646 descripción general de, 625 sobreescritura,
645 consejos de uso, 648 uso, 626 frente a fábricas bisect, 44 operadores bit a bit, 13, 372
bloqueo de funciones de E/S, 515, 552 micromarco
de propiedades, 635 validación de atributos
Bobo HTTP, 150, 163 BOM (marca de orden de
bytes), 110, 716 bool (x), 12 valores booleanos, tipos
personalizados y, 12 métodos enlazados, 716
funciones integradas, 42, 63, 144, 616, 716 métodos
integrados, 144 cadenas de bytes, 716
nombres de atributos de almacenamiento
automático, 631 nuevos tipos de descriptor, 637
clase de descriptor simple, 626

Índice | 727
Machine Translated by Google

tipo bytearray, 99 metaclase para personalizar descriptores, 673


bytes (ver codificación/descodificación) metaclases frente a decoradores de clase, 655
argumento de bytes, 129 tipo de bytes, __preparar__, 675 clases agregadas, 360 como
99 objetos similares a bytes, 716 objetos invocables, 145 como objetos, 677, 682

personalización en tiempo de ejecución (ver


metaprogramación de clase) definición de término,
716 descriptor, 625, 635, 718 gestionado, 626, 720
metaclases, 666–673, 720 notación MGN para, 628
C Algoritmo C3, 355
mixin, 359, 362–366, 721 jerarquías multinivel, 367
descriptores de

atributos de cachés y, 649 utilizando


la clase WeakValueDictionary, 237 referencias
débiles y, 236 llamada por referencia, 245
llamada compartida, 229, 245 ABC invocable, 323
objetos invocables, 144, 716 función invocable(),
144 devoluciones de llamada callback hell, 562 vs.
coroutines, 562 vs. futures, 562 CamelCase, 622, decorador de método de clase,
683, 716 equivalentes canónicos, 117 ejemplo de
252 cierres (ver decoradores y cierres) punto
baraja de cartas, 4–8 productos cartesianos,
de código, 98, olor de código 716 , 317, código
generación de listas a partir de, 23 casos
717 , nivel superior, módulo de códec 662 ,
plegables, 119 ChainMap, punto de código de
103, modismos de codificación 717 , colecciones
75 caracteres identificación, 98 compatibilidad,
304
118 definición de término, 98 codificación/
descodificación de, 98 Unicode estándar para, 98
Chardet Universal Character Encoding Detecÿ tor, definición de término, 717
109 módulo charfinder.py, 568 Cheese Shop, 716 clase
iterabilidad de, 402 módulo
decoradores inconvenientes de, 661 para personalizar collections.abc
descriptores , 659 frente a decoradores de funciones, ABCs in, 321
660 frente a metaclases, 655 metaprogramación de
colecciones.ChainMap, 75
clases, 655–679 fábrica de clases, 656 clases colecciones.Contador, 75
como objetos, 677 personalización de descriptores, colecciones.defaultdict, 66, 70
659 funciones exec/eval y, 659 tiempo de
colecciones.clase deque, 55
importación frente a tiempo de ejecución, 661–666
colecciones.MutableSequence, 319
conceptos básicos de metaclases , 666–673
colecciones.MutableSet, 82
colecciones.función namedtuple, 30
colecciones.OrderedDict, 66, 75
collections.UserDict, 76 Mapping/
MutableMapping ABCs, 64, 322 herencia múltiple
en, 356 Patrón de diseño de comandos, 177
operadores de comparación, 13, 372, 384–388 caracteres
de compatibilidad, 118 composición, 361 compuestos,
60 subclases concretas, 320, 329, 360 operaciones
asincrónicas de concurrencia, 552, 580 mejor enfoque
para, 534 secuencias de comandos concurrentes frente
a secuencias de comandos, 507

728 | Índice
Machine Translated by Google

descarga concurrent.futures, 509 generadores como, 439,


lanzamiento de tareas concurrent.futures, 515 465 en asyncio, 541
manejo de errores y, 520, 525 ejemplos de, 505 obteniendo tareas, 547
GIL (Global Interpreter Lock) y, 515 importancia estados posibles de, 465
de, 505 importancia de futuros en, 511 en otros valores devueltos desde, 475
idiomas, 535 descarga múltiple solicitudes, 564 terminación de, 471 frente a
diseño sin bloqueo y, 545 pantallas de progreso, devoluciones de llamada, 562
521 clientes más inteligentes para, 576 pruebas frente a futuros, 546 frente a
de clientes concurrentes, 521 subprocesamiento generadores, 463 frente a
frente a rutinas, 539 alternativas de subprocesos/ subprocesamiento, 539
multiprocesamiento, 530 uso de rendimiento de es decir, 483–489
futures.as_completed, 527 frente a paralelismo, rendimiento por uso, 477–483
537 con paquete asyncio, 539 –577 con función palabra clave de rendimiento y 467
Executor.map, 517 biblioteca concurrent.futures, similitud de coseno, 276 contador, 75
505–531 codificación cp1252, 104 codificación
cp437, 104 CPython, 235, 515, 717
CRUD (crear, leer, actualizar y eliminar) ,
717 algoritmos criptográficos, 517, 700
corchetes ({}), 22

operación tras bambalinas de, 511 beneficios


de, 533 descarga, 509 futures.as_completed,
522, 527 futures.ProcessPoolExecutor, 530 D
introducción de, 505 procesos de
atributos de datos (ver atributos)
lanzamiento con, 515 artículo "Considerado
perjudicial", 717 constructores, definición descriptores de datos (ver descriptores principales)
modelo de datos, 3–16 comportamiento de __len__,
del término, 717 Contenedor ABC, 322
14 valor booleano de tipos personalizados, 12
secuencias de contenedores, 20, 61
contenedores, definición de término, 717 emulación de tipos numéricos, 9 ejemplo de, 4

administradores de contexto utilidades contextlib, protocolo de metaobjetos, 16 descripción general


de, 3 protocolos y secuencias, 310 métodos
454 definición de término, 717 contextos
temporales a través de declaraciones, 447 usos especiales (mágicos), 4, 16 métodos especiales,
descripción general de, 13 métodos especiales,
para, 454 con declaración y, 450 simulación
continua, 489 función de copia, 228 usando, 8 representación de cadenas, 11 frente al

corrutinas beneficios de, 563 cálculo de modelo de objetos, 15 estructuras de datos


diccionarios y conjuntos, 63–95 secuencias, 19–
promedios móviles, 468 decoradores para preparación,
469 definición de término, 717 retraso, 543 evolución 62 texto frente a bytes, 97–136 utilidad de

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

classmethod vs staticmethod, 252 ejemplo


de cierre, 192

Índice | 729
Machine Translated by Google

descripción general del comprensiones de dict (dictcomp), 66 dict.get,


cierre, 195 cierres frente a funciones anónimas, 68 dict.setdefault, 69 diccionarios y conjuntos,
192 comportamiento del decorador, 198 63–95 creación de diccionarios, 65 creación
implementación del decorador, 196 definición de nuevos tipos de mapeo, 76 dictcomp
de cierres, 192 definición de decoradores, 184, (comprensiones de dict), 66 mapeos
717 ámbito dinámico, 213 en la biblioteca inmutables, 77 implementación con
estándar de Python, 199–205 función clave de tablas hash, 85–93 descripción general de
los decoradores, 185 no local declaración, 195 métodos de mapeo, 66 tipos de mapeo, 64
decoradores parametrizados, 206–211 mapeos con búsqueda de clave flexible, 70
decoradores de preparación, 543 propósito de consecuencias prácticas, 93 teoría de conjuntos,
los decoradores, 183 decoradores de registro, 79 variaciones de dict, 75 función dir, 146 función
187, 206 decoradores apilados, 205 decoradores dir([objeto]), 616 módulo dis, 191 discreto
de funciones apiladas, 329 reglas de alcance procesos de simulación de eventos (DES) en,
variable, 189 copias en profundidad, 228, 718 490 taxi_sim.py simulación, 490 taxi_sim.py
función de copia en profundidad, 228 dictado secuencia de comandos de soporte, 696 enfoques
por defecto , 66, 70 default_factory, 71 de subprocesamiento, 490 frente a simulación
declaración del continua, 489 visualizaciones, formateadas, 253
Django framework, 147, 362–366 docstrings (cadenas
de documentación) , 718 paquete de pruebas doctest

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

servidor web aiohttp, 573


servidor asyncio TCP, 568
simultáneo frente a secuencial, 505
Adaptador, 356 manejo de errores para, 520, 525
eligiendo la mejor Estrategia, 175 solicitudes múltiples para, 564
Estrategia clásica, 168 pantallas de progreso, 521, 527 con
Comando, 177 paquete aiohttp, 548 principio DRY
Decorator, 199 (no se repita), 718 pato escribiendo definición de
búsqueda de estrategias en módulos, 176 término, 718 ejemplo de, 68 origen de término, 314
Estrategia orientada a funciones, 172 orígenes de, 303 protocolos y, 279 Soporte de
relevancia dependiente del idioma de, 167, 181 Python para, 247
asignación de desestructuración, 721 diacríticos, 121
problema de diamantes, 351–356 dict, 87

730 | Índice
Machine Translated by Google

variables ficticias, 28 API de política de bucle de eventos,


métodos de dunder, 4 (ver 581 simulación de eventos (ver simulación de eventos
también métodos especiales) discretos (DES)) función exec, 659 Función
atributos dinámicos, disputa de datos, Executor.map, 517 Función Executor.submit, 520
586 exploración de datos
similares a JSON con, 588 creación de
objetos flexibles, 592 problema de nombre
de atributo no válido, 591 recuperación de
registros vinculados, 598 descripción general
de, 585 reestructuración datos con módulo de F diseño a prueba de fallas,
68, 718 falsedad, 12, 718
estantería, 594 alcance dinámico, 213 lenguajes de
objetos similares a archivos,
tipo dinámico, 308, 344
719 función de filtro, 23, 142
funciones de primera clase

funciones anónimas, 143, 164 como


objetos de primera clase, 139 objetos
E Principio EAFP, 449, 718 invocables, 144 definición de término ,
objetos ansiosos, 718 719 patrones de diseño con, 167–182
multiplicación por elementos, 380 puntos manejo flexible de parámetros, 148
suspensivos (…), 35, 277 cláusula else, anotaciones de funciones, 154
448 codificación/descodificación, 97–136 introspección de funciones, 146 paquetes
códecs básicos, 103 codificadores y de programación funcional, 156–161
decodificadores básicos, 103 BOM funciones de orden superior, 141 recuperación de
(marca de orden de bytes) , 110 bytes y información de parámetros, 150 tratamiento de funciones
bytearrays, 99 plegado de mayúsculas y como objetos, 140 llamadas definidas por el usuario
minúsculas, 119 conversión de punto de tipos, 145 objetos de primera clase, 139, 681 módulo
código durante, 98 configuración de flags2_common.py, 521, 703 secuencias planas, 20, 61,
codificación predeterminada, 114 719 representación de cadenas flexibles, 136 flyweights, 174
determinación de codificación de secuencia funciones de plegado, 434 función fold_equal, 120 bucles
de bytes, 109 signos diacríticos, 121 API de str y for, 293 for/else cláusula, 448 función de formato, 253
bytes de modo dual, 129 ejemplo de, 98 coincidencia Minilenguaje de especificación de formato, 254, 294 Biblioteca
de texto normalizado, 120 descripción general de, 97 de sintaxis de cadenas de formato, 11, 254 visualizaciones
problemas encontrados, 105–111 clasificación de formateadas, 253 404 errores (no encontrados), 525 variables
texto Unicode, 124 clasificación con Unicode Collation libres, 194, 214 método de clase fromhex, 101 frozenset, 79
Algorithm (UCA), 126 representación de cadenas en función fsdecode(nombre de archivo), 131 fse función
RAM, 136 representación de cadenas y caracteres, ncode(nombre de archivo), 131 invocación de función (()),
98 estructuras y vistas de memoria, 102 archivos de 371 sobrecarga de función, 203 parámetros de función, 229
texto, 111–117 base de datos Unicode , 127
Normalización Unicode, 117–124 descriptores
forzados (ver descripción anulada).

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

paquetes de programación funciones generadoras


funcional para, 156–161 definición de término, 145, 719
con Python, 163 funciones implementación de iteración con, 412
en biblioteca estándar, 424–433 sintaxis
acumulando, 434 de, 441 corrutinas generadoras como,
anónimo, 143, 164, 192 como 465 definición de término, 719 delegación,
objetos de primera clase, 681 478 en utilidad de conversión de base
asignando atributos arbitrarios a, 147 de datos, 437 subgeneradores, 478
bloqueando E/S, 515, 552 integrado, 42, frente a corrutinas , 463 frente a
63, 144, 616, 716 integrado frente a especial iteradores, 401 funciones genéricas,
métodos, 8 decoradores y cierres, 183– 202, 719 función getattr(objeto, nombre[,
215, 329 definición de término, 719 patrones predeterminado]), 617 GIL (Global
de diseño con, 167–182 primera clase, 139– Interpreter Lock) beneficios de, 534
165, 719 plegado, 434 generador, 145, 441, bloqueo de E/S y, 515 limitaciones de, 514
719 genérico, 719 de orden superior, 141, función global, 176 Lenguaje Go, 345 GoF (Gang of
159, 719 definido por el usuario, 144 usando Four), 719 escritura de ganso definiendo/usando un
operadores aritméticos como, 156 módulo de ABC, 324 definición de término, 315 ejemplo de,
funciones 319 verificaciones explícitas contra tipos
abstractos, 381 introducción de término, 341
registro de subclases virtuales con, 332
__subclasshook__ y, 339, 405

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

paquete heapq, 57 es operador, 223, 372


funciones de orden superior, 141, 159, 719 comprobaciones de
protocolo HTTP, 548 instancia, 317 script
isis2json.py, 691 codificación
yo iso8859_1, 104 acceso/corte de
elementos ([]), 371 llamada de
modismo, definición de término, 720
función iter con 2 argumentos, 436
sentencias if, 12 cláusula if/else, 448
pasos implementados por, 404 vs.
inmutabilidad, 244 secuencias
método especial, 8 palabras iteración
inmutables, 20, 312 herencia de
por palabra con, 402 ABC iterable, 322
implementación, 359 tiempo de objetos iterables, 405, 720 desempaquetado
importación, 185, 661, 720 suma in situ,
iterable, 28, 720 naturaleza fundamental de
38, 392 operadores infijos @ signo y,
iteración de, 401 naturaleza implícita de, 7
380 manejo de excepciones, 380 tratamiento especial durante, 311 con
nombres de métodos, 382 operandos y, funciones de generador, 412 generador
376 sobrecarga de operadores y,
de progresión aritmética de patrón de
371 mecanismo de envío especial
iterador , 420 beneficios de, 401
para, 377 herencia
implementación clásica de, 409 errores
comunes, 411 iterabilidad determinante, 405
expresiones generadoras y, 417 funciones
generadoras y, 412, 424–433 función integrada
iter, 404, 436 funciones reductoras iterables,
entre idiomas, 368 entre 434 iterables vs. iteradores, 405 implementación
tipos de mapeo genéricos, 64 de UserDict,
perezosa, 416 ejemplo práctico de, 437
76 interfaz frente a implementación, 359
iteración palabra por palabra, 402 definición
falta de en subclases virtuales, 332
de término de iteradores, 409, 720 entidades
trampas de subclases múltiples (ver
admitidas por, 402 frente a generadores, 401
herencia múltiple), 348 frente a composición,
frente a iterables, 405 frente a subgeneradores,
276, 361 inicializadores, definición de 479 módulo itertools , 423, 424
término, 720 funciones de ordenación
interna, 44 argumentos de instancia, 630
atributos de instancia, 649 instancias

como objetos invocables,


145 atributos frente a propiedades,
608 construcción de, 592 descriptor,
627 gestionado, 627, 720 notación
MGN para, 628 herencia de
interfaz, 359 interfaces

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

creación de nuevos tipos de mapeo, 76


inmutable, 77 tipos de mapeo, 64
Palabra clave L lambda,
descripción general de métodos, 66
143 codificación latin1, 104
mutable, 237 variaciones de dict, 75 con
evaluación perezosa, 396, 416
búsqueda de clave flexible, 70
objetos perezosos, 720 función
len, 8, 14 alcance léxico, 214
representaciones de longitud
Mapeo ABC, 322
limitada, 277 saltos de línea, 22 lenguaje
Clase contenedora MappingProxyType, 77
Lisp, 401 listas de comprensión (listcomps)
MappingView ABC, 322
como alternativa para mapear/filtrar funciones,
memorización, 200
142 construir secuencias con, 21 definición
vistas de memoria, 51, 102, 309
de término, 720 generar listas a partir de productos
secuencia de comandos memtest.py,
cartesianos, 23 listas anidadas, 37 legibilidad y, 21 690 conceptos básicos de
ámbito variable, 22 frente a expresiones generadoras,
metaclases, 666–673 definición
25 frente a mapa y filtro, 23 método list.sort, 42 listas
de término, 720 ejercicio de
tiempo de evaluación, 669 para
personalizar descriptores, 673 protocolo
de metaobjetos, 16 metaprogramación, 185,
655, 721
(ver también metaprogramación de clases)
sobrecarga de métodos, 203 métodos

alternativas a, 48–57 listas


accesor, 715
de argumentos, 143
como descriptores de atributos, 646
creación de listas de listas, 37
como objetos invocables, 144
elementos vistos por última
enlazados, 716 integrados, 144
vez, 55 tipos mixtos en, 61
definición de término, 585 mixin,
clasificación, 42 uso de tuplas
721 especial (ver métodos
como inmutables, 32 frente a matrices,
especiales) no enlazado, 353, 723
49 frente a deque, 56 ordenación de
bytes little-endian , 110 propiedad de
vitalidad, 720 configuración local, 124
Principio de acceso uniforme de Meyer (UAP), 620
función locale.strxfrm, 125 decorador
MGN (Notación Mills & Gizmos), 628 teclas
lru_cache, 200 Receta de refactorización
faltantes, 68 clases de mezcla, 359, 362–366,
lambda de Lundh, 144 principio LYBYL,
721 métodos de mezcla, 721 parches mono, 312,
449
345, 645, 721

MRO (Orden de resolución de métodos), 334, 351–356 división


multidimensional, 35 jerarquías de clases multinivel, 367
M mejores prácticas de herencia múltiple, 359–362 inconvenientes

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

multiplicación invocable, 144, 716

propiedad conmutativa de, 12 elemento clases como, 677


a elemento, 380 escalar, 10, 380 administradores de contexto,
paquete de multiprocesamiento, 57 717 creación con __nuevo__, 592
multiprocesamiento, alternativas a, 530 decoradores, 717 copias profundas

mutabilidad, 244 asignaciones mutables, 237 de, 228, 718 destrucción de, 245
secuencias mutables, 20, 312 ansioso, 718 similar a un archivo,

719 primera clase, 139, 681 hashable,


65 , 257, 719 iterable, 405, 720

Mapeo mutable ABC, 64 iteradores, 720 lazy, 720 mutable,


MutableSet, 82 230, 244 Pythonic, 247–274 referente,
mutadores, 715 236, 722 serialización de, 722 copias
superficiales de, 225, 722 singletons,

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

manejo de arreglos con, 52


instalación, 54 matemática
vectorial con, 276

composición de objetos, 361 ID


de objetos, 223 modelo de P
objetos, 15 (ver también modelo
asignación paralela, 28, 721 tareas
de datos) concepto de
paralelas, lanzamiento, 515 paralelismo,
publicación de objetos, 163 lenguajes
frente a concurrencia, 537 decoradores
orientados a objetos, 3 objetos que se
parametrizados, 206–211 parámetros que
comportan como funciones, 145 similares
evitan objetos mutables por defecto, 230
a bytes, 716
definición de término, 721

Índice | 735
Machine Translated by Google

manejo flexible de, 148 Objetos pitónicos, 247–274


parámetros de función como referencias, 229 constructor alternativo para, 251
pasar, 245 recuperar información sobre, 150 métodos de clase frente a decoradores de métodos estáticos,
usar tipos mutables, 232 252

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

S módulo sanitize.py, 121

736 | Índice
Machine Translated by Google

multiplicación escalar, 380 secuencias de memoria compartida, 51,


módulo schedule1.py, 595, 708 102 módulo de estantería, 594 SimPy,
beneficios de SciPy, 52 instalación, 490 simulación, discreta frente a continua,
54 matemática vectorial con, 276 489 guión bajo único (_), 264 decorador de
reglas de alcance, 189 problemas envío único, 202 singletons, 223, 722 tamaño
de seguridad, con atributos ABC, 322 asignación de cortes a cortes, 36
privados, 273, 308 argumento propio, definición de término, 722 demostración de,
630, 652 secuencia ABC, 322 secuencia interfaz, 310 281 características de, 33 multidimensional, 35
protocolo de secuencia, 310, 402 secuencias, 19–62 objetos de corte, 34 serpiente_caso, 722
alternativa a listas, 48 matrices, 48 asignación aumentada clasificación, 42, 62 atributos especiales,
con, 38–42 binario, 99, 716 construcción, 21–26 616 métodos especiales ventajas de, 6
concatenación de copias múltiples de, 36 definición de términos alternativos para, 4 operadores
término, 722 flat, 719 flat vs. container, 61 manejo de aritméticos, 12, 13 valor booleano de tipos
arreglos con vistas de memoria, 51 manejo con personalizados, 12 tipos incorporados, 8
deques y colas, 55 manejo con NumPy/SciPy, 52 definición de término, 723 emulación de
plano inmutable, 276 iterabilidad de, 404 manejo tipos numéricos, 9 ejemplo de implementación,
ordenado, 44 mutable, 36, 312 resumen de construido 4, 9 para manejo de atributos, 617 naturaleza
-in, 20 división de, 33–36, 722 clasificación, 42 implícita de, 8 denominación de, 3 descripción
tuplas, 26–33 Diagrama de clase UML para, 20 general de disponible, 13 propósito de,
serialización, 722 Set ABC, 322 hashability de Representación de 3 cadenas, 11 uso de,
elementos de teoría de conjuntos, 79 historia de 8 corchetes ([]), 6, 22, 35, 371 decoradores
conjuntos, 79 operaciones matemáticas de conjuntos, apilados, 205 verificación de tipo estático,
83 descripción general de, 79 operadores de 343 decorador de método estático, 252
comparación de conjuntos, 84 comprensiones de atributos de almacenamiento, 627, 631,
conjunto (setcomps), 81 literales de conjunto, 80 723 argumento str, 129 función str , 8, 248
operación de conjunto s, 82 función setattr(objeto, Método str.casefold(), 119 Método
nombre, valor), 617 función setlocale, 124 conjuntos str.format, 11, 253 Patrón de estrategia ern,
(ver diccionarios y conjuntos) copias superficiales, 168–177, 187 concepto de cadenas de, 98
225, 722 representación de, 11, 136, 248 StrKeyDict,
76 referencias sólidas, 723

Índice | 737
Machine Translated by Google

tipificación fuerte frente a débil,


módulo de estructura 344 , 102
subclases Operadores unarios, sobrecarga de, 372
métodos independientes, 353, 723 guión bajo
hormigón, 320, 329
(_), 28, 264 equivalentes canónicos de Unicode,
creación, 329
117 plegado de mayúsculas y minúsculas, 119
declaración virtual, 332
representaciones de caracteres frente a
trampas de la subclasificación,
bytes, 98 combinación de caracteres en,
348 pruebas, 335 virtual, 338,
117 caracteres de compatibilidad, 118 base de
381, 724 subgeneradores, 478
datos para, 127 signos diacríticos, 121 calificadores
función super(), 287 manejador de
imprecisos en, 135 normalización de, 117
errores de códec de escape sustituto,
coincidencia de texto normalizado, 120 posibles
131
errores de codificación/descodificación, 105
Error de sintaxis, 108
clasificación de texto, 124 SyntaxError, 108
Unicode Collation Algorithm (UCA), 126 Unicode
T sandwich, 111 UnicodeDecodeError, 106
tablas, 60 UnicodeEncodeError, 105 principio de acceso
Tareas, obtención en asyncio, 547 uniforme, 585, 620, 723 tipos invocables definidos
archivos de texto, codificación/descodificación, por el usuario, 145 definición de término, 723
111–117 coincidencia de texto, 120 intérpretes funciones, 144 UserDict, 76 codificación utf-16le, 105
seguros para subprocesos, 515 Módulo de codificación utf-8, 105

subprocesamiento, 530, 539 time.sleep(…), 543


Algoritmo de clasificación Timsort, 62 Tkinter Kit
de herramientas GUI, 356, 361 código de nivel
superior, 662 paquete TQDM, 521, 527 objetos de
seguimiento, 236 proyecto Trollius, 538 veracidad,
12, 723 cláusula try/else, 448 definición de término
de desempaquetado de tupla, 723 ejemplos de, 27
agarrar exceso elementos, 29 anidados, 29 frente
a desempaquetado iterable, 28 construcción de
tuplas con expresiones generadoras, 25 con
nombre, 30 inmutabilidad relativa de, 224, 240
referencia de retorno a, 240 comportamiento
similar a una secuencia de, 60 usados como
Descriptores de validación V , 648
listas inmutables, 32 usados como registros ,
reglas de alcance variable, 189
26 sugerencias de tipo, 343 metaclase de tipo,
variables como notas adhesivas,
667 tipo, definición de término, 723 operador
220 ficticias, 28 libres, 194
t[:], 240
referencia, 220 función
vars([objeto]), 617 suma de
vectores, 375–380

Modelo de espacio vectorial,


276 subclases virtuales, 332, 338, 381, 724

W
verrugas, definición de término, 724
referencias débiles, 236, 724

738 | Índice
Machine Translated by Google

escritura débil frente a fuerte, 344 Y


clase WeakKeyDictionary, 239 módulo
rendimiento de la
de referencia débil, 239 clase
construcción como para el reemplazo
WeakValueDictionary, 237 descargas
web servidor web aiohttp, 573 servidor del bucle, 433 en el paquete asyncio,
546, 551 en el proyecto Trollius, 538
TCP asyncio, 568 simultáneo frente
característica principal de, 478
a secuencial, 505 manejo de errores
significado de, 483 significado de,
para, 520, 525 solicitudes múltiples
502 usando, 477–483 rendimiento
para, 564 progreso pantallas, 521, 527
de corrutinas de palabras clave y,
con paquete aiohttp, 548 instrucciones
467 en funciones generadoras, 413
while, 12 cláusula while/else, 448 con
propósito de, 401, 463
comportamiento de declaraciones de,
447 administradores de contexto y, 450
utilidad de, 461
proyecto Trollius y, 538

Función cremallera Z , 293

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.

También podría gustarte