[go: up one dir, main page]

0% encontró este documento útil (0 votos)
292 vistas282 páginas

Programacion 1

Este documento presenta conceptos básicos sobre la codificación de clases en Python. Explica que las clases generan objetos de instancia múltiples que heredan atributos de clase y tienen su propio espacio de nombres, mientras que los objetos de clase proporcionan comportamiento predeterminado y sirven como fábricas. También presenta un primer ejemplo de una clase simple llamada FirstClass que define métodos setdata y display.

Cargado por

raull
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
292 vistas282 páginas

Programacion 1

Este documento presenta conceptos básicos sobre la codificación de clases en Python. Explica que las clases generan objetos de instancia múltiples que heredan atributos de clase y tienen su propio espacio de nombres, mientras que los objetos de clase proporcionan comportamiento predeterminado y sirven como fábricas. También presenta un primer ejemplo de una clase simple llamada FirstClass que define métodos setdata y display.

Cargado por

raull
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
Está en la página 1/ 282

Machine Translated by Google

CAPÍTULO 27

Conceptos básicos de codificación de clases

Ahora que hemos hablado de OOP en abstracto, es hora de ver cómo se traduce esto en código real. Este
capítulo comienza a completar los detalles de sintaxis detrás del modelo de clase en Python.

Si nunca ha estado expuesto a OOP en el pasado, las clases pueden parecer algo complicadas si se toman
en una sola dosis. Para hacer que la codificación de clases sea más fácil de absorber, comenzaremos nuestra
exploración detallada de OOP dando un primer vistazo a algunas clases básicas en acción en este capítulo.
Ampliaremos los detalles presentados aquí en capítulos posteriores de esta parte del libro, pero en su forma
básica, las clases de Python son fáciles de entender.

De hecho, las clases tienen solo tres distinciones principales. En un nivel básico, en su mayoría son solo
espacios de nombres, muy parecidos a los módulos que estudiamos en la Parte V. Sin embargo, a diferencia
de los módulos, las clases también admiten la generación de múltiples objetos, la herencia de espacios de
nombres y la sobrecarga de operadores. Comencemos nuestro recorrido por las declaraciones de clase
explorando cada una de estas tres distinciones a la vez.

Las clases generan objetos de múltiples instancias


Para comprender cómo funciona la idea de objetos múltiples, primero debe comprender que hay dos tipos de
objetos en el modelo OOP de Python: objetos de clase y objetos de instancia . Los objetos de clase
proporcionan un comportamiento predeterminado y sirven como fábricas para objetos de instancia.
Los objetos de instancia son los objetos reales que procesan sus programas: cada uno es un espacio de
nombres por derecho propio, pero hereda (es decir, tiene acceso automático a) los nombres de la clase a
partir de la cual se creó. Los objetos de clase provienen de declaraciones y las instancias provienen de
llamadas; cada vez que llama a una clase, obtiene una nueva instancia de esa clase.

Este concepto de generación de objetos es muy diferente de la mayoría de las demás construcciones de
programas que hemos visto hasta ahora en este libro. En efecto, las clases son esencialmente fábricas para
generar múltiples instancias. Por el contrario, solo se importa una copia de cada módulo en un solo programa.
De hecho, esta es la razón por la cual la recarga funciona como lo hace, actualizando un objeto compartido
de una sola instancia en su lugar. Con las clases, cada instancia puede tener sus propios datos independientes,
admitiendo múltiples versiones del objeto que la clase modela.

797
Machine Translated by Google

En este rol, las instancias de clase son similares al estado por llamada de las funciones de cierre (también conocidas
como fábrica) del Capítulo 17, pero esta es una parte natural del modelo de clase, y el estado en las clases son atributos
explícitos en lugar de referencias de alcance implícitas. Además, esto es solo parte de lo que hacen las clases: también
admiten la personalización por herencia, la sobrecarga de operadores y múltiples comportamientos a través de métodos.
En términos generales, las clases son una herramienta de programación más completa, aunque la POO y la programación
de funciones no son paradigmas mutuamente excluyentes. Podemos combinarlos usando herramientas funcionales en los
métodos, codificando métodos que son en sí mismos generadores, escribiendo iteradores definidos por el usuario (como
veremos en el Capítulo 30), etc.

El siguiente es un breve resumen de los elementos básicos de Python OOP en términos de sus dos tipos de objetos. Como
verá, las clases de Python son en cierto modo similares tanto a las definiciones como a los módulos, pero pueden ser
bastante diferentes de lo que está acostumbrado en otras redes.
calibres

Los objetos de clase proporcionan un comportamiento

predeterminado Cuando ejecutamos una declaración de clase , obtenemos un objeto de clase. Aquí hay un resumen de
las principales propiedades de las clases de Python:

• La declaración de clase crea un objeto de clase y le asigna un nombre. Al igual que la sentencia de definición de
función, la sentencia de clase de Python es una sentencia ejecutable .
Cuando se alcanza y ejecuta, genera un nuevo objeto de clase y lo asigna al nombre en el encabezado de la clase .
Además, al igual que las definiciones, las declaraciones de clase normalmente se ejecutan cuando los archivos en
los que están codificados se importan por primera vez. • Las asignaciones dentro de las declaraciones de clase

crean atributos de clase. Al igual que en los archivos de módulos, las asignaciones de nivel superior dentro de una
declaración de clase (no anidadas en una definición) generan atributos en un objeto de clase. Técnicamente, la
declaración de clase define un ámbito local que se transforma en el espacio de nombres de atributo del objeto de
clase, al igual que el ámbito global de un módulo. Después de ejecutar una declaración de clase , se accede a los
atributos de clase por calificación de nombre: objeto.nombre. • Los atributos de clase proporcionan el estado y
el comportamiento del objeto. Los atributos de un objeto de clase registran información de estado y comportamiento

para ser compartida por todas las instancias creadas a partir de la clase; Las instrucciones de definición de función
anidadas dentro de una clase generan métodos, que procesan instancias.

Los objetos de instancia son elementos concretos

Cuando llamamos a un objeto de clase, obtenemos un objeto de instancia. Aquí hay una descripción general de los puntos
clave detrás de las instancias de clase:

• Llamar a un objeto de clase como una función crea un nuevo objeto de instancia. Cada vez que se llama a una
clase, crea y devuelve un nuevo objeto de instancia. Las instancias representan elementos concretos en el dominio
de su programa.

798 | Capítulo 27: Conceptos básicos de codificación de clases


Machine Translated by Google

• Cada objeto de instancia hereda atributos de clase y obtiene su propio espacio de nombres.
Los objetos de instancia creados a partir de clases son nuevos espacios de nombres; comienzan vacíos
pero heredan atributos que viven en los objetos de clase a partir de los cuales se generaron. • Las

asignaciones a los atributos de uno mismo en los métodos crean atributos por instancia.
Dentro de las funciones de método de una clase, el primer argumento (llamado self por convención)
hace referencia al objeto de instancia que se está procesando; asignaciones a atributos de autocreación
o cambio de datos en la instancia, no en la clase.

El resultado final es que las clases definen datos y comportamientos comunes y compartidos, y generan
instancias. Las instancias reflejan entidades de aplicación concretas y registran datos por instancia que
pueden variar según el objeto.

Un primer ejemplo

Veamos un ejemplo real para mostrar cómo funcionan estas ideas en la práctica. Para comenzar, definamos
una clase llamada FirstClass ejecutando una declaración de clase de Python de forma interactiva:
>>> clase PrimeraClase: # Definir un objeto de
clase def setdata(self, value): # Definir los métodos de la
clase self.data = value # self es la instancia def
display(self):
print(self.data)
# self.data: por instancia

Estamos trabajando de forma interactiva aquí, pero normalmente, dicha declaración se ejecutaría cuando se
importe el archivo del módulo en el que está codificado. Al igual que las funciones creadas con defs, esta
clase ni siquiera existirá hasta que Python alcance y ejecute esta declaración.

Como todas las declaraciones compuestas, la clase comienza con una línea de encabezado que enumera el
nombre de la clase, seguida de un cuerpo de una o más declaraciones anidadas y (generalmente) sangradas.
Aquí, las sentencias anidadas son defs; definen funciones que implementan el comportamiento que la clase
quiere exportar.

Como aprendimos en la Parte IV, la definición es realmente una tarea. Aquí, asigna objetos de función a los
nombres setdata y display en el alcance de la declaración de clase , y así genera atributos adjuntos a la clase:
FirstClass.setdata y FirstClass.display. De hecho, cualquier nombre asignado en el nivel superior del bloque
anidado de la clase se convierte en un atributo de la clase.

Las funciones dentro de una clase generalmente se denominan métodos. Están codificados con definiciones
normales y son compatibles con todo lo que ya hemos aprendido sobre las funciones (pueden tener valores
predeterminados, devolver valores, producir elementos a pedido, etc.). Pero en una función de método, el
primer argumento recibe automáticamente un objeto de instancia implícito cuando se le llama: el sujeto de la
llamada. Necesitamos crear un par de instancias para ver cómo funciona esto:

>>> x = PrimeraClase() # Hacer dos instancias


>>> y = PrimeraClase() # Cada uno es un nuevo espacio de nombres

Al llamar a la clase de esta manera (observe los paréntesis), generamos objetos de instancia, que son solo
espacios de nombres que tienen acceso a los atributos de sus clases. hablar correctamente

Las clases generan objetos de múltiples instancias | 799


Machine Translated by Google

Figura 27-1. Las clases y las instancias son objetos de espacio de nombres vinculados en un árbol de clases que se
busca por herencia. Aquí, el atributo "datos" se encuentra en instancias, pero "setdata" y "display" están en la clase
por encima de ellos.

ing, en este punto, tenemos tres objetos: dos instancias y una clase. En realidad, tenemos tres espacios de nombres
vinculados, como se muestra en la Figura 27-1. En términos de programación orientada a objetos, decimos que x “es un”
FirstClass, al igual que y: ambos heredan nombres adjuntos a la clase.

Las dos instancias comienzan vacías pero tienen enlaces a la clase desde la que se generaron. Si calificamos una
instancia con el nombre de un atributo que vive en el objeto de la clase, Python obtiene el nombre de la clase mediante
la búsqueda de herencia (a menos que también viva en la instancia):

>>> x.setdata("Rey Arturo") >>> # Métodos de llamada: self is


y.setdata(3.14159) x # Ejecuciones: FirstClass.setdata(y, 3.14159)

Ni x ni y tienen un atributo setdata propio, por lo que para encontrarlo, Python sigue el enlace de la instancia a la clase.
Y eso es todo lo que hay que hacer con respecto a la herencia en Python: sucede en el momento de la calificación del
atributo y solo implica buscar nombres en los objetos vinculados; aquí, siguiendo los vínculos is-a de la figura 27-1.

En la función setdata dentro de FirstClass, el valor pasado se asigna a self.data. Dentro de un método, self (el nombre
que se le da al argumento más a la izquierda por convención) se refiere automáticamente a la instancia que se está
procesando (x o y), por lo que las asignaciones almacenan valores en los espacios de nombres de las instancias, no
en los de la clase; así es como se crean los nombres de datos en la Figura 27-1 .

Debido a que las clases pueden generar varias instancias, los métodos deben pasar por el argumento self para llegar
a la instancia que se va a procesar. Cuando llamamos al método de visualización de la clase para imprimir self.data,
vemos que es diferente en cada instancia; por otro lado, la visualización del nombre en sí es la misma en x e y, ya que
proviene (se hereda) de la clase:

>>> x.pantalla() # self.data difiere en cada instancia


Rey Arturo >>>
y.display() # Ejecuta: PrimeraClase.display(y)
3.14159

Observe que almacenamos diferentes tipos de objetos en el miembro de datos en cada instancia: una cadena y un
número de punto flotante. Como con todo lo demás en Python, no hay declaraciones de atributos de instancia (a veces
llamados miembros); saltan a la existencia la primera vez que se les asignan valores, al igual que las variables simples.
De hecho, si fuéramos

800 | Capítulo 27: Conceptos básicos de codificación de clases


Machine Translated by Google

para llamar a display en una de nuestras instancias antes de llamar a setdata, desencadenaríamos un error
de nombre indefinido: el atributo denominado data ni siquiera existe en la memoria hasta que se asigna
dentro del método setdata .

Como otra forma de apreciar cuán dinámico es este modelo, considere que podemos cambiar los atributos
de la instancia en la clase misma, asignándolos a uno mismo en los métodos, o fuera de la clase, asignándolos
a un objeto de instancia explícito:

>>> x.data = "Nuevo valor" # Puede obtener/ establecer atributos

>>> x.display() # Fuera de clase también


Nuevo valor

Aunque es menos común, incluso podríamos generar un atributo completamente nuevo en el espacio de
nombres de la instancia asignando funciones a su nombre fuera del método de la clase:

>>> x.otronombre = "spam" # ¡También puede establecer nuevos atributos aquí!

Esto adjuntaría un nuevo atributo llamado otro nombre, que puede o no ser utilizado por cualquiera de los
métodos de la clase, al objeto de instancia x. Las clases suelen crear todos los atributos de la instancia
asignándolos al argumento self , pero no tienen por qué hacerlo: los programas pueden recuperar, cambiar o
crear atributos en cualquier objeto al que tengan referencias.

Por lo general, no tiene sentido agregar datos que la clase no puede usar, y es posible evitar esto con un
código de "privacidad" adicional basado en la sobrecarga del operador de acceso de atributo, como veremos
más adelante en este libro (consulte el Capítulo 30 y el Capítulo 39). Aun así, el acceso gratuito a los atributos
se traduce en menos sintaxis, y hay casos en los que incluso es útil, por ejemplo, en la codificación de
registros de datos del tipo que veremos más adelante en este capítulo.

Las clases se personalizan por herencia Pasemos a la segunda

distinción importante de las clases. Además de servir como fábricas para generar múltiples objetos de
instancia, las clases también nos permiten realizar cambios mediante la introducción de nuevos componentes
(llamados subclases), en lugar de cambiar los componentes existentes en su lugar.

Como hemos visto, los objetos de instancia generados a partir de una clase heredan los atributos de la clase.
Python también permite que las clases hereden de otras clases, lo que abre la puerta a la codificación de
jerarquías de clases que se especializan en el comportamiento: al redefinir los atributos en las subclases que
aparecen más abajo en la jerarquía, anulamos las definiciones más generales de esos atributos más arriba
en el árbol. En efecto, cuanto más bajamos en la jerarquía, más específico se vuelve el software. Aquí
tampoco hay un paralelismo con los módulos, cuyos atributos residen en un único espacio de nombres plano
que no es tan susceptible de personalización.

En Python, las instancias se heredan de las clases y las clases se heredan de las superclases. Estas son las
ideas clave detrás de la maquinaria de la herencia de atributos:

• Las superclases se enumeran entre paréntesis en un encabezado de clase . Para hacer que una
clase herede atributos de otra clase, simplemente enumere la otra clase entre paréntesis en el nuevo

Las clases se personalizan por herencia | 801


Machine Translated by Google

línea de encabezado de la declaración de clase . La clase que hereda generalmente se denomina


subclase, y la clase de la que se hereda es su superclase. • Las clases heredan atributos de sus
superclases. Así como las instancias heredan los nombres de atributos definidos en sus clases, las
clases heredan todos los nombres de atributos definidos en sus superclases; Python los encuentra
automáticamente cuando se accede a ellos, si no existen en las subclases.

• Las instancias heredan atributos de todas las clases accesibles. Cada instancia obtiene nombres
de la clase de la que se genera, así como de todas las superclases de esa clase.
Al buscar un nombre, Python verifica la instancia, luego su clase y luego todas las superclases.

• Cada referencia de objeto.atributo invoca una nueva búsqueda independiente. Python realiza
una búsqueda independiente del árbol de clases para cada expresión de obtención de atributos.
Esto incluye referencias a instancias y clases realizadas fuera de las declaraciones de clase (por
ejemplo, X.attr), así como referencias a atributos del argumento de autoinstancia en las funciones
de método de una clase. Cada expresión self.attr en un método invoca una nueva búsqueda de attr
en self y superior.

• Los cambios lógicos se realizan mediante subclases, no cambiando superclases. Al redefinir


los nombres de las superclases en subclases más bajas en la jerarquía (árbol de clases), las
subclases reemplazan y, por lo tanto, personalizan el comportamiento heredado.

El efecto neto, y el objetivo principal de toda esta búsqueda, es que las clases admiten la factorización y
la personalización del código mejor que cualquier otra herramienta de lenguaje que hayamos visto hasta
ahora. Por un lado, nos permiten minimizar la redundancia de código (y, por lo tanto, reducir los costos
de mantenimiento) al factorizar las operaciones en una sola implementación compartida; por otro, nos
permiten programar personalizando lo que ya existe, en lugar de cambiarlo o empezar de cero.

Estrictamente hablando, la herencia de Python es un poco más rica que la descrita


aquí, cuando tenemos en cuenta descriptores y metaclases de nuevo estilo (temas
avanzados que estudiaremos más adelante), pero podemos restringir nuestro alcance
de forma segura a instancias y sus clases, tanto en este punto en el libro y en la mayoría
del código de aplicación de Python. Definiremos la herencia formalmente en el Capítulo 40.

Un segundo ejemplo

Para ilustrar el papel de la herencia, el siguiente ejemplo se basa en el anterior. Primero, definiremos una
nueva clase, SecondClass, que hereda todos los nombres de FirstClass y proporciona uno propio:

>>> class SecondClass(FirstClass): # Hereda setdata def


display(self): # Cambia display print('Valor actual
% self.data)
= "%s"'

802 | Capítulo 27: Conceptos básicos de codificación de clases


Machine Translated by Google

Figura 27-2. Especialización: anular nombres heredados redefiniéndolos en extensiones inferiores en el árbol de clases.
Aquí, SecondClass redefine y, por lo tanto, personaliza el método de "visualización" para sus instancias.

SecondClass define el método de visualización para imprimir con un formato diferente. Al definir
un atributo con el mismo nombre que un atributo en FirstClass, SecondClass reemplaza
efectivamente el atributo de visualización en su superclase.

Recuerde que las búsquedas de herencia avanzan hacia arriba desde las instancias hasta las
subclases y las superclases, deteniéndose en la primera aparición del nombre del atributo que
encuentra. En este caso, dado que el nombre de visualización en SecondClass se encontrará antes
que el de First Class, decimos que SecondClass anula la visualización de FirstClass. A veces
llamamos sobrecarga a este acto de reemplazar atributos al redefinirlos más abajo en el árbol.

El efecto neto aquí es que SecondClass especializa a FirstClass cambiando el comportamiento del
método de visualización . Por otro lado, SecondClass (y cualquier instancia creada a partir de ella)
aún hereda el método setdata en FirstClass textualmente . Hagamos una instancia para demostrar:

>>> z = Segunda Clase() >>>


z.setdata(42) >>> z.display() # Encuentra setdata en FirstClass
# Encuentra el método anulado en SecondClass
Valor actual = "42"

Como antes, creamos un objeto de instancia de SecondClass llamándolo. La llamada setdata aún
ejecuta la versión en FirstClass, pero esta vez el atributo de visualización proviene de Second Class
e imprime un mensaje personalizado. La figura 27-2 esboza los espacios de nombres involucrados.

Ahora, aquí hay una cosa crucial a tener en cuenta sobre OOP: la especialización introducida en
SecondClass es completamente externa a FirstClass. Es decir, no afecta a los objetos FirstClass
existentes o futuros , como la x del ejemplo anterior: >>> x.display()

# x sigue siendo una instancia de FirstClass (mensaje antiguo)


Nuevo valor

En lugar de cambiar FirstClass, lo personalizamos . Naturalmente, este es un ejemplo artificial, pero


como regla, debido a que la herencia nos permite hacer cambios como este en componentes
externos (es decir, en subclases), las clases a menudo admiten la extensión y la reutilización mejor
que las funciones o los módulos.

Las clases se personalizan por herencia | 803


Machine Translated by Google

Las clases son atributos en los módulos


Antes de continuar, recuerda que no hay nada mágico en el nombre de una clase. es solo
una variable asignada a un objeto cuando se ejecuta la declaración de clase , y el objeto puede ser
referenciado con cualquier expresión normal. Por ejemplo, si nuestra Primera Clase estuviera codificada en
un archivo de módulo en lugar de escribirse interactivamente, podríamos importarlo y usar su nombre
normalmente en una línea de encabezado de clase :

from modulename import FirstClass class # Copiar nombre en mi alcance


SecondClass(FirstClass): def display(self): ... # Usar el nombre de la clase directamente

O equivalente:

import modulename class # Accede a todo el módulo

SecondClass(modulename.FirstClass): # Calificar para hacer referencia


def mostrar(auto): ...

Como todo lo demás, los nombres de clase siempre viven dentro de un módulo, por lo que deben seguir todos
las reglas que estudiamos en la Parte V. Por ejemplo, se puede codificar más de una clase en un
archivo de módulo único: al igual que otras declaraciones en un módulo, las declaraciones de clase se ejecutan durante
imports para definir nombres, y estos nombres se convierten en atributos de módulo distintos. Más
en general, cada módulo puede mezclar arbitrariamente cualquier número de variables, funciones y
clases, y todos los nombres en un módulo se comportan de la misma manera. El archivo food.py demuestra:

# comida.py
variable = 1 # comida.var
def func(): ... clase # comida.func
spam: ... clase jamón: # comida.spam
clase huevos: ... ... #comida.jamon
# comida.huevos

Esto es cierto incluso si el módulo y la clase tienen el mismo nombre. Por ejemplo, dado el siguiente archivo, person.py:

persona de clase: ...

tenemos que pasar por el módulo para buscar la clase como de costumbre:

importar persona x # Módulo de importación


= persona.persona() # Clase dentro del módulo

Aunque esta ruta puede parecer redundante, es obligatoria: persona.persona se refiere a la persona
clase de hijo dentro del módulo de persona . Decir que solo la persona obtiene el módulo, no la clase,
a menos que se use la sentencia from :

de persona import persona x = persona() # Obtener clase del módulo


# Usar el nombre de la clase

Como con cualquier otra variable, nunca podemos ver una clase en un archivo sin antes importar y
de alguna manera buscándolo de su archivo adjunto. Si esto parece confuso, no use el mismo
nombre de un módulo y una clase dentro de él. De hecho, la convención común en Python dicta
que los nombres de las clases deben comenzar con una letra mayúscula , para ayudar a hacerlos más distintos:

804 | Capítulo 27: Conceptos básicos de codificación de clases


Machine Translated by Google

importar persona # Minúsculas para módulos


x = persona.Persona() # Mayúsculas para clases

Además, tenga en cuenta que aunque las clases y los módulos son espacios de nombres para adjuntar
atributos, corresponden a estructuras de código fuente muy diferentes: un módulo refleja un archivo completo,
pero una clase es una declaración dentro de un archivo. Hablaremos más sobre tales distinciones más
adelante en esta parte del libro.

Las clases pueden interceptar operadores de Python


Pasemos a la tercera y última gran diferencia entre clases y módulos: la sobrecarga de operadores. En
términos simples, la sobrecarga de operadores permite que los objetos codificados con clases intercepten y
respondan a operaciones que funcionan en tipos integrados: suma, división, impresión, calificación, etc. En su
mayoría, es solo un mecanismo de envío automático: las expresiones y otras operaciones integradas enrutan
el control a las implementaciones en las clases. Aquí tampoco hay nada similar en los módulos: los módulos
pueden implementar llamadas a funciones, pero no el comportamiento de las expresiones.

Aunque podríamos implementar todo el comportamiento de clase como funciones de método, la sobrecarga
de operadores permite que los objetos se integren más estrechamente con el modelo de objetos de Python.
Además, debido a que la sobrecarga de operadores hace que nuestros propios objetos actúen como
integrados, tiende a fomentar interfaces de objetos que son más consistentes y fáciles de aprender, y permite
que los objetos basados en clases sean procesados por código escrito para esperar un tipo integrado. interfaz.
Aquí hay un resumen rápido de las ideas principales detrás de la sobrecarga de operadores:

• Los métodos nombrados con guiones bajos dobles (__X__) son ganchos especiales. En las clases
de Python, implementamos la sobrecarga de operadores al proporcionar métodos con nombres
especiales para interceptar operaciones. El lenguaje Python define un mapeo fijo e inmutable de cada
una de estas operaciones a un método con nombre especial. • Dichos métodos se llaman

automáticamente cuando las instancias aparecen en operaciones integradas. Por ejemplo, si un objeto
de instancia hereda un método __add__ , se llama a ese método cada vez que el objeto aparece en una
expresión + . El valor de retorno del método se convierte en el resultado de la expresión correspondiente.

• Las clases pueden anular la mayoría de las operaciones de tipos integradas. Hay docenas de
nombres de métodos de sobrecarga de operadores especiales para interceptar e implementar casi todas
las operaciones disponibles para los tipos integrados. Esto incluye expresiones, pero también operaciones
básicas como impresión y creación de objetos.

• No hay valores predeterminados para los métodos de sobrecarga de operadores y no se requiere


ninguno. Si una clase no define o hereda un método de sobrecarga de operadores, simplemente
significa que la operación correspondiente no es compatible con las instancias de la clase.
Si no hay __add__, por ejemplo, las expresiones + generan excepciones.

• Las clases de estilo nuevo tienen algunos valores predeterminados, pero no para operaciones
comunes. En Python 3.X, y las llamadas clases de "nuevo estilo" en 2.X que definiremos más adelante, una raíz

clases pueden interceptar operadores de Python | 805


Machine Translated by Google

El objeto con nombre de clase proporciona valores predeterminados para algunos métodos __X__ ,
pero no para muchos, y no para las operaciones más utilizadas. • Los operadores permiten que las

clases se integren con el modelo de objetos de Python. Al sobrecargar las operaciones de tipo, los
objetos definidos por el usuario que implementamos con las clases pueden actuar como elementos
integrados y, por lo tanto, brindar consistencia y compatibilidad con las interfaces esperadas.

La sobrecarga de operadores es una función opcional; lo utilizan principalmente las personas que desarrollan
herramientas para otros programadores de Python, no los desarrolladores de aplicaciones. Y, sinceramente,
probablemente no debería usarlo solo porque parece inteligente o "genial". A menos que una clase necesite
imitar las interfaces de tipos integradas, por lo general debería ceñirse a métodos con nombres más simples.
¿Por qué una aplicación de base de datos de empleados admitiría expresiones como * y +, por ejemplo?
Los métodos con nombre como giveRaise y promover generalmente tendrían más sentido.

Debido a esto, no entraremos en detalles sobre cada método de sobrecarga de operadores disponible en
Python en este libro. Aún así, hay un método de sobrecarga de operadores que probablemente verá en casi
todas las clases realistas de Python: el método __init__ , que se conoce como el método constructor y se
usa para inicializar el estado de los objetos. Debe prestar especial atención a este método, porque __init__,
junto con el argumento self , resulta ser un requisito clave para leer y comprender la mayoría del código
OOP en Python.

Un tercer ejemplo

Pasemos a otro ejemplo. Esta vez, definiremos una subclase de la segunda clase de la sección anterior que
implementa tres atributos con nombres especiales que Python llamará automáticamente:

• __init__ se ejecuta cuando se crea un nuevo objeto de instancia: self es la nueva ThirdClass
objeto.1

• __add__ se ejecuta cuando aparece una instancia de ThirdClass en una expresión + . •

__str__ se ejecuta cuando se imprime un objeto (técnicamente, cuando se convierte en su cadena de


impresión mediante la función incorporada str o su equivalente interno de Python).

Nuestra nueva subclase también define un método con nombre normal llamado mul, que cambia el objeto
de la instancia en su lugar. Aquí está la nueva subclase:

>>> class ThirdClass(SecondClass): def # Heredar de SecondClass


__init__(self, valor): self.data = # En "Tercera Clase (valor)"
valor
def __add__(self, other): return # Sobre "yo + otro"
ThirdClass(self.data + other) def __str__(self):
return '[ThirdClass: %s]' % self.data # En "imprimir(auto)", "str()"

1. ¡No debe confundirse con los archivos __init__.py en los paquetes de módulos! El método aquí es una función constructora de
clase utilizada para inicializar la instancia recién creada, no un paquete de módulo. Consulte el Capítulo 24 para obtener más
detalles.

806 | Capítulo 27: Conceptos básicos de codificación de clases


Machine Translated by Google

def mul(self, otro): self.data # Cambio en el lugar: nombrado


*= otro

>>> a = Tercera Clase('abc') >>> # __init__ llamado


a.display() # Método heredado llamado
Valor actual = "abc"
>>> imprimir(a) # __str__: devuelve la cadena de visualización
[Tercera Clase: abc]

>>> b = a + 'xyz' >>> # __add__: crea una nueva instancia


b.mostrar() # b tiene todos los métodos de ThirdClass

Valor actual = "abcxyz"


>>> imprimir(b) # __str__: devuelve la cadena de visualización
[Tercera Clase: abcxyz]

>>> a.mul(3) >>> # mul: cambia la instancia en su lugar


imprimir(a)
[Tercera Clase: abcabcabc]

ThirdClass "es una" SecondClass, por lo que sus instancias heredan el método de visualización personalizado
de SecondClass de la sección anterior. Esta vez, sin embargo, las llamadas de creación de ThirdClass
pasar un argumento (por ejemplo, "abc"). Este argumento se pasa al argumento de valor en el

__init__ constructor y asignado a self.data allí. El efecto neto es que Tercera


La clase organiza para establecer el atributo de datos automáticamente en el momento de la construcción, en lugar de
requiriendo llamadas de setdata después del hecho.

Además, los objetos de ThirdClass ahora pueden aparecer en expresiones + y llamadas de impresión . para +,
Python pasa el objeto de instancia de la izquierda al argumento self en __add__ y el
valor a la derecha de otro, como se ilustra en la Figura 27-3; cualquier cosa que __add__ devuelva se convierte en el
resultado de la expresión + (más sobre su resultado en un momento).

Para imprimir, Python pasa el objeto que se está imprimiendo a sí mismo en __str__; cualquier cadena
este método devuelve se toma como la cadena de impresión para el objeto. Con __str__ (o su
gemelo __repr__ más ampliamente relevante , que conoceremos y usaremos en el próximo capítulo),
podemos usar una impresión normal para mostrar objetos de esta clase, en lugar de llamar a la especial

método de visualización .

Figura 27-3. En la sobrecarga de operadores, los operadores de expresión y otras operaciones integradas realizadas
en las instancias de clase se asignan de nuevo a métodos con nombres especiales en la clase. Estos métodos especiales
son opcionales y se pueden heredar como de costumbre. Aquí, una expresión + activa el método __add__.

clases pueden interceptar operadores de Python | 807


Machine Translated by Google

Los métodos con nombres especiales como __init__, __add__ y __str__ son heredados por subclases
e instancias, al igual que cualquier otro nombre asignado en una clase. Si no están codificados en una
clase, Python busca dichos nombres en todas sus superclases, como de costumbre. Los nombres de
los métodos de sobrecarga del operador tampoco son palabras incorporadas o reservadas; son solo
atributos que Python busca cuando los objetos aparecen en varios contextos. Por lo general, Python los
llama automáticamente, pero su código también puede llamarlos ocasionalmente. Por ejemplo, el método
__init__ a menudo se llama manualmente para activar los pasos de inicialización en una superclase,
como veremos en el próximo capítulo.

Devolviendo resultados,

o no . Algunos métodos de sobrecarga de operadores como __str__ requieren resultados, pero otros
son más flexibles. Por ejemplo, observe cómo el método __add__ crea y devuelve un nuevo objeto de
instancia de su clase llamando a ThirdClass con el valor del resultado, que a su vez activa __init__ para
inicializar el resultado. Esta es una convención común y explica por qué b en la lista tiene un método de
visualización ; también es un objeto de ThirdClass , porque eso es lo que + devuelve para los objetos de
esta clase. Esto esencialmente propaga el tipo.

Por el contrario, mul cambia el objeto de instancia actual en su lugar, reasignando el atributo self .
Podríamos sobrecargar la expresión * para hacer lo último, pero esto sería demasiado diferente del
comportamiento de * para tipos integrados como números y cadenas, para los que siempre crea nuevos
objetos. La práctica común dicta que los operadores sobrecargados deberían funcionar de la misma
manera que lo hacen las implementaciones de operadores integrados. Sin embargo, debido a que la
sobrecarga de operadores es solo un mecanismo de envío de expresión a método, puede interpretar los
operadores de la forma que desee en sus propios objetos de clase.

¿Por qué utilizar la sobrecarga de operadores?

Como diseñador de clases, puede optar por utilizar la sobrecarga de operadores o no. Su elección
simplemente depende de cuánto desea que su objeto se vea y se sienta como tipos incorporados.
Como se mencionó anteriormente, si omite un método de sobrecarga de operadores y no lo hereda de
una superclase, la operación correspondiente no se admitirá en sus instancias; si se intenta, se generará
una excepción (o, en algunos casos, como la impresión, se utilizará un valor predeterminado estándar).

Francamente, muchos métodos de sobrecarga de operadores tienden a usarse solo cuando se


implementan objetos que son de naturaleza matemática; una clase de vector o matriz puede sobrecargar
el operador de suma, por ejemplo, pero una clase de empleado probablemente no lo haría. Para clases
más simples, es posible que no use la sobrecarga en absoluto y, en cambio, confiaría en llamadas a
métodos explícitos para implementar el comportamiento de sus objetos.

Por otro lado, puede decidir usar la sobrecarga de operadores si necesita pasar un objeto definido por
el usuario a una función que se codificó para esperar que los operadores estén disponibles en un tipo
integrado como una lista o un diccionario. La implementación del mismo conjunto de operadores en su
clase garantizará que sus objetos admitan la misma interfaz de objeto esperada y, por lo tanto, sean
compatibles con la función. Aunque no cubriremos la sobrecarga de todos los operadores

808 | Capítulo 27: Conceptos básicos de codificación de clases


Machine Translated by Google

En este libro, examinaremos técnicas adicionales comunes de sobrecarga de operadores en acción en


el Capítulo 30.

Un método de sobrecarga que usaremos a menudo aquí es el método constructor __init__ , que se
usa para inicializar objetos de instancia recién creados y está presente en casi todas las clases
realistas. Debido a que permite que las clases completen los atributos en sus nuevas instancias de
inmediato, el constructor es útil para casi todos los tipos de clases que pueda codificar. De hecho,
aunque los atributos de instancia no se declaran en Python, por lo general puede averiguar qué
atributos tendrá una instancia inspeccionando el método __init__ de su clase.

Por supuesto, no hay nada de malo en experimentar con herramientas de lenguaje interesantes, pero
no siempre se traducen en código de producción. Con el tiempo y la experiencia, encontrará que estos
patrones y pautas de programación son naturales y casi automáticos.

La clase de Python más simple del mundo


Hemos comenzado a estudiar la sintaxis de las instrucciones de clase en detalle en este capítulo, pero nuevamente me gustaría recordarle

que el modelo de herencia básico que producen las clases es muy simple: todo lo que realmente implica es buscar atributos en árboles de

objetos vinculados. De hecho, podemos crear una clase sin nada en ella. La siguiente instrucción crea una clase sin atributos adjuntos, un

objeto de espacio de nombres vacío: >>> class rec: pass # Objeto de espacio de nombres vacío

Necesitamos la declaración de marcador de posición de paso sin operación (discutida en el Capítulo


13) aquí porque no tenemos ningún método para codificar. Después de crear la clase ejecutando esta
declaración de forma interactiva, podemos comenzar a adjuntar atributos a la clase asignándole
nombres completamente fuera de la declaración de clase original :
>>> rec.nombre = 'Bob' >>> # Solo objetos con atributos
rec.edad = 40

Y, después de haber creado estos atributos por asignación, podemos obtenerlos con la sintaxis habitual. Cuando se usa de esta manera, una

clase es más o menos similar a una "estructura" en C, o un "registro" en Pascal. Básicamente es un objeto con nombres de campo adjuntos

(como veremos más adelante, hacer lo mismo con las teclas del diccionario requiere caracteres adicionales): >>> print(rec.name)

# Como una estructura C o un registro


Beto

Tenga en cuenta que esto funciona aunque todavía no haya instancias de la clase; las clases son
objetos por derecho propio, incluso sin instancias. De hecho, son solo espacios de nombres autónomos;
siempre que tengamos una referencia a una clase, podemos establecer o cambiar sus atributos en
cualquier momento que deseemos. Sin embargo, observe lo que sucede cuando creamos dos instancias:
>>> x = rec() >>> y # Las instancias heredan los nombres de las clases

= rec()

La clase de Python más simple del mundo | 809


Machine Translated by Google

Estas instancias comienzan su vida como objetos de espacio de nombres completamente vacíos. Sin
embargo, debido a que recuerdan la clase a partir de la cual se crearon, obtendrán los atributos que
adjuntamos a la clase por herencia:

>>> x.nombre, y.nombre # nombre se almacena solo en la clase


('Bob', 'Bob')

Realmente, estas instancias no tienen atributos propios; simplemente obtienen el atributo de nombre del
objeto de clase donde está almacenado. Sin embargo, si asignamos un atributo a una instancia, crea (o
cambia) el atributo en ese objeto, y no otro; de manera crucial, las referencias de atributos inician las
búsquedas de herencia, pero las asignaciones de atributos afectan solo a los objetos en los que se realizan
las asignaciones . . Aquí, esto significa que x obtiene su propio nombre, pero y todavía hereda el nombre
adjunto a la clase que se encuentra arriba:
>>> x.nombre = 'Sue' # Pero la asignación cambia solo x
>>> rec.nombre, x.nombre, y.nombre
('Bob', 'Sue', 'Bob')

De hecho, como exploraremos con más detalle en el Capítulo 29, los atributos de un objeto de espacio de
nombres generalmente se implementan como diccionarios, y los árboles de herencia de clase son (en
términos generales) solo diccionarios con enlaces a otros diccionarios. Si sabe dónde buscar, puede ver
esto explícitamente.

Por ejemplo, el atributo __dict__ es el diccionario de espacio de nombres para la mayoría de los objetos
basados en clases. Algunas clases también pueden (o en su lugar) definir atributos en __slots__, una
función avanzada y poco utilizada que veremos en el Capítulo 28, pero que pospondremos en gran medida
hasta el Capítulo 31 y el Capítulo 32. Normalmente, __dict__ es literalmente el espacio de nombres de
atributos de una instancia.

Para ilustrar, lo siguiente se ejecutó en Python 3.3; el orden de los nombres y el conjunto de __X__ nombres
internos presentes pueden variar de una versión a otra, y filtramos los integrados con una expresión
generadora como lo hemos hecho antes, pero los nombres que asignamos están presentes en todos:

>>> list(rec.__dict__.keys()) ['edad',


'__module__', '__qualname__', '__weakref__', 'name', '__dict__', '__doc__']

>>> lista(nombre por nombre en rec.__dict__ si no nombre.comienza con('__')) ['edad',


'nombre'] >>> lista(x.__dict__.claves()) ['nombre'] >>> lista(y.__dict__.teclas()) []

# list() no requerido en Python 2.X

Aquí, el diccionario de espacio de nombres de la clase muestra los atributos de nombre y edad que le
asignamos, x tiene su propio nombre e y todavía está vacío. Debido a este modelo, un atributo a menudo
se puede obtener mediante indexación de diccionario o notación de atributos, pero solo si está presente en
el objeto en cuestión; la notación de atributos inicia la búsqueda de herencia, pero la indexación busca solo
en el objeto único (como veremos). ver más adelante, ambos tienen roles válidos): # Los atributos presentes

>>> x.nombre, x.__dict__['nombre'] aquí son claves de dictado


('Sue', 'Sue')

810 | Capítulo 27: Conceptos básicos de codificación de clases


Machine Translated by Google

>>> x.edad 40 # Pero la búsqueda de atributos también verifica las clases

>>> x.__dict__['edad'] # El dictado de indexación no hace herencia


KeyError: 'edad'

Para facilitar la búsqueda de herencia en las extracciones de atributos, cada instancia tiene un enlace a su clase que Python crea para nosotros; se

llama __class__, si desea inspeccionarla: >>> x.__class__ <class '__main__.rec'>

# Enlace de instancia a clase

Las clases también tienen un atributo __bases__ , que es una tupla de referencias a sus objetos de
superclase; en este ejemplo, solo la clase raíz del objeto implícito en Python 3.X que exploraremos más
adelante (obtendrá una tupla vacía en 2.X en su lugar ):

>>> rec.__bases__ (<clase # Enlace de clase a superclases, () en 2.X


'objeto'>,)

Estos dos atributos son cómo Python representa literalmente los árboles de clases en la memoria.
Los detalles internos como estos no son conocimientos necesarios (los árboles de clases están implícitos
en el código que ejecuta y su búsqueda normalmente es automática), pero a menudo pueden ayudar a
desmitificar el modelo.

El punto principal que se debe sacar de este aspecto oculto es que el modelo de clase de Python es
extremadamente dinámico. Las clases y las instancias son solo objetos de espacio de nombres, con
atributos creados sobre la marcha por asignación. Esas asignaciones generalmente ocurren dentro de las
declaraciones de clase que codifica, pero pueden ocurrir en cualquier lugar donde tenga una referencia a
uno de los objetos en el árbol.

Incluso los métodos, normalmente creados por una definición anidada en una clase, se pueden crear de
forma completamente independiente de cualquier objeto de clase. Lo siguiente, por ejemplo, define una
función simple fuera de cualquier clase que toma un argumento:

>>> def nombre superior(obj):


devolver obj.nombre.superior() # Todavía necesita un argumento propio (obj)

Aquí todavía no hay nada sobre una clase: es una función simple y se puede llamar como tal en este punto,
siempre que pasemos un objeto obj con un atributo de nombre , cuyo valor a su vez tiene un método
superior : nuestras instancias de clase suceden para ajustarse a la interfaz esperada e iniciar la conversión
de cadenas en mayúsculas:

>>> nombresuperior(x) # Llamar como una función simple


'DEMANDAR'

Sin embargo, si asignamos esta función simple a un atributo de nuestra clase, se convierte en un método,
al que se puede llamar a través de cualquier instancia, así como a través del nombre de la clase en sí,
siempre que pasemos una instancia manualmente, una técnica que aprovecharemos más. en el proximo
capitulo :2

>>> rec.método = nombresuperior # ¡Ahora es el método de una clase!

>>> x.método() # Ejecutar método para procesar x


'DEMANDAR'

La clase de Python más simple del mundo | 811


Machine Translated by Google

>>> y.método() # Lo mismo, pero pasándose y a sí mismo


'BETO'

>>> metodo rec.(x) # Puede llamar a través de instancia o clase


'DEMANDAR'

Normalmente, las clases se completan con declaraciones de clase y los atributos de instancia se crean
mediante asignaciones a atributos propios en funciones de método. Sin embargo, el punto nuevamente es
que no tienen que serlo; OOP en Python realmente se trata principalmente de buscar atributos en objetos
de espacio de nombres vinculados.

Registros revisados: clases versus diccionarios


Aunque las clases simples de la sección anterior están destinadas a ilustrar los conceptos básicos del
modelo de clase, las técnicas que emplean también se pueden usar para el trabajo real. Por ejemplo, el
Capítulo 8 y el Capítulo 9 mostraron cómo usar diccionarios, tuplas y listas para registrar propiedades de
entidades en nuestros programas, genéricamente llamados registros. Resulta que las clases a menudo
pueden desempeñar mejor esta función: empaquetan información como diccionarios, pero también pueden
empaquetar la lógica de procesamiento en forma de métodos. Como referencia, aquí hay un ejemplo de
registros basados en tuplas y diccionarios que usamos anteriormente en el libro (usando una de las
muchas técnicas de codificación de diccionarios): >>> rec = ('Bob', 40.5, ['dev', 'mgr' ]) >>> imprimir(rec[0])

# registro basado en tuplas

Beto

>>> rec = {} >>>


rec['nombre'] = 'Bob' >>> # Registro basado en diccionario
rec['edad'] = 40.5 >>> rec['trabajos'] # O {...}, dict(n=v), etc.
= ['desarrollador', 'director']
>>>

>>> imprimir(rec['nombre'])
Beto

Este código emula herramientas como registros en otros idiomas. Sin embargo, como acabamos de ver, también hay varias formas de hacer lo

mismo con las clases. Quizás el más simple es este: intercambiar claves por atributos: >>> class rec: pass

>>> rec.nombre = 'Bob' >>> # Registro basado en clases

rec.edad = 40.5 >>> rec.jobs


= ['dev', 'mgr']

2. De hecho, esta es una de las razones por las que el argumento propio siempre debe ser explícito en los métodos de Python, dado
que los métodos se pueden crear como funciones simples independientes de una clase, necesitan hacer explícito el argumento
de instancia implícito. Se pueden llamar como funciones o como métodos, y Python no puede adivinar ni asumir que una función
simple eventualmente podría convertirse en el método de una clase. Sin embargo, la razón principal del argumento self explícito
es hacer que los significados de los nombres sean más obvios: los nombres a los que no se hace referencia a través de self son
variables simples asignadas a ámbitos, mientras que los nombres a los que se hace referencia a través de self con notación de
atributos son obviamente atributos de instancia.

812 | Capítulo 27: Conceptos básicos de codificación de clases


Machine Translated by Google

>>>
>>> print(rec.nombre)
Beto

Este código tiene sustancialmente menos sintaxis que el equivalente del diccionario. Utiliza una
declaración de clase vacía para generar un objeto de espacio de nombres vacío. Una vez que creamos
la clase vacía, la llenamos asignando atributos de clase a lo largo del tiempo, como antes.

Esto funciona, pero se requerirá una nueva declaración de clase para cada registro distinto que
necesitaremos. Quizás más típicamente, podemos generar instancias de una clase vacía para
representar cada entidad distinta:
>>> clase rec: pasar

>>> pers1 = rec() >>> # Registros basados en instancias

pers1.nombre = 'Bob' >>>


pers1.jobs = ['dev', 'mgr'] >>>
pers1.edad = 40.5 >>>

>>> pers2 = rec() >>>


pers2.nombre = 'Sue' >>>
pers2.jobs = ['dev', 'cto']
>>>
>>> pers1.nombre, pers2.nombre
('Bob', 'Sue')

Aquí, hacemos dos registros de la misma clase. Las instancias comienzan su vida vacías, al igual que
las clases. Luego completamos los registros asignándolos a atributos. Esta vez, sin embargo, hay dos
objetos separados y, por lo tanto, dos atributos de nombre separados. De hecho, las instancias de la
misma clase ni siquiera tienen que tener el mismo conjunto de nombres de atributos; en este ejemplo,
uno tiene un nombre de edad único . Las instancias son realmente espacios de nombres distintos, por
lo que cada uno tiene un diccionario de atributos distinto. Aunque normalmente los métodos de una
clase los completan de manera consistente, son más flexibles de lo que cabría esperar.

Finalmente, podríamos codificar una clase más completa para implementar el registro y su procesamiento,
algo que los diccionarios orientados a datos no admiten directamente:
>>> clase Persona:
def __init__(self, nombre, trabajos, edad=Ninguno): # clase = datos + lógica
self.name = nombre self.jobs = trabajos
self.edad = edad def info(self): return
(self.name, self.jobs )

>>> rec1 = Persona('Bob', ['dev', 'mgr'], 40.5) >>> rec2 = # Llamadas de construcción

Persona('Sue', ['dev', 'cto'])


>>>
>>> rec1.jobs, rec2.info() (['dev', # Atributos + métodos
'mgr'], ('Sue', ['dev', 'cto']))

Este esquema también crea instancias múltiples, pero la clase no está vacía esta vez: hemos agregado
lógica (métodos) para inicializar instancias en el momento de la construcción y recopilar atributos

La clase de Python más simple del mundo | 813


Machine Translated by Google

en una tupla a petición. El constructor impone cierta consistencia en las instancias aquí al establecer siempre los
atributos de nombre, trabajo y edad , aunque este último se puede omitir cuando se crea un objeto. Juntos, los
métodos de la clase y los atributos de instancia crean un paquete que combina datos y lógica.

Podríamos ampliar aún más este código agregando lógica para calcular salarios, analizar nombres, etc. En última
instancia, podríamos vincular la clase a una jerarquía más grande para heredar y personalizar un conjunto
existente de métodos a través de la búsqueda automática de atributos de clases, o tal vez incluso almacenar
instancias de la clase en un archivo con decapado de objetos de Python para hacerlos persistentes. De hecho, lo
haremos: en el próximo capítulo, ampliaremos esta analogía entre clases y registros con un ejemplo de ejecución
más realista que demuestra los conceptos básicos de clase en acción.

Para ser justos con otras herramientas, en esta forma, las dos llamadas de construcción de clases anteriores se
asemejan más a los diccionarios hechos todos a la vez, pero aún parecen menos desordenados y brindan
métodos de procesamiento adicionales. De hecho, las llamadas a la construcción de la clase se asemejan más a
las tuplas con nombre del capítulo 9, lo cual tiene sentido, dado que las tuplas con nombre en realidad son clases
con lógica adicional para asignar atributos a compensaciones de tupla:

>>> rec = dict(nombre='Bob', edad=40.5, trabajos=['desarrollador', 'director']) # Diccionarios

>>> rec = {'nombre': 'Bob', 'edad': 40.5, 'trabajos': ['desarrollador', 'director']}

>>> rec = Rec('Bob', 40.5, ['desarrollador', 'director']) # tuplas con nombre

Al final, aunque los tipos como los diccionarios y las tuplas son flexibles, las clases nos permiten agregar
comportamiento a los objetos en formas que los tipos integrados y las funciones simples no admiten directamente.
Aunque también podemos almacenar funciones en diccionarios, usarlas para procesar instancias implícitas no es
tan natural y estructurado como lo es en las clases. Para ver esto más claramente, avancemos al siguiente
capítulo.

Resumen del capítulo


Este capítulo introdujo los conceptos básicos de la codificación de clases en Python. Estudiamos la sintaxis de la
declaración de clase y vimos cómo usarla para construir un árbol de herencia de clase.
También estudiamos cómo Python completa automáticamente el primer argumento en las funciones de método,
cómo los atributos se adjuntan a los objetos en un árbol de clases mediante una asignación simple y cómo los
métodos de sobrecarga de operadores con nombres especiales interceptan e implementan operaciones
integradas para nuestras instancias (por ejemplo, expresiones e imprenta).

Ahora que hemos aprendido todo sobre la mecánica de las clases de codificación en Python, el siguiente capítulo
se convierte en un ejemplo más grande y realista que une gran parte de lo que hemos aprendido sobre
programación orientada a objetos hasta ahora y presenta algunos temas nuevos. Después de eso, continuaremos
con nuestra mirada a la codificación de clases, dando un segundo paso sobre el modelo para completar algunos
de los detalles que se omitieron aquí para simplificar las cosas. Primero, sin embargo, hagamos un cuestionario
para repasar los conceptos básicos que hemos cubierto hasta ahora.

814 | Capítulo 27: Conceptos básicos de codificación de clases


Machine Translated by Google

Pon a prueba tus conocimientos: Cuestionario

1. ¿Cómo se relacionan las clases con los módulos?

2. ¿Cómo se crean las instancias y las clases?

3. ¿Dónde y cómo se crean los atributos de clase?

4. ¿Dónde y cómo se crean los atributos de instancia?

5. ¿Qué significa self en una clase de Python?

6. ¿Cómo se codifica la sobrecarga de operadores en una clase de Python?

7. ¿Cuándo podría querer admitir la sobrecarga de operadores en sus clases?

8. ¿Qué método de sobrecarga de operadores se usa más comúnmente?

9. ¿Cuáles son los dos conceptos clave necesarios para comprender el código Python OOP?

Pon a prueba tus conocimientos: respuestas

1. Las clases siempre están anidadas dentro de un módulo; son atributos de un objeto de módulo.
Tanto las clases como los módulos son espacios de nombres, pero las clases corresponden a declaraciones
(no archivos completos) y admiten las nociones de programación orientada a objetos de instancias múltiples,
herencia y sobrecarga de operadores (los módulos no). En cierto sentido, un módulo es como una clase de
instancia única, sin herencia, que corresponde a un archivo completo de código.

2. Las clases se crean ejecutando sentencias de clase ; las instancias se crean llamando a una clase como si
fuera una función.

3. Los atributos de clase se crean asignando atributos a un objeto de clase. Normalmente se generan mediante
asignaciones de nivel superior anidadas en una declaración de clase : cada nombre asignado en el bloque de
declaración de clase se convierte en un atributo del objeto de clase (técnicamente, el alcance local de la
declaración de clase se transforma en el espacio de nombres de atributo del objeto de clase, de forma muy
similar a un módulo). Sin embargo, los atributos de clase también se pueden crear asignando atributos a la
clase en cualquier lugar donde exista una referencia al objeto de clase, incluso fuera de la declaración de
clase .

4. Los atributos de instancia se crean asignando atributos a un objeto de instancia. Normalmente se crean dentro
de las funciones de método de una clase codificadas dentro de la declaración de clase , asignando atributos
al argumento self (que siempre es la instancia implícita). De nuevo, sin embargo, pueden crearse mediante
asignación en cualquier lugar donde aparezca una referencia a la instancia, incluso fuera de la declaración de
clase . Normalmente, todos los atributos de instancia se inicializan en el método constructor __init__ ; de esa
forma, las llamadas de métodos posteriores pueden suponer que los atributos ya existen.

5. self es el nombre comúnmente dado al primer argumento (más a la izquierda) en la función de método de una
clase; Python lo completa automáticamente con el objeto de instancia que es el sujeto implícito de la llamada
al método. Este argumento no necesita llamarse self (aunque esta es una convención muy fuerte); su posición
es lo significativo. (Los programadores de C++ o Java podrían preferir llamarlo así porque en esos lenguajes

Pon a prueba tus conocimientos: respuestas | 815


Machine Translated by Google

ese nombre refleja la misma idea; en Python, sin embargo, este argumento siempre debe ser
explícito).
6. La sobrecarga de operadores está codificada en una clase de Python con métodos con nombres
especiales; todos comienzan y terminan con guiones bajos dobles para hacerlos únicos. Estos no
son nombres incorporados o reservados; Python simplemente los ejecuta automáticamente
cuando aparece una instancia en la operación correspondiente. Python mismo define las
asignaciones de operaciones a nombres de métodos especiales.
7. La sobrecarga de operadores es útil para implementar objetos que se asemejan a tipos integrados
(p. ej., secuencias u objetos numéricos como matrices) y para imitar la interfaz de tipo integrado
que espera una pieza de código. Imitar las interfaces de tipo incorporadas le permite pasar
instancias de clase que también tienen información de estado (es decir, atributos que recuerdan
datos entre llamadas de operación). Sin embargo, no debe usar la sobrecarga del operador
cuando un método con nombre simple será suficiente.
8. El método constructor __init__ es el más utilizado; casi todas las clases usan este método para
establecer valores iniciales para atributos de instancia y realizar otras tareas de inicio.

9. El autoargumento especial en funciones de método y el método constructor __init__ son las dos
piedras angulares del código OOP en Python; si los obtiene, debería poder leer el texto de la
mayoría de los códigos OOP Python; aparte de estos, son en gran medida solo paquetes de
funciones. La búsqueda de herencia también importa, por supuesto, pero self representa el
argumento de objeto automático, y __init__ está muy extendido.

816 | Capítulo 27: Conceptos básicos de codificación de clases


Machine Translated by Google

CAPÍTULO 28

Un ejemplo más realista

Profundizaremos en más detalles de sintaxis de clase en el próximo capítulo. Sin embargo, antes de hacerlo,
me gustaría mostrarles un ejemplo más realista de clases en acción que es más práctico que lo que hemos
visto hasta ahora. En este capítulo, vamos a construir un conjunto de clases que hacen algo más concreto:
registrar y procesar información sobre personas. Como verá, lo que llamamos instancias y clases en la
programación de Python a menudo pueden cumplir las mismas funciones que los registros y programas en
términos más tradicionales.

Específicamente, en este capítulo vamos a codificar dos clases:

• Persona: una clase que crea y procesa información sobre personas • Gerente: una

personalización de Persona que modifica el comportamiento heredado

En el camino, crearemos instancias de ambas clases y probaremos su funcionalidad.


Cuando hayamos terminado, le mostraré un buen ejemplo de caso de uso para las clases: almacenaremos
nuestras instancias en una base de datos orientada a objetos archivada, para que sean permanentes. De esa
manera, puede usar este código como plantilla para desarrollar una base de datos personal completa escrita
completamente en Python.

Sin embargo, además de la utilidad real, nuestro objetivo aquí también es educativo: este capítulo proporciona
un tutorial sobre programación orientada a objetos en Python. A menudo, las personas captan la sintaxis de
clase del último capítulo en papel, pero tienen problemas para ver cómo empezar cuando se enfrentan a tener
que codificar una nueva clase desde cero. Con este fin, lo daremos paso a paso aquí, para ayudarlo a
aprender los conceptos básicos; construiremos las clases gradualmente, para que pueda ver cómo sus
características se unen en programas completos.

Al final, nuestras clases seguirán siendo relativamente pequeñas en términos de código, pero demostrarán
todas las ideas principales del modelo OOP de Python. A pesar de los detalles de su sintaxis, el sistema de
clases de Python es en gran medida solo una cuestión de buscar un atributo en un árbol de objetos, junto con
un primer argumento especial para las funciones.

817
Machine Translated by Google

Paso 1: Creación de instancias


Bien, hasta aquí la fase de diseño, pasemos a la implementación. Nuestra primera tarea es comenzar a
codificar la clase principal, Persona. En su editor de texto favorito, abra un nuevo archivo para el código que
escribiremos. Es una convención bastante fuerte en Python comenzar los nombres de los módulos con una
letra minúscula y los nombres de las clases con una letra mayúscula; como el nombre de los argumentos
propios en los métodos, esto no es requerido por el lenguaje, pero es tan común que desviarse puede ser
confuso para las personas que luego leen su código. Para cumplir, llamaremos a nuestro nuevo archivo de
módulo person.py y nuestra clase dentro de él Person, así:

# Archivo persona.py (inicio)

Persona de clase: # Iniciar una clase

Todo nuestro trabajo se realizará en este archivo hasta más adelante en este capítulo. Podemos codificar
cualquier cantidad de funciones y clases en un solo archivo de módulo en Python, y el nombre person.py de
este podría no tener mucho sentido si le agregamos componentes no relacionados más adelante. Por ahora,
asumiremos que todo en él estará relacionado con la Persona. Probablemente debería ser de todos modos,
como hemos aprendido, los módulos tienden a funcionar mejor cuando tienen un propósito único y cohesivo .

Codificación de constructores

Ahora, lo primero que queremos hacer con nuestra clase Person es registrar información básica sobre las
personas, para completar campos de registro, por así decirlo. Por supuesto, estos se conocen como atributos
de objeto de instancia en Python-speak, y generalmente se crean mediante la asignación de atributos propios
en las funciones de método de una clase. La forma normal de dar a los atributos de instancia sus primeros
valores es asignárselos a sí mismos en el método constructor __init__ , que contiene código que Python
ejecuta automáticamente cada vez que se crea una instancia. Agreguemos uno a nuestra clase:

# Agregar inicialización de campo de registro

clase Persona: def


__init__(self, nombre, trabajo, pago): self.name = # Constructor toma tres argumentos
nombre self.job = trabajo self.pay = pago # Rellenar los campos cuando se
crean # self es el nuevo objeto de instancia

Este es un patrón de codificación muy común: pasamos los datos que se adjuntarán a una instancia como
argumentos al método constructor y los asignamos a self para retenerlos de forma permanente. En términos
de OO, self es el objeto de instancia recién creado, y el nombre, el trabajo y el pago se convierten en
información de estado : datos descriptivos guardados en un objeto para su uso posterior. Aunque otras
técnicas (como incluir cierres de referencia de alcance) también pueden guardar detalles, los atributos de
instancia hacen que esto sea muy explícito y fácil de entender.

Observe que los nombres de los argumentos aparecen dos veces aquí. Este código puede incluso parecer un
poco redundante al principio, pero no lo es. El argumento del trabajo , por ejemplo, es una variable local en el
ámbito de la función __init__ , pero self.job es un atributo de la instancia que es el

818 | Capítulo 28: Un ejemplo más realista


Machine Translated by Google

sujeto implícito de la llamada al método. Son dos variables diferentes, que casualmente tienen el mismo nombre. Al asignar
el trabajo local al atributo self.job con self.job=job, guardamos el trabajo pasado en la instancia para su uso posterior.
Como es habitual en Python, dónde se asigna un nombre o a qué objeto se asigna determina lo que significa.

Hablando de argumentos, realmente no hay nada mágico en __init__, aparte del hecho de que se llama automáticamente
cuando se crea una instancia y tiene un primer argumento especial. A pesar de su extraño nombre, es una función normal
y admite todas las características de las funciones que ya hemos cubierto. Podemos, por ejemplo, proporcionar valores
predeterminados para algunos de sus argumentos, por lo que no es necesario proporcionarlos en los casos en que sus
valores no estén disponibles o no sean útiles.

Para demostrarlo, hagamos que el argumento del trabajo sea opcional: por defecto será Ninguno, lo que significa que la
persona que se está creando no está (actualmente) empleada. Si el valor predeterminado del trabajo es Ninguno,
probablemente también querremos que el pago predeterminado sea 0, por coherencia (¡a menos que algunas de las
personas que conoce logren recibir su pago sin tener un trabajo!). De hecho, tenemos que especificar un valor
predeterminado para el pago porque, de acuerdo con las reglas de sintaxis de Python y el Capítulo 18, todos los
argumentos en el encabezado de una función después del primer valor predeterminado también deben tener valores predeterminados:

# Agregar valores predeterminados para los argumentos del constructor

Persona de clase:
def __init__(self, nombre, trabajo=Ninguno, pago=0): # Argumentos de funciones normales
self.nombre = nombre
self.job = trabajo
self.pay = pago

Lo que significa este código es que necesitaremos pasar un nombre al crear Personas, pero el trabajo y el pago ahora son
opcionales; se establecerán de forma predeterminada en Ninguno y 0 si se omiten. El argumento self , como de costumbre,
lo completa Python automáticamente para hacer referencia al objeto de la instancia: la asignación de valores a los atributos
de self los vincula a la nueva instancia.

Pruebas sobre la marcha

Esta clase no hace mucho todavía, esencialmente solo llena los campos de un nuevo registro, pero es una verdadera
clase de trabajo. En este punto, podríamos agregarle más código para obtener más funciones, pero aún no lo haremos.
Como probablemente ya haya comenzado a apreciar, la programación en Python es realmente una cuestión de creación
de prototipos incrementales: escribe un código, lo prueba, escribe más código, vuelve a probar, etc. Debido a que Python
proporciona tanto una sesión interactiva como una respuesta casi inmediata después de los cambios en el código, es más
natural probar sobre la marcha que escribir una gran cantidad de código para probar todo a la vez.

Antes de agregar más funciones, probemos lo que tenemos hasta ahora creando algunas instancias de nuestra clase y
mostrando sus atributos tal como los creó el constructor. Podríamos hacer esto de forma interactiva, pero como
probablemente ya haya adivinado, las pruebas interactivas tienen sus límites: se vuelve tedioso tener que volver a importar
módulos y volver a escribir casos de prueba cada vez que inicia una nueva sesión de prueba. Más comúnmente, los
programadores de Python usan

Paso 1: Creación de instancias | 819


Machine Translated by Google

el indicador interactivo para pruebas únicas simples, pero realice pruebas más sustanciales escribiendo código en la parte
inferior del archivo que contiene los objetos que se van a probar, como este:

# Añadir código de autodiagnóstico incremental

Persona de clase:
def __init__(self, nombre, trabajo=Ninguno, pago=0):
self.name = nombre self.job = trabajo self.pay = pago

bob = Persona('Bob Smith') # Probar la clase


sue = Person('Sue Jones', job='dev', pay=100000) # Ejecuta __init__ automáticamente print(bob.name,
bob.pay) print(sue.name, sue.pay) # Obtener atributos adjuntos #
Los atributos de sue y bob difieren

Observe aquí que el objeto bob acepta los valores predeterminados para trabajo y pago, pero sue proporciona valores
explícitamente. También tenga en cuenta cómo usamos argumentos de palabras clave al hacer sue; en su lugar, podríamos
pasar por posición, pero las palabras clave pueden ayudarnos a recordar más tarde cuáles son los datos, y nos permiten pasar
los argumentos en el orden de izquierda a derecha que queramos. Nuevamente, a pesar de su nombre inusual, __init__ es
una función normal que admite todo lo que ya sabe sobre las funciones, incluidos los argumentos de palabras clave
predeterminados y de paso por nombre.
mentos.

Cuando este archivo se ejecuta como un script, el código de prueba en la parte inferior crea dos instancias de nuestra clase e
imprime dos atributos de cada uno (nombre y pago):

C:\código> persona.py
Bob Smith 0 Sue Jones
100000

También puede escribir el código de prueba de este archivo en el indicador interactivo de Python (suponiendo que primero
importe la clase Persona allí), pero codificar pruebas enlatadas dentro del archivo del módulo de esta manera hace que sea
mucho más fácil volver a ejecutarlas en el futuro.

Aunque este es un código bastante simple, ya está demostrando algo importante.


Observe que el nombre de bob no es el de sue, y la paga de sue no es la de bob. Cada uno es un registro independiente de
información. Técnicamente, bob y sue son objetos de espacio de nombres; como todas las instancias de clase, cada una tiene
su propia copia independiente de la información de estado creada por la clase. Debido a que cada instancia de una clase tiene
su propio conjunto de atributos propios , las clases son naturales para registrar información para múltiples objetos de esta
manera; Al igual que los tipos incorporados, como las listas y los diccionarios, las clases sirven como una especie de fábrica
de objetos.

Otras estructuras de programas de Python, como funciones y módulos, no tienen ese concepto. Las funciones de cierre del
Capítulo 17 se acercan en términos de estado por llamada, pero no tienen los métodos múltiples, la herencia y la estructura
más grande que obtenemos de las clases.

Usar código de dos maneras

Tal como está, el código de prueba en la parte inferior del archivo funciona, pero hay un gran inconveniente: sus declaraciones
de impresión de nivel superior se ejecutan tanto cuando el archivo se ejecuta como un script como cuando se importa como un

820 | Capítulo 28: Un ejemplo más realista


Machine Translated by Google

módulo. Esto significa que si alguna vez decidimos importar la clase en este archivo para usarla en otro lugar (y lo
haremos pronto en este capítulo), veremos la salida de su código de prueba cada vez que se importe el archivo.
Sin embargo, eso no es muy buena ciudadanía de software: a los programas cliente probablemente no les importen
nuestras pruebas internas y no querrán ver nuestra salida mezclada con la suya.

Aunque podríamos dividir el código de prueba en un archivo separado, a menudo es más conveniente codificar las
pruebas en el mismo archivo que los elementos que se van a probar. Sería mejor hacer arreglos para ejecutar las
declaraciones de prueba en la parte inferior solo cuando el archivo se ejecuta para la prueba, no cuando se importa
el archivo. Eso es exactamente para lo que está diseñada la verificación del módulo __name__ , como aprendió
en la parte anterior de este libro. Así es como se ve esta adición: agregue la prueba requerida y sangre su código
de autoevaluación:

# Permitir que este archivo sea importado así como ejecutado/ probado

Persona de clase:

def __init__(self, nombre, trabajo=Ninguno, pago=0): self.name =


nombre self.job = trabajo self.pay = pago

if __name__ == '__main__': # código de # Cuando se ejecuta solo para pruebas


autodiagnóstico

bob = Persona('Bob Smith') sue =


Persona('Sue Jones', job='dev', pay=100000) print(bob.name, bob.pay)
print(sue.name, sue.pay)

Ahora, obtenemos exactamente el comportamiento que buscamos: ejecutar el archivo como un script de nivel
superior lo prueba porque su __name__ es __main__, pero importarlo como una biblioteca de clases más tarde no

lo hace: C:\code> person.py Bob Smith 0 Sue Jones 100000

C:\code> python Python


3.3.0 (v3.3.0:bd8afb90ebf2, 29 de septiembre de 2012, 10:57:17) ... >>> importar persona

>>>

Cuando se importa, el archivo ahora define la clase, pero no la usa. Cuando se ejecuta directamente, este archivo
crea dos instancias de nuestra clase como antes e imprime dos atributos de cada uno; nuevamente, debido a que
cada instancia es un objeto de espacio de nombres independiente, los valores de sus atributos difieren.

Portabilidad de versiones:

impresiones Todo el código de este capítulo funciona tanto en Python 2.X como en 3.X, pero lo
estoy ejecutando en Python 3.X, y algunas de sus salidas usan llamadas de función de impresión
3.X con múltiples argumentos. . Como se explicó en el Capítulo 11, esto significa que algunas de
sus salidas pueden variar ligeramente en Python 2.X. Si ejecuta bajo 2.X, el código funcionará tal cual, pero notará

Paso 1: Creación de instancias | 821


Machine Translated by Google

paréntesis alrededor de algunas líneas de salida porque los paréntesis adicionales en una impresión convierten varios
elementos en una tupla solo en 2.X:

C:\código> c:\python27\python persona.py ('Bob


Smith', 0)
('Sue Jones', 100000)

Si esta diferencia es el tipo de detalle que podría mantenerlo despierto por las noches, simplemente elimine los
paréntesis para usar declaraciones de impresión 2.X , o agregue una importación de la función de impresión de Python
3.X en la parte superior de su secuencia de comandos, como se muestra en el Capítulo 11 (agregaría esto en todas
partes aquí, pero distrae un poco):

de __futuro__ importar print_function

También puede evitar los paréntesis adicionales de forma portátil mediante el uso de formato para generar un solo
objeto para imprimir. Cualquiera de los siguientes funciona tanto en 2.X como en 3.X, aunque la forma del método es
más nueva:

print('{0} {1}'.format(bob.nombre, bob.pago)) # Método de formato print('%s %s' %


(bob.nombre, bob.pago)) # Expresión de formato

Como también se describe en el Capítulo 11, dicho formato puede ser necesario en algunos casos, porque los
objetos anidados en una tupla pueden imprimirse de manera diferente a los impresos como objetos de nivel superior:
el primero se imprime con __repr__ y el último con __str__ (métodos de sobrecarga del operador). discutido más
adelante en este capítulo, así como en el Capítulo 30).

Para evitar este problema, los códigos de esta edición se muestran con __repr__ (el respaldo en todos los casos,
incluido el anidamiento y el mensaje interactivo) en lugar de __str__ (el valor predeterminado para las impresiones)
para que todas las apariencias de los objetos se impriman de la misma manera en 3.X y 2.X. , ¡incluso aquellos entre
paréntesis de tuplas superfluas!

Paso 2: agregar métodos de comportamiento Todo se

ve bien hasta ahora; en este punto, nuestra clase es esencialmente una fábrica de discos; crea y completa
campos de registros (atributos de instancias, en términos más pitónicos).
Sin embargo, a pesar de lo limitado que es, aún podemos ejecutar algunas operaciones en sus objetos. Si
bien las clases agregan una capa adicional de estructura, en última instancia hacen la mayor parte de su
trabajo incorporando y procesando tipos básicos de datos básicos como listas y cadenas. En otras palabras,
si ya sabe cómo usar los tipos básicos simples de Python, ya conoce gran parte de la historia de la clase de
Python; las clases son en realidad solo una extensión estructural menor.

Por ejemplo, el campo de nombre de nuestros objetos es una cadena simple, por lo que podemos extraer los
apellidos de nuestros objetos dividiéndolos en espacios e indexándolos. Todas estas son operaciones de tipos
de datos centrales, que funcionan ya sea que sus sujetos estén incrustados en instancias de clase o no: #
>>> nombre = 'Bob Smith' Cadena simple, fuera de clase # Extraer apellido
>>> nombre.split()
['Bob', 'Smith'] >>>
nombre.split()[-1] # O [1], si siempre solo dos partes
'Herrero'

822 | Capítulo 28: Un ejemplo más realista


Machine Translated by Google

De manera similar, podemos dar a un objeto un aumento de sueldo actualizando su campo de pago , es decir, cambiando
su información de estado en su lugar con una asignación. Esta tarea también implica operaciones básicas que funcionan
en los objetos principales de Python, independientemente de si son independientes o no.
incrustado en una estructura de clase (estoy formateando el resultado a continuación para enmascarar el
hecho de que diferentes pitones imprimen un número diferente de dígitos decimales):

>>> pago = 100000 *= # Variable simple, fuera de clase


1.10 >>>
pago >>> print('%.2f' # Dar un aumento del 10%

% pago) # O: pay = pay * 1.10, si te gusta escribir


110000.00 # O: paga = paga + (paga * .10), ¡si _realmente_ lo haces!

Para aplicar estas operaciones a los objetos Person creados por nuestro script, simplemente haga lo siguiente
bob.name y sue.pay lo que acabamos de hacer para nombrar y pagar. Las operaciones son las mismas,
pero los sujetos se adjuntan como atributos a los objetos creados a partir de nuestra clase:

# Procesar tipos incorporados incrustados: cadenas, mutabilidad

Persona de clase:
def __init__(self, nombre, trabajo=Ninguno, pago=0):
self.nombre = nombre
self.trabajo = trabajo
self.pay = pagar

si __nombre__ == '__principal__':
bob = Persona('Bob Smith')
sue = Persona('Sue Jones', job='dev', pay=100000)
imprimir(bob.nombre, bob.pago)
print(sue.nombre, sue.pago)
print(bob.nombre.split()[-1]) *= 1.10 # Extraer el apellido del objeto
sue.pay) sue.pay print('%.2f' % # Dale un aumento a este objeto

Hemos agregado las últimas tres líneas aquí; cuando se ejecutan, extraemos el apellido de bob por
usando operaciones básicas de cadena y lista en su campo de nombre, y darle a sue un aumento de sueldo por
modificando su atributo de pago en su lugar con operaciones numéricas básicas. En cierto sentido, Sue es
también es un objeto mutable : su estado cambia en su lugar como una lista después de una llamada de adición .
Aquí está la salida de la nueva versión:

Bob Smith 0
sue jones 100000
Herrero
110000.00

El código anterior funciona según lo planeado, pero si se lo muestra a un desarrollador de software veterano
él o ella probablemente le dirá que su enfoque general no es una gran idea en la práctica.
Las operaciones de codificación como estas fuera de la clase pueden provocar problemas de mantenimiento .
en el futuro.

Por ejemplo, ¿qué sucede si ha codificado la fórmula de extracción del apellido en muchos lugares diferentes de su
programa? Si alguna vez necesita cambiar la forma en que funciona (para admitir
una nueva estructura de nombre, por ejemplo), deberá buscar y actualizar cada ocurrencia. De manera similar, si el
código de aumento de sueldo alguna vez cambia (por ejemplo, para requerir aprobación o da

Paso 2: Adición de métodos de comportamiento | 823


Machine Translated by Google

actualizaciones de tabase), es posible que tenga varias copias para modificar. El simple hecho de
encontrar todas las apariencias de dicho código puede ser problemático en programas más grandes:
pueden estar dispersos en muchos archivos, divididos en pasos individuales, etc. En un prototipo como
este, el cambio frecuente está casi garantizado.

Métodos de

codificación Lo que realmente queremos hacer aquí es emplear un concepto de diseño de software
conocido como encapsulación : resumir la lógica de operación detrás de las interfaces, de modo que cada
operación se codifique solo una vez en nuestro programa. De esa forma, si nuestras necesidades
cambian en el futuro, solo hay una copia para actualizar. Además, somos libres de cambiar las partes
internas de la copia única casi arbitrariamente, sin romper el código que la usa.

En términos de Python, queremos codificar operaciones en objetos en los métodos de una clase, en lugar
de esparcirlos por todo nuestro programa. De hecho, esta es una de las cosas en las que las clases son
muy buenas : factorizar el código para eliminar la redundancia y, por lo tanto, optimizar la capacidad de
mantenimiento. Como beneficio adicional, convertir las operaciones en métodos les permite aplicarse a
cualquier instancia de la clase, no solo a aquellas que han sido codificadas para procesar.

Todo esto es más simple en código de lo que puede parecer en teoría. Lo siguiente logra la encapsulación
moviendo las dos operaciones del código fuera de la clase a los métodos dentro de la clase. Mientras
estamos en eso, cambiemos nuestro código de autoevaluación en la parte inferior para usar los nuevos
métodos que estamos creando, en lugar de operaciones de codificación:
# Agregue métodos para encapsular operaciones para mantener

Persona de clase:
def __init__(self, nombre, trabajo=Ninguno, pago=0):
self.name = nombre self.job = trabajo self.pay = pago
def lastName(self): return self.name.split()[-1]

# Métodos de
comportamiento # self es sujeto implícito
def giveRaise(self, percent): self.pay =
int(self.pay * (1 + percent)) # Debe cambiar solo aquí

si __nombre__ == '__principal__':
bob = Persona('Bob Smith') sue =
Persona('Sue Jones', job='dev', pay=100000) print(bob.name,
bob.pay) print(sue.name, sue.pay) print( bob.apellido(),
sue.apellido()) sue.giveRaise(.10) print(sue.pay)
# Use los nuevos métodos
# en lugar de codificar

Como hemos aprendido, los métodos son simplemente funciones normales que se adjuntan a las clases
y están diseñadas para procesar instancias de esas clases. La instancia es el sujeto de la llamada al
método y se pasa automáticamente al argumento propio del método.

La transformación a los métodos en esta versión es sencilla. El nuevo método del apellido , por ejemplo,
simplemente hace para sí mismo lo que la versión anterior codificaba

824 | Capítulo 28: Un ejemplo más realista


Machine Translated by Google

para bob, porque self es el sujeto implícito cuando se llama al método. lastName también devuelve el resultado, porque
esta operación ahora es una función llamada; calcula un valor para que la persona que llama lo use arbitrariamente,
incluso si solo se va a imprimir. De manera similar, el nuevo método giveRaise solo hace para sí mismo lo que hicimos
para demandar antes.

Cuando se ejecuta ahora, la salida de nuestro archivo es similar a la anterior: en su mayoría, solo hemos refactorizado el
código para permitir cambios más fáciles en el futuro, sin alterar su comportamiento:

Bob Smith 0
sue jones 100000
herrero jones
110000

Vale la pena señalar aquí algunos detalles de codificación. Primero, observe que el pago de sue ahora sigue siendo un
número entero después de un aumento de sueldo: convertimos el resultado matemático de nuevo en un número entero
llamando al int integrado en el método. Cambiar el valor a int o float probablemente no sea una preocupación importante
para esta demostración: los objetos enteros y de punto flotante tienen las mismas interfaces y se pueden mezclar dentro
de las expresiones. Aún así, es posible que debamos abordar los problemas de truncamiento y redondeo en un sistema
real: ¡el dinero probablemente sea importante para las personas!

Como aprendimos en el Capítulo 5, podemos manejar esto usando el round(N, 2) integrado para redondear y retener
centavos, usando el tipo decimal para fijar la precisión, o almacenando valores monetarios como números de coma
flotante completos y mostrándolos con un %.2f o {0:.2f} para la cadena de esteras para mostrar centavos como lo hicimos
antes. Por ahora, simplemente truncamos cualquier centavo con int. Para otra idea, vea también la función de dinero en
el módulo format.py del Capítulo 25; puede importar esta herramienta para mostrar el pago con comas, centavos y signos
de moneda.

En segundo lugar, observe que esta vez también estamos imprimiendo el apellido de sue, ya que la lógica del apellido se
ha encapsulado en un método, podemos usarlo en cualquier instancia de la clase.
Como hemos visto, Python le dice a un método qué instancia procesar pasándola automáticamente al primer argumento,
generalmente llamado self. Específicamente:

• En la primera llamada, bob.lastName(), bob es el sujeto implícito que se pasa a sí


mismo. • En la segunda llamada, sue.lastName(), sue va a sí mismo .

Realice un seguimiento de estas llamadas para ver cómo termina la instancia en sí misma: es un concepto clave.
El efecto neto es que el método obtiene el nombre del sujeto implícito cada vez.
Lo mismo sucede con giveRaise. Podríamos, por ejemplo, darle a bob un aumento llamando a giveRaise para ambas
instancias de esta manera también. Sin embargo, desafortunadamente para bob, su salario inicial cero le impedirá obtener
un aumento ya que el programa está codificado actualmente: nada por nada es nada, algo que tal vez queramos abordar
en un futuro 2.0
lanzamiento de nuestro software.

Finalmente, observe que el método giveRaise asume que el porcentaje se pasa como un número de coma flotante entre
cero y uno. Esa puede ser una suposición demasiado radical en el mundo real (¡un aumento del 1000% probablemente
sería un error para la mayoría de nosotros!); lo dejaremos pasar para este prototipo, pero es posible que queramos probar
o al menos documentar esto en una iteración futura de

Paso 2: Adición de métodos de comportamiento | 825


Machine Translated by Google

este código. Estén atentos para una repetición de esta idea en un capítulo posterior de este libro, donde codificaremos
algo llamado decoradores de funciones y exploraremos la declaración de afirmación de Python: alternativas que
pueden hacer la prueba de validez automáticamente durante el desarrollo. En el Capítulo 39, por ejemplo, escribiremos
una herramienta que nos permita validar con encantamientos extraños como los siguientes:

@rangetest(percent=(0.0, 1.0)) def # Usar decorador para validar


giveRaise(self, percent): self.pay =
int(self.pay * (1 + percent))

Paso 3: sobrecarga del operador


En este punto, tenemos una clase bastante completa que genera e inicializa instancias, junto con dos nuevos bits de
comportamiento para procesar instancias en forma de métodos.
Hasta aquí todo bien.

Tal como están las cosas, sin embargo, las pruebas aún son un poco menos convenientes de lo que deberían ser:
para rastrear nuestros objetos, tenemos que obtener e imprimir manualmente los atributos individuales (por ejemplo,
bob.name, sue.pay). Sería bueno si mostrar una instancia de una sola vez nos diera información útil.
Desafortunadamente, el formato de visualización predeterminado para un objeto de instancia no es muy bueno:
muestra el nombre de la clase del objeto y su dirección en la memoria (que es esencialmente inútil en Python, excepto
como un identificador único).

Para ver esto, cambie la última línea del script a print(sue) para que muestre el objeto como un todo. Esto es lo que
obtendrá: el resultado dice que sue es un "objeto" en 3.X y una "instancia" en 2.X tal como está codificado:

Bob Smith 0
Sue Jones 100000
Smith Jones
<__main__.Persona objeto en 0x00000000029A0668>

Proporcionar pantallas de impresión

Afortunadamente, es fácil hacerlo mejor empleando métodos de codificación de sobrecarga de operadores en una
clase que interceptan y procesan operaciones integradas cuando se ejecutan en las instancias de la clase.
Específicamente, podemos hacer uso de lo que probablemente sea el segundo método de sobrecarga de operadores
más utilizado en Python, después de __init__: el método __repr__ que implementaremos aquí, y su gemelo __str__
presentado en el capítulo anterior.

Estos métodos se ejecutan automáticamente cada vez que una instancia se convierte a su cadena de impresión.
Debido a que eso es lo que hace la impresión de un objeto, el efecto transitivo neto es que la impresión de un objeto
muestra lo que sea devuelto por el método __str__ o __repr__ del objeto, si el objeto define uno por sí mismo o
hereda uno de una superclase. Los nombres con doble subrayado se heredan como cualquier otro.

Técnicamente, print y str prefieren __str__ , y __repr__ se usa como respaldo para estos roles y en todos los demás
contextos. Aunque los dos pueden usarse para implementar

826 | Capítulo 28: Un ejemplo más realista


Machine Translated by Google

diferentes pantallas en diferentes contextos, codificar solo __repr__ solo es suficiente para dar una sola pantalla en
todos los casos: impresiones, apariencias anidadas y ecos interactivos. Esto aún permite a los clientes proporcionar
una visualización alternativa con __str__, pero solo para contextos limitados; dado que este es un ejemplo autónomo,
este es un punto discutible aquí.

El método constructor __init__ que ya hemos codificado es, estrictamente hablando, también sobrecarga de
operadores: se ejecuta automáticamente en el momento de la construcción para inicializar una instancia recién
creada. Sin embargo, los constructores son tan comunes que casi parecen un caso especial. Los métodos más
enfocados como __repr__ nos permiten aprovechar operaciones específicas y proporcionar un comportamiento
especializado cuando nuestros objetos se usan en esos contextos.

Pongamos esto en código. Lo siguiente amplía nuestra clase para brindar una visualización personalizada que
enumera los atributos cuando las instancias de nuestra clase se muestran como un todo, en lugar de depender de la
visualización predeterminada menos útil: # Agregue el método de sobrecarga __repr__ para imprimir objetos

Persona de clase:
def __init__(self, nombre, trabajo=Ninguno, pago=0):
self.name = nombre self.job = trabajo self.pay = pago
def lastName(self): return self.name.split()[-1]

def giveRaise(self, percent): self.pay =


int(self.pay * (1 + percent)) def __repr__(self): return
'[Persona: %s, %s]' % (self.name, self. pagar) # Método agregado
# Cadena para imprimir

si __nombre__ == '__principal__':
bob = Persona('Bob Smith') sue =
Persona('Sue Jones', job='dev', pay=100000) print(bob) print(sue)
print(bob.lastName(), sue.lastName()) sue.giveRaise(.10) print(sue)

Observe que estamos formateando la cadena % para construir la cadena de visualización en __repr__ aquí; en la
parte inferior, las clases usan objetos de tipo incorporados y operaciones como estas para realizar su trabajo. Una
vez más, todo lo que ya aprendió sobre los tipos y funciones incorporados se aplica al código basado en clases. En
gran medida, las clases solo agregan una capa adicional de estructura que empaqueta funciones y datos y admite
extensiones.

También hemos cambiado nuestro código de autodiagnóstico para imprimir objetos directamente, en lugar de
imprimir atributos individuales. Cuando se ejecuta, la salida es más coherente y significativa ahora; las líneas "[...]"
son devueltas por nuestro nuevo __repr__, ejecutado automáticamente por operaciones de impresión:

[Persona: Bob Smith, 0]


[Persona: Sue Jones, 100000]
herrero jones
[Persona: Sue Jones, 110000]

Paso 3: Sobrecarga de operadores | 827


Machine Translated by Google

Nota de diseño: como aprenderemos en el Capítulo 30, el método __repr__ a menudo se usa para proporcionar una
visualización de bajo nivel como código de un objeto cuando está presente, y __str__ está reservado para visualizaciones
informativas más fáciles de usar como la nuestra aquí. A veces, las clases proporcionan un __str__ para visualizaciones
fáciles de usar y un __repr__ con detalles adicionales para que los desarrolladores los vean. Debido a que la impresión
ejecuta __str__ y el indicador interactivo repite los resultados con __repr__, esto puede proporcionar a ambas audiencias
objetivo una visualización adecuada.

Dado que __repr__ se aplica a más vitrinas, incluidas las apariencias anidadas, y debido a que no estamos interesados
en mostrar dos formatos diferentes, el __repr__ todo incluido es suficiente para nuestra clase. Aquí, esto también significa
que nuestra visualización personalizada se usará en 2.X si enumeramos tanto a bob como a sue en una llamada de
impresión 3.X , una apariencia técnicamente anidada, según la barra lateral en "Portabilidad de la versión: impresiones"
en la página 821.

Paso 4: personalizar el comportamiento mediante subclases


En este punto, nuestra clase captura gran parte de la maquinaria de programación orientada a objetos en Python: crea
instancias, proporciona comportamiento en los métodos e incluso ahora hace un poco de sobrecarga de operadores para
interceptar operaciones de impresión en __repr__. Empaqueta de manera eficaz nuestros datos y nuestra lógica en un
único componente de software autónomo , lo que facilita la localización del código y su modificación en el futuro. Al
permitirnos encapsular el comportamiento, también nos permite factorizar ese código para evitar la redundancia y sus
dolores de cabeza de mantenimiento asociados.

El único concepto importante de programación orientada a objetos que aún no captura es la personalización por herencia.
En cierto sentido, ya estamos haciendo herencia, porque las instancias heredan métodos de sus clases. Sin embargo,
para demostrar el poder real de OOP, necesitamos definir una relación de superclase/subclase que nos permita extender
nuestro software y reemplazar fragmentos de comportamiento heredado. Esa es la idea principal detrás de OOP, después
de todo; al fomentar un modelo de codificación basado en la personalización del trabajo ya realizado, puede reducir
drásticamente el tiempo de desarrollo.

Codificación de subclases

Entonces, como siguiente paso, pongamos la metodología de programación orientada a objetos para usar y personalizar
nuestra clase de persona mediante la ampliación de nuestra jerarquía de software. A los efectos de este tutorial,
definiremos una subclase de Person llamada Manager que reemplaza el método giveRaise heredado con una versión
más especializada. Nuestra nueva clase comienza de la siguiente manera: Gerente de clase (Persona):

# Definir una subclase de Persona

Este código significa que estamos definiendo una nueva clase llamada Administrador, que hereda y puede agregar
personalizaciones a la superclase Persona. En términos sencillos, un Gerente es casi como una Persona (ciertamente,
un viaje muy largo para una broma muy pequeña...), pero el Gerente tiene una forma personalizada de dar aumentos.

828 | Capítulo 28: Un ejemplo más realista


Machine Translated by Google

En aras de la discusión, supongamos que cuando un gerente obtiene un aumento, recibe el porcentaje
transferido como de costumbre, pero también obtiene una bonificación adicional que por defecto es del 10 %.
Por ejemplo, si el aumento de un gerente se especifica en un 10 %, en realidad obtendrá un 20 %. (Cualquier
relación con Personas vivas o muertas es, por supuesto, estrictamente coincidente.) Nuestro nuevo método
comienza de la siguiente manera; Debido a que esta redefinición de giveRaise estará más cerca en el árbol
de clases de las instancias de Manager que la versión original en Person , efectivamente reemplaza y, por lo
tanto, personaliza la operación. Recuerde que de acuerdo con las reglas de búsqueda de herencia, gana la
versión más baja del nombre:1

Gerente de clase (Persona): # Heredar atributos de persona

def giveRaise (auto, porcentaje, bonificación = .10): # Redefinir para personalizar

Métodos de aumento: la forma incorrecta Ahora,

hay dos formas en las que podríamos codificar esta personalización de Manager : una forma buena y una
forma mala. Empecemos por el mal camino, ya que puede ser un poco más fácil de entender. La mala forma
es cortar y pegar el código de giveRaise in Person y modificarlo para Manager,
como esto:

Gerente de clase (Persona):


def giveRaise(self, percent, bonus=.10): self.pay =
int(self.pay * (1 + percent + bonus)) # Malo: cortar y pegar

Esto funciona como se anuncia: cuando más tarde llamemos al método giveRaise de una instancia de
Gerente , ejecutará esta versión personalizada, que agrega la bonificación adicional. Entonces, ¿qué tiene de
malo algo que se ejecuta correctamente?

El problema aquí es muy general: cada vez que copia código con cortar y pegar, esencialmente duplica su
esfuerzo de mantenimiento en el futuro. Piénselo: debido a que copiamos la versión original, si alguna vez
tenemos que cambiar la forma en que se otorgan los aumentos (y probablemente lo haremos), tendremos que
cambiar el código en dos lugares, no en uno. Aunque este es un ejemplo pequeño y artificial, también es
representativo de un problema universal: cada vez que tenga la tentación de programar copiando el código de
esta manera, probablemente desee buscar un mejor enfoque.

Métodos de aumento: la buena manera Lo que

realmente queremos hacer aquí es aumentar de alguna manera el giveRaise original , en lugar de reemplazarlo
por completo. La buena manera de hacerlo en Python es llamando directamente a la versión original, con
argumentos aumentados, como este:

Gerente de clase (Persona):


def giveRaise (auto, porcentaje, bonificación = .10):
Person.giveRaise(yo, porcentaje + bonificación) # Bueno: aumentar original

1. Y sin ofender a los gerentes de la audiencia, por supuesto. Una vez enseñé una clase de Python en Nueva Jersey,
y nadie se rió de esta broma, entre otras. Más tarde, los organizadores me dijeron que era un grupo de gerentes
que evaluaban Python.

Paso 4: Personalización del comportamiento mediante subclases | 829


Machine Translated by Google

Este código aprovecha el hecho de que el método de una clase siempre se puede llamar a través de una
instancia (la forma habitual, donde Python envía la instancia al argumento self automáticamente) o a través
de la clase (el esquema menos común, donde debe pasar la instancia manualmente ). En términos más
simbólicos, recuerde que una llamada de método normal de esta forma:

instancia.método(args...)

Python lo traduce automáticamente a esta forma equivalente:

class.method(instancia, argumentos...)

donde la clase que contiene el método a ejecutar está determinada por la regla de búsqueda de herencia
aplicada al nombre del método. Puede codificar cualquiera de los formularios en su secuencia de comandos,
pero existe una ligera asimetría entre los dos: debe recordar pasar la instancia manualmente si llama
directamente a través de la clase. El método siempre necesita una instancia de sujeto de una forma u otra, y
Python lo proporciona automáticamente solo para las llamadas realizadas a través de una instancia. Para las
llamadas a través del nombre de la clase, debe enviar una instancia a sí mismo; para el código dentro de un
método como giveRaise, self ya es el sujeto de la llamada y, por lo tanto, la instancia que se transmite.

Llamar a través de la clase directamente subvierte efectivamente la herencia y eleva la llamada más arriba en
el árbol de clases para ejecutar una versión específica. En nuestro caso, podemos usar esta técnica para
invocar el giveRaise predeterminado en persona, aunque se haya redefinido en el nivel de administrador . En
cierto sentido, debemos llamar a través de Persona de esta manera, porque un self.giveRaise() dentro del
código giveRaise de Manager generaría un bucle; dado que self ya es un Manager, self.giveRaise() se
resolvería nuevamente como Manager.giveRaise, y así sucesivamente . y así sucesivamente hasta que se
agote la memoria disponible.

Esta versión "buena" puede parecer una pequeña diferencia en el código, pero puede marcar una gran
diferencia para el mantenimiento futuro del código, ya que la lógica de giveRaise vive ahora en un solo lugar
( método de la persona), solo tenemos una versión para cambiar en el futuro a medida que evolucionan las
necesidades. Y realmente, este formulario captura nuestra intención más directamente de todos modos:
queremos realizar la operación estándar de dar aumento , pero simplemente agregar una bonificación
adicional. Aquí está nuestro archivo de módulo completo con este paso aplicado:

# Agregar personalización de un comportamiento en una subclase

Persona de clase:
def __init__(self, nombre, trabajo=Ninguno, pago=0):
self.name = nombre self.job = trabajo self.pay = pago
def lastName(self): return self.name.split()[-1]

def giveRaise(self, percent): self.pay =


int(self.pay * (1 + percent)) def __repr__(self): return
'[Persona: %s, %s]' % (self.name, self. pagar)

Gerente de clase (Persona):

830 | Capítulo 28: Un ejemplo más realista


Machine Translated by Google

def giveRaise(self, percent, bonus=.10): # Redefinir a este nivel


Person.giveRaise(yo, porcentaje + bonificación) # Versión de la persona que llama

si __nombre__ == '__principal__':
bob = Persona('Bob Smith') sue
= Persona('Sue Jones', job='dev', pay=100000) print(bob)
print(sue) print(bob.lastName(), sue.lastName())
sue.giveRaise(.10) print(sue) tom = Manager('Tom Jones',
'mgr', 50000) tom.giveRaise(.10) print(tom.lastName())
print(tom)

# Hacer un Gerente: __init__


# Ejecuta una versión personalizada
# Ejecuta el método heredado
# Ejecuta __repr__ heredado

Para probar la personalización de nuestra subclase Manager , también hemos agregado un código de
autocomprobación que crea un Manager, llama a sus métodos y lo imprime. Cuando creamos un Gerente,
pasamos un nombre, y un trabajo opcional y pagamos como antes, porque el Gerente no tenía un
constructor __init__ , lo hereda en Persona. Aquí está el resultado de la nueva versión: [Persona: Bob
Smith, 0]
[Persona: Sue Jones, 100000]
Smith Jones
[Persona: Sue Jones, 110000]
Jones
[Persona: Tom Jones, 60000]

Todo se ve bien aquí: bob y sue son como antes, y cuando tom the Manager recibe un aumento
del 10 %, realmente obtiene el 20 % (su salario va de $50 000 a $60 000), porque el giveRaise
personalizado en Manager se ejecuta para él solo Observe también cómo la impresión de tom
como un todo al final del código de prueba muestra el formato agradable definido en __repr__
de Person : los objetos Manager obtienen esto, lastName y el código del método constructor
__init__ "gratis" de Person, por herencia.

¿Qué hay de súper?


Para ampliar los métodos heredados, los ejemplos de este capítulo simplemente llaman al original a través
del nombre de la superclase: Person.giveRaise(...). Este es el esquema tradicional y más simple en Python,
y el que se usa en la mayor parte de este libro.

Los programadores de Java pueden estar especialmente interesados en saber que Python también tiene
una función súper incorporada que permite volver a llamar a los métodos de una superclase de manera
más genérica, pero es engorroso de usar en 2.X; difiere en forma entre 2.X y 3.X; se basa en una semántica
inusual en 3.X; funciona de manera desigual con la sobrecarga del operador de Python; y no siempre
encaja bien con la herencia múltiple codificada tradicionalmente, donde una sola llamada de superclase no
será suficiente.

En su defensa, la superllamada también tiene un caso de uso válido (despacho del método cooperativo del
mismo nombre en múltiples árboles de herencia), pero se basa en el ordenamiento de clases "MRO", que
muchos encuentran esotérico y artificial; asume de manera poco realista que el despliegue universal se
utilizará de manera confiable; no es totalmente compatible con el reemplazo del método y el argumento variable

Paso 4: Personalización del comportamiento mediante subclases | 831


Machine Translated by Google

liza; y para muchos observadores parece una solución oscura para un caso de uso que es raro en el código
real de Python.

Debido a estas desventajas, este libro prefiere llamar a las superclases por un nombre explícito en lugar
de super, recomienda la misma política para los recién llegados y difiere la presentación de super hasta el
Capítulo 32. Por lo general, se juzga mejor después de aprender el más simple, y generalmente más
tradicional y "Pythonic". ” maneras de lograr los mismos objetivos, especialmente si eres nuevo en OOP.
Temas como los MRO y el envío cooperativo de herencia múltiple parecen pedir mucho a los principiantes
y a otros.

Y a cualquier programador de Java en la audiencia: sugiero resistir la tentación de usar el super de Python
hasta que haya tenido la oportunidad de estudiar sus implicaciones sutiles. Una vez que pasa a la herencia
múltiple, no es lo que cree que es, y probablemente más de lo que espera. La clase que invoca puede no
ser la superclase en absoluto, e incluso puede variar según el contexto. O parafraseando una frase de una
película: el súper de Python es como una caja de bombones: ¡ nunca sabes lo que te va a tocar!

Polimorfismo en acción
Para que esta adquisición de comportamiento heredado sea aún más llamativa, podemos añadir el
siguiente código al final de nuestro archivo de forma temporal:
si __nombre__ == '__principal__':
...
print('--Los tres--') for obj
in (bob, sue, tom): # Procesar objetos de forma genérica
obj.giveRaise(.10) print(obj) # Ejecuta el giveRaise de este objeto
# Ejecutar el __repr__ común

Aquí está la salida resultante, con sus nuevas partes resaltadas en negrita:
[Persona: Bob Smith, 0]
[Persona: Sue Jones, 100000]
herrero jones
[Persona: Sue Jones, 110000]
jones
[Persona: Tom Jones, 60000]
--Los tres--
[Persona: Bob Smith, 0]
[Persona: Sue Jones, 121000]
[Persona: Tom Jones, 72000]

En el código agregado, el objeto es una Persona o un Gerente, y Python ejecuta el giveRaise


apropiado automáticamente: nuestra versión original en Persona para bob y sue, y nuestra versión
personalizada en Manager para tom. Rastree las llamadas del método para ver cómo Python selecciona
el método giveRaise correcto para cada objeto.

Esta es solo la noción de polimorfismo de Python, que conocimos anteriormente en el libro, en el


trabajo nuevamente: lo que hace giveRaise depende de a qué lo hagas. Aquí, se hace aún más obvio
cuando selecciona del código que hemos escrito nosotros mismos en las clases. El efecto práctico en
este código es que sue obtiene otro 10 % pero tom obtiene otro 20 %, porque

832 | Capítulo 28: Un ejemplo más realista


Machine Translated by Google

giveRaise se envía en función del tipo de objeto. Como hemos aprendido, el polimorfismo
está en el corazón de la flexibilidad de Python. Pasar cualquiera de nuestros tres objetos
a una función que llame a un método giveRaise , por ejemplo, tendría el mismo efecto: la
versión adecuada se ejecutaría automáticamente, según el tipo de objeto que se pasara.
Por otro lado, la impresión ejecuta el mismo __repr__ para los tres objetos, porque está codificado
solo una vez en Persona. Manager se especializa y aplica el código que escribimos originalmente en
Person. Aunque este ejemplo es pequeño, ya está aprovechando el talento de OOP para la
personalización y reutilización de código; con las clases, esto casi parece automático a veces.

Heredar, personalizar y ampliar De

hecho, las clases pueden ser incluso más flexibles de lo que implica nuestro ejemplo. En general, las
clases pueden heredar, personalizar o ampliar el código existente en las superclases. Por ejemplo,
aunque aquí nos enfocamos en la personalización, también podemos agregar métodos únicos a
Manager que no están presentes en Person, si los Managers requieren algo completamente diferente
(se pretende que sea una referencia del mismo nombre de Python). El siguiente fragmento ilustra.
Aquí, giveRaise re define el método de una superclase para personalizarlo, pero someThingElse
define algo nuevo para extender:

clase Persona: def


lastName(self): ... def
giveRaise(self): ... def
__repr__(self): ...

Gerente de clase(Persona): def # heredar


giveRaise(self, ...): ... def # Personalizar
someThingElse(self, ...): ... # Extender

tom = Gerente()
tom.apellido() # Heredado palabra por palabra

tom.giveRaise() # Versión personalizada


tom.someThingElse() # Extensión aquí
print(tom) # Método de sobrecarga heredado

Los métodos adicionales como someThingElse de este código amplían el software existente y están
disponibles solo en los objetos Manager , no en Persons. Sin embargo, para los propósitos de este
tutorial, limitaremos nuestro alcance a personalizar parte del comportamiento de Person redefiniéndolo,
no agregándolo.

OOP: The Big Idea Tal

como está, nuestro código puede ser pequeño, pero es bastante funcional. Y realmente, ya ilustra el
punto principal detrás de OOP en general: en OOP, programamos personalizando lo que ya se ha
hecho, en lugar de copiar o cambiar el código existente. Esto no siempre es una victoria obvia para
los recién llegados a primera vista, especialmente dados los requisitos de codificación adicionales de
las clases. Pero, en general, el estilo de programación implícito en las clases puede reducir
radicalmente el tiempo de desarrollo en comparación con otros enfoques.

Paso 4: Personalización del comportamiento mediante subclases | 833


Machine Translated by Google

Por ejemplo, en nuestro ejemplo, teóricamente podríamos haber implementado una operación personalizada
de aumento de donaciones sin subclases, pero ninguna de las otras opciones produce un código tan óptimo
como el nuestro:

• Aunque podríamos haber simplemente codificado a Manager desde cero como un código nuevo e
independiente, habríamos tenido que volver a implementar todos los comportamientos en Person que
son iguales para Managers. • Aunque podríamos simplemente haber cambiado la clase de Persona

existente en su lugar para los requisitos del aumento de sueldo del Gerente , hacerlo probablemente
rompería los lugares donde todavía necesitamos el comportamiento de Persona original.

• Aunque podríamos simplemente haber copiado la clase Person en su totalidad, cambiar el nombre de la
copia a Manager y cambiar su giveRaise, hacerlo introduciría redundancia de código que duplicaría
nuestro trabajo en el futuro; los cambios realizados en Person en el futuro no se recogerá
automáticamente, pero tendría que propagarse manualmente al código del administrador . Como de
costumbre, el enfoque de cortar y pegar puede parecer rápido ahora, pero duplicará su trabajo en el
futuro.

Las jerarquías personalizables que podemos construir con clases brindan una solución mucho mejor para el
software que evolucionará con el tiempo. Ninguna otra herramienta en Python admite este modo de desarrollo.
Debido a que podemos adaptar y ampliar nuestro trabajo anterior mediante la codificación de nuevas
subclases, podemos aprovechar lo que ya hemos hecho, en lugar de comenzar desde cero cada vez,
rompiendo lo que ya funciona o introduciendo varias copias de código que es posible que deban actualizarse.
en el futuro. Cuando se hace bien, OOP es un poderoso aliado del programador.

Paso 5: Personalización de constructores también


Nuestro código funciona tal como está, pero si estudia la versión actual detenidamente, es posible que le
sorprenda algo un poco extraño: parece inútil tener que proporcionar un nombre de trabajo de administrador
para los objetos del Administrador cuando los creamos: esto ya está implícito en la clase misma. Sería mejor
si de alguna manera pudiéramos completar este valor automáticamente cuando se crea un Gerente .

El truco que necesitamos mejorar en esto resulta ser el mismo que empleamos en la sección anterior:
queremos personalizar la lógica del constructor para los Gerentes de tal manera que proporcione un nombre
de trabajo automáticamente. En términos de código, queremos redefinir un método __init__ en Manager que
nos proporcione la cadena mgr . Y como en la personalización de giveRaise , también queremos ejecutar el
__init__ original en Person llamando a través del nombre de la clase, por lo que todavía inicializa los atributos
de información de estado de nuestros objetos.

La siguiente extensión de person.py hará el trabajo: codificamos el nuevo constructor de Manager y


cambiamos la llamada que crea a tom para que no pase el nombre del trabajo de mgr :

# File person.py #
Agregar personalización del constructor en una subclase

Persona de clase:
def __init__(self, nombre, trabajo=Ninguno, pago=0):
self.name = nombre

834 | Capítulo 28: Un ejemplo más realista


Machine Translated by Google

self.job = job
self.pay = pay def
lastName(self): return
self.name.split()[-1]
def giveRaise(self, percent): self.pay
= int(self.pay * (1 + percent)) def __repr__(self):
return '[Persona: %s, %s]' % (self.name, self. pagar)

Gerente de clase (Persona):


def __init__(yo, nombre, pago): # Redefinir constructor
Persona.__init__(yo, nombre, 'gestor', pago) # Ejecutar original con 'mgr'
def giveRaise(self, percent, bonus=.10):
Person.giveRaise(yo, porcentaje + bonificación)

si __nombre__ == '__principal__':
bob = Persona('Bob Smith') sue
= Persona('Sue Jones', job='dev', pay=100000) print(bob)
print(sue) print(bob.lastName(), sue.lastName())
sue.giveRaise(.10) print(sue) tom = Manager('Tom Jones',
50000) tom.giveRaise(.10) print(tom.lastName()) print(tom)

# Nombre del trabajo no necesario:


# Implícito/ establecido por clase

Nuevamente, estamos usando la misma técnica para aumentar el constructor __init__ aquí que usamos para
giveRaise anteriormente: ejecutar la versión de la superclase llamando directamente a través del nombre de la
clase y pasando la instancia propia explícitamente. Aunque el constructor tiene un nombre extraño, el efecto es
idéntico. Debido a que también necesitamos que se ejecute la lógica de construcción de Person (para inicializar
atributos de instancia), realmente tenemos que llamarlo de esta manera; de lo contrario, las instancias no
tendrían ningún atributo adjunto.

Llamar a constructores de superclases desde redefiniciones de esta manera resulta ser un patrón de
codificación muy común en Python. Por sí mismo, Python usa la herencia para buscar y llamar solo a un método
__init__ en el momento de la construcción: el más bajo en el árbol de clases. Si necesita que se ejecuten
métodos __init__ más altos en el momento de la construcción (y generalmente lo hace), debe llamarlos
manualmente y, por lo general, a través del nombre de la superclase. La ventaja de esto es que puede ser
explícito sobre qué argumento pasar al constructor de la superclase y puede optar por no llamarlo en absoluto:
no llamar al constructor de la superclase le permite reemplazar su lógica por completo, en lugar de aumentarla.

El resultado del código de autocomprobación de este archivo es el mismo que antes: no hemos cambiado lo
que hace, simplemente lo hemos reestructurado para deshacernos de alguna redundancia lógica:

[Persona: Bob Smith, 0]


[Persona: Sue Jones, 100000]
herrero jones
[Persona: Sue Jones, 110000]
jones
[Persona: Tom Jones, 60000]

Paso 5: Personalización de constructores también | 835


Machine Translated by Google

OOP es más simple de lo que piensas


En esta forma completa, y a pesar de sus tamaños relativamente pequeños, nuestras clases capturan
casi todos los conceptos importantes en la maquinaria OOP de Python:

• Creación de instancias: completar atributos de instancia •


Métodos de comportamiento: encapsular la lógica en los métodos de una
clase • Sobrecarga de operadores: proporcionar comportamiento para operaciones integradas como
imprimir • Personalización del comportamiento: redefinición de métodos en subclases para
especializarlos • Personalización de constructores: adición de lógica de inicialización a pasos de superclase

La mayoría de estos conceptos se basan en solo tres ideas simples: la búsqueda hereditaria de atributos
en los árboles de objetos, el argumento propio especial en los métodos y el envío automático de la
sobrecarga del operador a los métodos.

En el camino, también hemos hecho que nuestro código sea fácil de cambiar en el futuro, aprovechando
la propensión de la clase a factorizar el código para reducir la redundancia. Por ejemplo, resumimos la
lógica en los métodos y volvimos a llamar a los métodos de la superclase desde las extensiones para
evitar tener varias copias del mismo código. La mayoría de estos pasos fueron una consecuencia natural
del poder estructurador de las clases.

En general, eso es todo lo que hay en POO en Python. Las clases ciertamente pueden llegar a ser más
grandes que esto, y hay algunos conceptos de clase más avanzados, como decoradores y metaclases,
que veremos en capítulos posteriores. Sin embargo, en términos de lo básico, nuestras clases ya lo hacen
todo. De hecho, si ha comprendido el funcionamiento de las clases que hemos escrito, la mayor parte del
código Python OOP ahora debería estar a su alcance.

Otras formas de combinar clases Habiendo

dicho eso, también debo decirte que aunque la mecánica básica de OOP es simple en Python, parte del
arte en programas más grandes radica en la forma en que se combinan las clases. Nos estamos
enfocando en la herencia en este tutorial porque ese es el mecanismo que proporciona el lenguaje
Python, pero los programadores a veces también combinan clases de otras maneras.

Por ejemplo, un patrón de codificación común consiste en anidar objetos unos dentro de otros para crear
compuestos. Exploraremos este patrón con más detalle en el Capítulo 31, que en realidad se trata más
de diseño que de Python. Sin embargo, como un ejemplo rápido, podríamos usar esta idea de composición
para codificar nuestra extensión Manager incrustando una Persona, en lugar de heredarla.

La siguiente alternativa, codificada en el archivo person-composite.py, lo hace usando el método de


sobrecarga del operador __get attr__ para interceptar extracciones de atributos indefinidos y delegarlas
al objeto incrustado con el getattr incorporado. La llamada getattr se introdujo en el Capítulo 25: es lo
mismo que la notación de búsqueda de atributo XY y, por lo tanto, por

836 | Capítulo 28: Un ejemplo más realista


Machine Translated by Google

herencia de formularios, pero el nombre del atributo Y es una cadena de tiempo de ejecución, y __getattr__ se cubre
en su totalidad en el Capítulo 30, pero su uso básico es lo suficientemente simple como para aprovecharlo aquí.

Al combinar estas herramientas, el método giveRaise aquí todavía logra la personalización, al cambiar el argumento
que se pasa al objeto incrustado. En efecto, Manager se convierte en una capa de controlador que pasa las llamadas
al objeto incrustado, en lugar de a los métodos de la superclase:

# Archivo person-composite.py #
Alternativa de administrador basada en incrustaciones

Persona de clase:
...mismo...

Gerente de clase:
def __init__(self, name, pay): self.person =
Person(name, 'mgr', pay) # Incrustar un objeto Persona
def giveRaise(self, percent, bonus=.10):
self.person.giveRaise(porcentaje + bonificación) def # Interceptar y delegar
__getattr__(self, attr): return getattr(self.person, attr) def
__repr__(self): return str(self.person) # Delegar todos los demás atributos

# Debe sobrecargar nuevamente (en 3.X)

si __nombre__ == '__principal__':
...mismo...

El resultado de esta versión es el mismo que el anterior, por lo que no lo mencionaré nuevamente. El punto más
importante aquí es que esta alternativa de Manager es representativa de un patrón de codificación general
generalmente conocido como delegación: una estructura basada en compuestos que administra un objeto envuelto
y propaga las llamadas a métodos.

Este patrón funciona en nuestro ejemplo, pero requiere aproximadamente el doble de código y es menos adecuado
que la herencia para los tipos de personalizaciones directas que pretendíamos expresar (de hecho, ningún
programador razonable de Python codificaría este ejemplo de esta manera en la práctica, ¡excepto quizás aquellos
que escriben tutoriales generales!). Manager no es realmente una persona aquí, por lo que necesitamos un código
adicional para enviar llamadas de método manualmente al objeto incrustado; los métodos de sobrecarga de
operadores como __repr__ deben redefinirse (en 3.X, al menos, como se indica en la próxima barra lateral "Captura
de atributos incorporados en 3.X" en la página 839); y agregar un nuevo comportamiento de administrador es menos
sencillo ya que la información de estado se elimina un nivel.

Aun así, la incrustación de objetos y los patrones de diseño basados en ella pueden encajar muy bien cuando los
objetos incrustados requieren una interacción más limitada con el contenedor de lo que implica la personalización
directa. Una capa de controlador, o proxy, como este Administrador alternativo , por ejemplo, podría ser útil si
queremos adaptar una clase a una interfaz esperada que no admite, o rastrear o validar llamadas a los métodos de
otro objeto (de hecho, lo haremos). use un patrón de codificación casi idéntico cuando estudiemos a los decoradores
de clase más adelante en el libro).

Además, una clase Departamento hipotética como la siguiente podría agregar otros objetos para tratarlos como un
conjunto. Reemplace el código de autodiagnóstico en la parte inferior de la

Paso 5: Personalización de constructores también | 837


Machine Translated by Google

person.py temporalmente para probar esto por su cuenta; el archivo person-department.py en los ejemplos
del libro hace lo siguiente:
# Archivo persona-departamento.py
# Agregar objetos incrustados en un compuesto

Persona de clase:
...mismo...

Gerente de clase (Persona):


...mismo...

clase Departamento: def


__init__(self, *args):
self.members = list(args) def
addMember(self, person):
self.members.append(persona) def
giveRaises(self, percent): for person in
self.members:
person.giveRaise(percent) def
showAll(self): for person in self.members:
print(person)

si __nombre__ == '__principal__':
bob = Persona('Bob Smith') sue =
Persona('Sue Jones', trabajo='dev', pago=100000) tom = Gerente('Tom
Jones', 50000)

desarrollo = Departamento(bob, sue) # Incrustar objetos en un compuesto


desarrollo.addMember(tom) desarrollo.giveRaises(.10)
desarrollo.showAll() # Ejecuta giveRaise de objetos incrustados
# Ejecuta __repr__ de objetos incrustados

Cuando se ejecuta, el método showAll del departamento enumera todos los objetos que contiene
después de actualizar su estado de manera polimórfica real con giveRaises:
[Persona: Bob Smith, 0]
[Persona: Sue Jones, 110000]
[Persona: Tom Jones, 60000]

Curiosamente, este código utiliza herencia y composición: el Departamento es un compuesto que


incrusta y controla otros objetos para agregarlos, pero los objetos Person y Manager incrustados usan la
herencia para personalizar. Como otro ejemplo, una GUI podría usar la herencia de manera similar para
personalizar el comportamiento o la apariencia de las etiquetas y los botones, pero también la composición
para crear paquetes más grandes de widgets integrados, como formularios de entrada, calculadoras y
editores de texto. La estructura de clases a usar depende de los objetos que intenta modelar; de hecho,
la capacidad de modelar entidades del mundo real de esta manera es una de las fortalezas de OOP.

Los problemas de diseño como la composición se exploran en el Capítulo 31, por lo que pospondremos
más investigaciones por ahora. Pero nuevamente, en términos de la mecánica básica de OOP en Python,
nuestras clases Person y Manager ya cuentan la historia completa. Ahora que has dominado

838 | Capítulo 28: Un ejemplo más realista


Machine Translated by Google

Sin embargo, los conceptos básicos de OOP, el desarrollo de herramientas generales para aplicarlo más
fácilmente en sus scripts es a menudo un siguiente paso natural, y el tema de la siguiente sección.

Captura de atributos incorporados en 3.X Una

nota de implementación: en Python 3.X, y en 2.X cuando las clases de "nuevo estilo" de 3.X están habilitadas, la
clase Manager alternativa basada en delegación del sitio compuesto por persona de archivo .py que codificamos
en este capítulo no podrá interceptar y delegar atributos de métodos de sobrecarga de operadores como
__repr__ sin redefinirlos. Aunque sabemos que __repr__ es el único nombre que se usa en nuestro ejemplo
específico, este es un problema general para las clases basadas en delegación.

Recuerde que las operaciones integradas como imprimir y sumar invocan implícitamente métodos de sobrecarga
de operadores como __repr__ y __add__. En las clases de nuevo estilo de 3.X, las operaciones integradas como
estas no enrutan sus búsquedas de atributos implícitas a través de administradores de atributos genéricos: no
se invoca ni __getattr__ (ejecutar para atributos indefinidos) ni su primo __getattribute__ (ejecutar para todos los
atributos). Esta es la razón por la que tenemos que redefinir __repr__ de forma redundante en el Administrador
alternativo , para garantizar que la impresión se enruta al objeto Persona incrustado en 3.X.

Comente este método para verlo en vivo: la instancia de Manager se imprime con un valor predeterminado en
3.X, pero todavía usa __repr__ de Person en 2.X. De hecho, __repr__ en Manager no se requiere en absoluto
en 2.X, ya que está codificado para usar las clases normales y predeterminadas de 2.X (también conocidas
como "clásicas") :

c:\code> py ÿ3 persona-compuesto.py
[Persona: Bob Smith, 0]
...etc...
<__principal__.Objeto administrador en 0x00000000029AA8D0>

c:\code> py ÿ2 persona-compuesto.py
[Persona: Bob Smith, 0] ...etc...

[Persona: Tom Jones, 60000]

Técnicamente, esto sucede porque las operaciones integradas comienzan su búsqueda implícita de nombres de
métodos en la instancia en las clases clásicas predeterminadas de 2.X , pero comienzan en la clase en las
clases de nuevo estilo obligatorias de 3.X , omitiendo la instancia por completo. Por el contrario, las extracciones
explícitas de atributos por nombre siempre se enrutan primero a la instancia en ambos modelos. En las clases
clásicas 2.X, los atributos de ruta incorporados también son así: por ejemplo, la impresión enruta __repr__ a
través de __getattr__. Es por eso que comentar el __repr__ del Gerente no tiene efecto en 2.X: la llamada se
delega a Persona. Las clases de nuevo estilo también heredan un valor predeterminado para __repr__ de su
superclase de objeto automático que frustraría __getattr__, pero el __getattribute__ de nuevo estilo tampoco
intercepta el nombre.

Este es un cambio, pero no es un impedimento: las clases de nuevo estilo basadas en delegación generalmente
pueden redefinir los métodos de sobrecarga de operadores para delegarlos a objetos envueltos, ya sea
manualmente o mediante herramientas o superclases. Sin embargo, este tema es demasiado avanzado para
explorar más en este tutorial, así que no se preocupe demasiado por los detalles aquí. Esté atento a que se
revise en el Capítulo 31 y el Capítulo 32 (el último de los cuales define las clases de nuevo estilo de manera más
formal); para impactar ejemplos nuevamente en la cobertura de gestión de atributos de

Paso 5: Personalización de constructores también | 839


Machine Translated by Google

el Capítulo 38 y el decorador de clase Privado en el Capítulo 39 (el último de estos también codifica
soluciones alternativas); y ser un factor de caso especial en una definición de herencia casi formal en
el Capítulo 40. En un lenguaje como Python que admite tanto la intercepción de atributos como la
sobrecarga de operadores, ¡los impactos de este cambio pueden ser tan amplios como implica esta propagación!

Paso 6: Uso de herramientas de introspección

Hagamos un ajuste final antes de colocar nuestros objetos en una base de datos. Tal como están, nuestras clases
están completas y demuestran la mayoría de los conceptos básicos de programación orientada a objetos en
Python. Sin embargo, todavía tienen dos problemas restantes que probablemente deberíamos resolver antes de
comenzar con ellos:

• En primer lugar, si observa la visualización de los objetos tal como están ahora, notará que cuando imprime
tom the Manager, la visualización lo etiqueta como una Persona. Eso no es técnicamente incorrecto, ya que
el Gerente es una especie de Persona personalizada y especializada . Aún así, sería más preciso mostrar
un objeto con la clase más específica (es decir, la más baja) posible: aquella de la que está hecho el objeto.

• En segundo lugar, y quizás más importante, el formato de visualización actual muestra solo los atributos que
incluimos en nuestro __repr__, y eso podría no tener en cuenta los objetivos futuros.
Por ejemplo, aún no podemos verificar que el nombre del trabajo de tom haya sido establecido correctamente
en mgr por el constructor de Manager , porque el __repr__ que codificamos para Persona no imprime este
campo. Peor aún, si alguna vez expandimos o cambiamos el conjunto de atributos asignados a nuestros
objetos en __init__, tendremos que recordar actualizar también __repr__ para que se muestren los nuevos
nombres, o se desincronizará con el tiempo.

El último punto significa que, una vez más, hemos creado un potencial trabajo adicional para nosotros mismos en
el futuro al introducir redundancia en nuestro código. Debido a que cualquier disparidad en __repr__ se reflejará
en la salida del programa, esta redundancia puede ser más obvia que las otras formas que abordamos
anteriormente; aun así, evitar el trabajo extra en el futuro es generalmente algo bueno.

Atributos de clases especiales

Podemos abordar ambos problemas con las herramientas de introspección de Python: funciones y atributos
especiales que nos brindan acceso a algunas de las partes internas de las implementaciones de los objetos. Estas
herramientas son algo avanzadas y generalmente las usan más las personas que escriben herramientas para que
las usen otros programadores que los programadores que desarrollan aplicaciones. Aun así, un conocimiento
básico de algunas de estas herramientas es útil porque nos permiten escribir código que procesa clases de
manera genérica. En nuestro código, por ejemplo, hay dos ganchos que pueden ayudarnos, los cuales se
introdujeron cerca del final del capítulo anterior y se usaron en ejemplos anteriores:

• El atributo incorporado instance.__class__ proporciona un enlace desde una instancia a la clase a partir de la
cual se creó. Las clases a su vez tienen un __nombre__, al igual que los módulos,

840 | Capítulo 28: Un ejemplo más realista


Machine Translated by Google

y una secuencia __bases__ que proporciona acceso a las superclases. Podemos usarlos aquí
para imprimir el nombre de la clase a partir de la cual se crea una instancia en lugar de una que
hemos codificado.

• El atributo incorporado object.__dict__ proporciona un diccionario con un par clave/valor para cada
atributo adjunto a un objeto de espacio de nombres (incluidos módulos, clases e instancias).
Debido a que es un diccionario, podemos obtener su lista de claves, indexar por clave, iterar sobre
sus claves, etc., para procesar todos los atributos de forma genérica. Podemos usar esto aquí
para imprimir cada atributo en cualquier instancia, no solo aquellos que codificamos en pantallas
personalizadas, como hicimos en las herramientas del módulo del Capítulo 25.

Conocimos la primera de estas categorías en el capítulo anterior, pero aquí hay una revisión rápida del
indicador interactivo de Python con las últimas versiones de nuestras clases person.py . Observe cómo
cargamos Person en el indicador interactivo con una declaración from aquí: los nombres de clase viven
y se importan desde módulos, exactamente como nombres de funciones y otras variables:

>>> from person import Persona >>>


bob = Persona('Bob Smith') >>> bob
# Mostrar __repr__ de bob (no __str__)
[Persona: Bob Smith, 0] >>>
imprimir(bob) # Ídem: imprimir => __str__ o __repr__
[Persona: Bob Smith, 0]

>>> bob.__clase__ # Muestra la clase de bob y su nombre


<clase 'persona.Persona'> >>>
bob.__clase__.__nombre__ 'Persona'

>>> list(bob.__dict__.keys()) ['pago', # Los atributos son realmente claves de


'trabajo', 'nombre'] dictado # Usar la lista para forzar la lista en 3.X

>>> for key in bob.__dict__: print(key,


'=>', bob.__dict__[key]) # Indexar manualmente

pago => 0
trabajo =>
Ninguno nombre => Bob Smith

>>> for clave en bob.__dict__:


print(key, '=>', getattr(bob, key)) # obj.attr, pero attr es una var

pago => 0
trabajo => Ninguno
nombre => Bob Smith

Como se señaló brevemente en el capítulo anterior, es posible que algunos atributos accesibles desde
una instancia no se almacenen en el diccionario __dict__ si la clase de la instancia define __slots__:
una característica opcional y relativamente oscura de las clases de nuevo estilo (y, por lo tanto, todas
las clases en Python 3.X ) que almacena atributos secuencialmente en la instancia; puede excluir una
instancia __dict__ por completo; y que no estudiaremos en su totalidad hasta el Capítulo 31 y el Capítulo
32. Dado que las ranuras realmente pertenecen a clases en lugar de instancias, y dado que rara vez son

Paso 6: Uso de herramientas de introspección | 841


Machine Translated by Google

utilizado en cualquier caso, podemos ignorarlos razonablemente aquí y centrarnos en el __dict__ normal.

Mientras lo hacemos, sin embargo, tenga en cuenta que algunos programas pueden necesitar detectar
excepciones para un __dict__ faltante, o usar hasattr para probar o getattr con un valor predeterminado si sus
usuarios pueden implementar ranuras. Como veremos en el Capítulo 32, el código de la siguiente sección no
fallará si lo usa una clase con espacios (la falta de ellos es suficiente para garantizar un __dict__) , pero los
espacios—y otros atributos “virtuales”—no serán reportados como datos de instancia.

Una herramienta de

visualización genérica Podemos poner estas interfaces a trabajar en una superclase que muestra nombres
de clase precisos y da formato a todos los atributos de una instancia de cualquier clase. Abra un archivo nuevo
en su editor de texto para codificar lo siguiente: es un módulo nuevo e independiente llamado classtools.py que
implementa esa clase. Debido a que su sobrecarga de visualización de __repr__ utiliza herramientas de
introducción genéricas, funcionará en cualquier instancia, independientemente de los atributos de la instancia establecidos.
Y como se trata de una clase, automáticamente se convierte en una herramienta general de formato: gracias a
la herencia, se puede mezclar con cualquier clase que desee utilizar su formato de visualización. Como
beneficio adicional, si alguna vez queremos cambiar la forma en que se muestran las instancias, solo
necesitamos cambiar esta clase, ya que cada clase que hereda su __repr__ recogerá automáticamente el
nuevo formato cuando se ejecute la próxima vez:

# Archivo classtools.py (nuevo)


"Utilidades y herramientas de clases variadas"

clase AttrDisplay:
"""

Proporciona un método de sobrecarga de visualización heredable que


muestra las instancias con sus nombres de clase y un par nombre=valor
para cada atributo almacenado en la propia instancia (pero no los atributos
heredados de sus clases). Se puede mezclar con cualquier clase y funcionará
en cualquier instancia.
"""

def reunirAttrs(self): attrs =


[] for key in
sorted(self.__dict__): attrs.append('%s=%s'
% (key, getattr(self, key))) return ', '.join( atributos)

def __repr__(self): return


'[%s: %s]' % (self.__class__.__name__, self.gatherAttrs())

si __nombre__ == '__principal__':

clase TopTest (AttrDisplay):


cuenta = 0
def __init__(self):
self.attr1 = TopTest.count self.attr2
= TopTest.count+1 TopTest.count
+= 2

842 | Capítulo 28: Un ejemplo más realista


Machine Translated by Google

clase SubTest (TopTest): pase

X, Y = TopTest(), SubTest() imprimir(X) # Hacer dos instancias


imprimir(Y) # Mostrar todos los atributos de la instancia

# Mostrar el nombre de clase más bajo

Observe las cadenas de documentación aquí: debido a que esta es una herramienta de uso general, queremos
agregar documentación funcional para que la lean los usuarios potenciales. Como vimos en el Capítulo 15, las
cadenas de documentos se pueden colocar en la parte superior de funciones y módulos simples, y también al
comienzo de las clases y cualquiera de sus métodos; la función de ayuda y la herramienta PyDoc los extraen y
muestran automáticamente. Revisaremos las cadenas de documentación para las clases en el Capítulo 29.

Cuando se ejecuta directamente, la autocomprobación de este módulo crea dos instancias y las imprime; el
__repr__ definido aquí muestra la clase de la instancia, y todos los nombres y valores de sus atributos, en el
orden de los nombres de los atributos ordenados. Esta salida es la misma en Python 3.X y 2.X porque la
visualización de cada objeto es una sola cadena construida:

C:\código> classtools.py
[TopTest: attr1=0, attr2=1]
[Subprueba: atributo1=2, atributo2=3]

Otra nota de diseño aquí: debido a que esta clase usa __repr__ en lugar de __str__ , sus pantallas se usan en
todos los contextos, pero sus clientes tampoco tendrán la opción de proporcionar una pantalla alternativa de
bajo nivel; aún pueden agregar un __str__, pero esto se aplica solo a print y str . En una herramienta más
general, usar __str__ en su lugar limita el alcance de una pantalla, pero deja a los clientes la opción de agregar
un __repr__ para una pantalla secundaria en indicaciones interactivas y apariencias anidadas. Seguiremos
esta política alternativa cuando codifiquemos versiones ampliadas de esta clase en el Capítulo 31; para esta
demostración, nos quedaremos con el __repr__ todo incluido.

Atributos de instancia frente a clase


Si estudia el código de autodiagnóstico del módulo classtools durante el tiempo suficiente, notará que su clase
muestra solo atributos de instancia, adjuntos al objeto self en la parte inferior del árbol de herencia; eso es lo
que contiene el __dict__ de self . Como consecuencia prevista, no vemos los atributos heredados por la
instancia de las clases superiores en el árbol (p. ej., contar en el código de autoevaluación de este archivo, un
atributo de clase que se usa como contador de instancias). Los atributos de clase heredados se adjuntan solo
a la clase, no se copian a las instancias.

Si alguna vez desea incluir atributos heredados también, puede escalar el enlace __class__ a la clase de la
instancia, usar el __dict__ allí para obtener los atributos de la clase y luego iterar a través del atributo __bases__
de la clase para escalar a superclases aún más altas, repitiendo según sea necesario. Si es fanático del código
simple, ejecutar una llamada de directorio integrada en la instancia en lugar de usar __dict__ y escalar tendría
el mismo efecto, ya que los resultados de directorio incluyen nombres heredados en la lista de resultados
ordenados. En Pitón 2.7:

>>> from persona import Persona >>> # 2.X: las teclas son una lista, dir muestra menos

bob = Persona('Bob Smith')

Paso 6: Uso de herramientas de introspección | 843


Machine Translated by Google

>>> bob.__dict__.keys() # Solo atributos de instancia


['pago', 'trabajo', 'nombre']

>>> dir(bob) # Más atributos heredados


en las clases ['__doc__', '__init__', '__module__', '__repr__', 'giveRaise', 'job', 'lastName',
'name', 'pay']

Si está utilizando Python 3.X, su salida variará y puede ser más de lo que esperaba; aquí está el resultado
3.3 para las dos últimas declaraciones (el orden de la lista de claves puede variar según la ejecución):

>>> list(bob.__dict__.keys()) # 3.X teclas es una vista, no una lista


['nombre', 'trabajo', 'pago']

>>> dir(bob) # 3.X incluye métodos de tipo


de clase ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__getattribute__', '__gt__', ' __hash__', '__init__', ...más
omitidos: 31 atributos... '__setattr__', '__sizeof__', '__str__', '__subclasshook__',
'__weakref__', 'giveRaise', 'job', 'lastName', 'nombre', 'pago']

El código y la salida aquí varían entre Python 2.X y 3.X, porque dict.keys de 3.X no es una lista, y dir de 3.X
devuelve atributos de implementación de tipo de clase adicionales.
Técnicamente, dir devuelve más en 3.X porque las clases son todas de "nuevo estilo" y heredan un
gran conjunto de nombres de sobrecarga de operadores del tipo de clase. De hecho, como de
costumbre, probablemente querrá filtrar la mayoría de los nombres __X__ en el resultado del directorio
3.X , ya que son detalles de implementación internos y no algo que normalmente desee mostrar: >>>
len(dir( bob)) 31 >>> list(name for name in dir(bob) if not name.startswith('__')) ['giveRaise',
'job', 'lastName', 'name', 'pay']

En aras del espacio, dejaremos la visualización opcional de los atributos de clase heredados con subidas de
árboles o dir como experimentos sugeridos por ahora. Sin embargo, para obtener más sugerencias en este
frente, observe el trepador del árbol de herencia classtree.py que escribiremos en el Capítulo 29, y los listados
y escaladores de atributos lister.py que codificaremos en el Capítulo 31.

Consideraciones de nombres en clases de herramientas

Una última sutileza aquí: debido a que nuestra clase AttrDisplay en el módulo classtools es una herramienta
general diseñada para combinarse con otras clases arbitrarias, debemos ser conscientes del potencial de
colisiones de nombres no intencionales con las clases de clientes. Tal como está, asumí que las subclases
de clientes pueden querer usar tanto su __repr__ como la recopilación de Attrs, pero la última de ellas puede
ser más de lo que espera una subclase: si una subclase define inocentemente un nombre propio de
recopilación de Attrs , es probable que se rompa. nuestra clase, porque se usará la versión más baja en la
subclase en lugar de la nuestra.

Para ver esto por sí mismo, agregue un joinAttrs a TopTest en el código de autodiagnóstico del archivo; a
menos que el nuevo método sea idéntico o personalice intencionalmente el original, nuestra clase de herramienta

844 | Capítulo 28: Un ejemplo más realista


Machine Translated by Google

ya no funciona según lo planeado: self.gatherAttrs dentro de AttrDisplay busca de nuevo desde la instancia de
TopTest : class TopTest(AttrDisplay):

....
def collectAttrs(self): devuelve # ¡Reemplaza el método en AttrDisplay!
'Spam'

Esto no es necesariamente malo, a veces queremos que haya otros métodos disponibles para las subclases,
ya sea para llamadas directas o para la personalización de esta manera. Sin embargo, si realmente quisiéramos
proporcionar solo una __repr__ , esto es menos que ideal.

Para minimizar las posibilidades de colisiones de nombres como esta, los programadores de Python a menudo
anteponen métodos que no están destinados a uso externo con un solo guión bajo: _gatherAttrs en nuestro caso.
Esto no es infalible (¿y si otra clase también define _gatherAttrs ?), pero suele ser suficiente y es una convención
común de nomenclatura de Python para los métodos internos de una clase.

Una solución mejor y menos utilizada sería usar dos guiones bajos solo al principio del nombre del método:
__gatherAttrs para nosotros. Python expande automáticamente dichos nombres para incluir el nombre de la
clase adjunta, lo que los hace verdaderamente únicos cuando se buscan mediante la búsqueda de herencia.
Esta es una característica generalmente llamada atributos de clase pseudoprivada, que ampliaremos en el
Capítulo 31 e implementaremos en una versión ampliada de esta clase allí. Por ahora, haremos que nuestros
dos métodos estén disponibles.

Formulario Final de Nuestras Clases

Ahora, para usar esta herramienta genérica en nuestras clases, todo lo que tenemos que hacer es importarla
desde su módulo, mezclarla por herencia en nuestra clase de nivel superior y deshacernos del __repr__ más
específico que codificamos antes. El nuevo método de sobrecarga de visualización lo heredarán las instancias
de Persona, así como de Gerente; Manager obtiene __repr__ de Person, que ahora lo obtiene de AttrDisplay
codificado en otro módulo. Aquí está la versión final de nuestro archivo person.py con estos cambios aplicados:

# Archivo classtools.py
(nuevo) ...como se mencionó anteriormente...

# Archivo persona.py (final)


"""

Registrar y procesar información sobre personas.


Ejecute este archivo directamente para probar sus clases.
"""

de classtools importar AttrDisplay # Usar herramienta de visualización genérica

clase Persona(AttrDisplay): # Mezclar en una repetición a este nivel


"""

Crear y procesar registros de personas


"""

def __init__(self, nombre, trabajo=Ninguno, pago=0):


self.name = nombre self.job = trabajo self.pay =
pago

Paso 6: Uso de herramientas de introspección | 845


Machine Translated by Google

def lastName(self): # Supone que el último es el último

return self.name.split()[-1]

def giveRaise(self, percent): self.pay # El porcentaje debe ser 0..1

= int(self.pay * (1 + percent))

Gerente de clase (Persona):


"""

Una Persona personalizada con requerimientos especiales


"""

def __init__(self, name, pay):


Person.__init__(self, name, 'mgr', pay) # El nombre del trabajo está implícito

def giveRaise(self, percent, bonus=.10):


Person.giveRaise(yo, porcentaje + bonificación)

si __nombre__ == '__principal__':
bob = Persona('Bob Smith') sue
= Persona('Sue Jones', job='dev', pay=100000) print(bob)
print(sue) print(bob.lastName(), sue.lastName())
sue.giveRaise(.10) print(sue) tom = Manager('Tom Jones',
50000) tom.giveRaise(.10) print(tom.lastName()) print(tom)

Como esta es la revisión final, hemos agregado algunos comentarios aquí para documentar nuestro trabajo:
cadenas de documentación para descripciones funcionales y # para notas más pequeñas, según las
convenciones de mejores prácticas, así como líneas en blanco entre los métodos para mejorar la legibilidad.
elección de estilo cuando las clases o los métodos aumentan de tamaño, a lo que me resistí anteriormente
para estas clases pequeñas, en parte para ahorrar espacio y mantener el código más compacto.

Cuando ejecutamos este código ahora, vemos todos los atributos de nuestros objetos, no solo los que
codificamos en el __repr__ original. Y nuestro problema final está resuelto: debido a que AttrDis play elimina
los nombres de clase de la instancia propia directamente, cada objeto se muestra con el nombre de su clase
más cercana (la más baja): tom se muestra ahora como Gerente , no como Persona, y finalmente podemos
verificar que su nombre de trabajo ha sido completado correctamente por el constructor Manager : C:\code>
persona.py [Persona: trabajo=Ninguno, nombre=Bob Smith, pago=0]

[Persona: trabajo=desarrollador, nombre=Sue Jones, pago=100000]


Smith Jones
[Persona: trabajo=dev, nombre=Sue Jones, pago=110000]
jones
[Gerente: trabajo=administrador, nombre=Tom Jones, pago=60000]

Esta es la pantalla más útil que buscábamos. Sin embargo, desde una perspectiva más amplia, nuestra clase
de visualización de atributos se ha convertido en una herramienta general, que podemos mezclar en cualquier
clase por herencia para aprovechar el formato de visualización que define. Además, todos sus clientes se auto

846 | Capítulo 28: Un ejemplo más realista


Machine Translated by Google

recoger automáticamente cambios futuros en nuestra herramienta. Más adelante en el libro, conoceremos conceptos
de herramientas de clase aún más poderosos, como decoradores y metaclases; junto con las muchas herramientas de
introspección de Python, nos permiten escribir código que aumenta y administra las clases de manera estructurada y
mantenible.

Paso 7 (Final): Almacenamiento de objetos en una base de datos

En este punto, nuestro trabajo está casi completo. Ahora tenemos un sistema de dos módulos que no solo implementa
nuestros objetivos de diseño originales para representar personas, sino que también proporciona una herramienta de
visualización de atributos generales que podemos usar en otros programas en el futuro. Al codificar funciones y clases
en archivos de módulos, nos hemos asegurado de que sean compatibles con la reutilización de forma natural.
Y al codificar nuestro software como clases, nos hemos asegurado de que admita la extensión de forma natural.

Aunque nuestras clases funcionan según lo planeado, los objetos que crean no son registros reales de la base de datos.
Es decir, si eliminamos Python, nuestras instancias desaparecerán: son objetos transitorios en la memoria y no se
almacenan en un medio más permanente como un archivo, por lo que no estarán disponibles en futuras ejecuciones del
programa. Resulta que es fácil hacer que los objetos de instancia sean más permanentes, con una característica de
Python llamada persistencia de objetos: hacer que los objetos vivan después de que el programa que los crea se cierra.
Como paso final en este tutorial, hagamos que nuestros objetos sean permanentes.

Encurtidos y estantes
La persistencia de objetos se implementa mediante tres módulos de biblioteca estándar, disponibles en todos los
Pitón:

pepinillo
Serializa objetos de Python arbitrarios hacia y desde una cadena de bytes

dbm (llamado anydbm en Python 2.X)


Implementa un sistema de archivos de acceso por clave para almacenar cadenas
dejar de lado

Utiliza los otros dos módulos para almacenar objetos de Python en un archivo por clave

Conocimos estos módulos muy brevemente en el Capítulo 9 cuando estudiamos los conceptos básicos de archivos.
Proporcionan potentes opciones de almacenamiento de datos. Aunque no podemos hacerles justicia en este tutorial o
libro, son lo suficientemente simples como para que una breve introducción sea suficiente para comenzar.

El módulo pickle El

módulo pickle es una especie de herramienta supergeneral para formatear y deformatear objetos: dado un objeto de
Python casi arbitrario en la memoria, es lo suficientemente inteligente como para convertir el objeto en una cadena de
bytes, que puede usar más tarde para reconstruir el original objeto en la memoria. El módulo pickle puede manejar casi
cualquier objeto que pueda crear: listas, dic

Paso 7 (Final): Almacenamiento de objetos en una base de datos | 847


Machine Translated by Google

cionarios, combinaciones anidadas de los mismos e instancias de clase. Estas últimas son cosas especialmente
útiles para encurtir, porque proporcionan datos (atributos) y comportamiento (métodos); de hecho, la combinación
es más o menos equivalente a "registros" y "programas". Debido a que pickle es tan general, puede reemplazar
el código adicional que de otro modo podría escribir para crear y analizar representaciones de archivos de texto
personalizados para sus objetos. Al almacenar la cadena pickle de un objeto en un archivo, lo hace permanente
y persistente de manera efectiva: simplemente cárguelo y elimínelo más tarde para volver a crear el objeto
original.

El módulo de estantería

Aunque es fácil usar pickle solo para almacenar objetos en archivos planos simples y cargarlos desde allí más
tarde, el módulo shelve proporciona una capa adicional de estructura que le permite almacenar objetos
encurtidos por clave. shelve traduce un objeto a su cadena encurtida con pickle y almacena esa cadena bajo
una clave en un archivo dbm ; cuando se carga más tarde, shelve busca la cadena encurtida por clave y recrea
el objeto original en la memoria con pickle. Todo esto es un gran truco, pero para su secuencia de comandos,
una estantería2 de objetos encurtidos se ve como un diccionario: indexa por clave para buscar, asigna claves
para almacenar y usa herramientas de diccionario como len, in y dict.keys para obtener información. Los estantes
asignan automáticamente las operaciones del diccionario a los objetos almacenados en un archivo.

De hecho, para su secuencia de comandos, la única diferencia de codificación entre una estantería y un
diccionario normal es que debe abrir las estanterías inicialmente y cerrarlas después de realizar cambios.
El efecto neto es que una estantería proporciona una base de datos simple para almacenar y obtener objetos
nativos de Python por claves y, por lo tanto, los hace persistentes a lo largo de las ejecuciones del programa.
No es compatible con herramientas de consulta como SQL y carece de algunas características avanzadas que
se encuentran en las bases de datos de nivel empresarial (como el procesamiento de transacciones reales),
pero los objetos nativos de Python almacenados en una estantería pueden procesarse con todo el poder del
lenguaje Python. una vez que son recuperados por clave.

Almacenamiento de objetos en una base de datos

de estantes El decapado y los estantes son temas algo avanzados, y no entraremos en todos sus detalles aquí;
puede leer más sobre ellos en los manuales de la biblioteca estándar, así como en libros centrados en
aplicaciones, como el texto de seguimiento de Programación de Python . Sin embargo, todo esto es más simple
en Python que en inglés, así que pasemos a un poco de código.

Escribamos un nuevo script que arroje objetos de nuestras clases a un estante. En su editor de texto, abra un
nuevo archivo que llamaremos makedb.py. Dado que este es un archivo nuevo, necesitaremos importar nuestras
clases para crear algunas instancias para almacenar. Antes usamos from para cargar una clase en el indicador

interactivo, pero en realidad, al igual que con las funciones y otras variables, hay dos formas de cargar una clase
desde un archivo (los nombres de clase son variables como cualquier otra, y no tienen nada de mágico en este
contexto):

2. Sí, usamos "shelve" como sustantivo en Python, para disgusto de una variedad de editores con los que he trabajado.
a lo largo de los años, tanto electrónicos como humanos.

848 | Capítulo 28: Un ejemplo más realista


Machine Translated by Google

importar persona bob # Cargar clase con importación


= persona.Persona(...) # Ir a través del nombre del módulo

from person import Persona bob = # Cargar clase con from


Persona(...) # Usar el nombre directamente

Usaremos from para cargar nuestro script, solo porque es un poco menos para escribir. para mantener esto
simple, copie o vuelva a escribir en nuestro nuevo script las líneas de autoevaluación de person.py que hacen
instancias de nuestras clases, por lo que tenemos algo que almacenar (esta es una demostración simple, por lo que
no se preocupe por la redundancia del código de prueba aquí). Una vez que tenemos algunas instancias, es
casi trivial guardarlos en un estante. Simplemente importamos el módulo estantería, abrimos un
nuevo estante con un nombre de archivo externo, asigne los objetos a las claves en el estante y cierre
el estante cuando hayamos terminado porque hemos hecho cambios:

# Archivo makedb.py: almacene objetos Person en una base de datos archivada

de persona importar Persona, Gerente # Cargar nuestras clases

bob = Persona('Bob Smith') # Re-crear objetos para ser almacenados


sue = Persona('Sue Jones', job='dev', pay=100000)
tom = Gerente('Tom Jones', 50000)

estantería de importación

db = shelve.open('persondb') for obj in (bob, # Nombre de archivo donde se almacenan los objetos
sue, tom): db[obj.name] = obj db.close() # Usar el atributo del nombre del objeto como clave

# Almacenar objeto en estante por llave


# Cerrar después de hacer cambios

Observe cómo asignamos objetos a la estantería utilizando sus propios nombres como claves. Esto es simplemente
por conveniencia; en una estantería, la clave puede ser cualquier cadena, incluida una que podamos crear
ser único utilizando herramientas como ID de procesos y marcas de tiempo (disponibles en el sistema operativo y
módulos de biblioteca estándar de tiempo ). La única regla es que las claves deben ser cadenas y deben
ser único, ya que podemos almacenar solo un objeto por clave, aunque ese objeto puede ser una lista,
diccionario u otro objeto que contiene muchos objetos en sí mismo.

De hecho, los valores que almacenamos bajo las claves pueden ser objetos de Python de casi cualquier tipo: tipos
integrados como cadenas, listas y diccionarios, así como instancias de clases definidas por el usuario y
combinaciones anidadas de todos estos y más. Por ejemplo, el nombre y los atributos del trabajo
de nuestros objetos podrían ser diccionarios anidados y listas como en encarnaciones anteriores en este
book (aunque esto requeriría un poco de rediseño del código actual).

Eso es todo: si este script no tiene salida cuando se ejecuta, significa que probablemente
trabajó; no estamos imprimiendo nada, solo creando y almacenando objetos en un archivo
base de datos.

C:\código> makedb.py

Explorando estantes de forma interactiva

En este punto, hay uno o más archivos reales en el directorio actual cuyos nombres todos
comience con "persondb". Los archivos reales creados pueden variar según la plataforma, y al igual que en el
función abierta incorporada, el nombre de archivo en shelve.open() es relativo al trabajo actual

Paso 7 (Final): Almacenamiento de objetos en una base de datos | 849


Machine Translated by Google

directorio a menos que incluya una ruta de directorio. Dondequiera que estén almacenados, estos
archivos implementan un archivo de acceso con clave que contiene la representación encurtida de nuestros
tres objetos de Python. No elimine estos archivos: son su base de datos y es lo que necesitará copiar o
transferir cuando realice una copia de seguridad o traslade su almacenamiento.

Puede ver los archivos de estantería si lo desea, ya sea desde el Explorador de Windows o desde el shell
de Python, pero son archivos hash binarios y la mayor parte de su contenido tiene poco sentido fuera del
contexto del módulo de estantería . Con Python 3.X y sin software adicional instalado, nuestra base de
datos se almacena en tres archivos (en 2.X, es solo un archivo, persondb, porque el módulo de extensión
bsddb está preinstalado con Python para estantes; en 3.X, bsddb es un complemento opcional de código
abierto de terceros).

Por ejemplo, el módulo global de biblioteca estándar de Python nos permite obtener listados de directorios
en código de Python para verificar los archivos aquí, y podemos abrir los archivos en modo de texto o
binario para explorar cadenas y bytes:

>>> import glob >>>


glob.glob('persona*') ['persona-
compuesto.py', 'persona-departamento.py', 'persona.py', 'persona.pyc', 'personadb.bak ', 'persondb.dat',
'persondb.dir']

>>> imprimir(abrir('personabd.dir').leer())
'Sue Jones', (512, 92)
'Tom Jones', (1024, 91)
'Bob Smith', (0, 80)

>>> print(open('personadb.dat','rb').read())
b'\x80\x03cpersona\nPersona\nq\x00)\x81q\x01}q\x02(X\x03\x00\ x00\x00jobq\x03NX\x03\x00 ...más omitido...

Este contenido no es imposible de descifrar, pero puede variar en diferentes plataformas y no califica
exactamente como una interfaz de base de datos fácil de usar. Para verificar mejor nuestro trabajo,
podemos escribir otro guión o hurgar en nuestro estante en el indicador interactivo. Debido a que los
estantes son objetos de Python que contienen objetos de Python, podemos procesarlos con la sintaxis y
los modos de desarrollo normales de Python. Aquí, el indicador interactivo se convierte efectivamente en
un cliente de base de datos:

>>> importar archivar


>>> db = archivar.open('persondb') # Reabrir la estantería

>>> len(db) 3 # Tres 'registros' almacenados


>>>
lista(db.teclas()) # teclas es el índice
['Sue Jones', 'Tom Jones', 'Bob Smith'] # list() para hacer una lista en 3.X

>>> bob = db['Bob Smith'] >>> # Obtener bob por clave


bob # Ejecuta __repr__ desde AttrDisplay
[Persona: trabajo=Ninguno, nombre=Bob Smith, pago=0]

>>> bob.apellido() # Ejecuta apellido de Persona


'Herrero'

850 | Capítulo 28: Un ejemplo más realista


Machine Translated by Google

>>> para clave en db: # Iterar, buscar, imprimir


print(clave, '=>', db[clave])

Sue Jones => [Persona: trabajo=desarrollador, nombre=Sue Jones, pago=100000]


Tom Jones => [Gerente: trabajo=administrador, nombre=Tom Jones, pago=50000]
Bob Smith => [Persona: trabajo=Ninguno, nombre=Bob Smith, pago=0]

>>> for key in sorted(db): print(key,


'=>', db[key]) # Iterar por claves ordenadas

Bob Smith => [Persona: trabajo=Ninguno, nombre=Bob Smith, pago=0]


Sue Jones => [Persona: trabajo=desarrollador, nombre=Sue Jones, pago=100000]
Tom Jones => [Gerente: trabajo=administrador, nombre=Tom Jones, pago=50000]

Tenga en cuenta que no tenemos que importar nuestras clases Person o Manager aquí para cargar o usar
nuestros objetos almacenados. Por ejemplo, podemos llamar libremente al método lastName de bob y obtener su
formato de visualización de impresión personalizado automáticamente, aunque no tengamos su clase Person en
nuestro alcance aquí. Esto funciona porque cuando Python selecciona una instancia de clase, registra sus
atributos de instancia propia , junto con el nombre de la clase a partir de la cual se creó y el módulo donde vive la
clase. Cuando bob se recupera más tarde de la estantería y se elimina, Python volverá a importar automáticamente

la clase y vinculará a bob con ella.

El resultado de este esquema es que las instancias de clase adquieren automáticamente todo su comportamiento
de clase cuando se cargan en el futuro. Tenemos que importar nuestras clases solo para crear nuevas instancias,
no para procesar las existentes. Aunque es una característica deliberada, este esquema tiene consecuencias un
tanto mixtas:

• La desventaja es que las clases y los archivos de sus módulos deben poder importarse cuando se carga una
instancia más adelante. De manera más formal, las clases pickleables deben estar codificadas en el nivel
superior de un archivo de módulo accesible desde un directorio enumerado en la ruta de búsqueda del
módulo sys.path (y no deben vivir en el módulo __main__ de los archivos de secuencia de comandos
superior a menos que estén siempre en ese módulo cuando se usa). Debido a este requisito de archivo de
módulo externo, algunas aplicaciones eligen encurtir objetos más simples como diccionarios o listas,
especialmente si se van a transferir a través de Internet. • La ventaja es que los cambios en el archivo de

código fuente de una clase se recogen automáticamente cuando las instancias de la clase se vuelven a cargar;
a menudo no hay necesidad de actualizar los propios objetos almacenados, ya que actualizar el código de
su clase cambia su comportamiento.

Los estantes también tienen limitaciones bien conocidas (las sugerencias de la base de datos al final de este
capítulo mencionan algunas de ellas). Sin embargo, para el almacenamiento de objetos simples, los estantes y los
pepinillos son herramientas muy fáciles de usar.

Actualización de objetos en una estantería

Ahora, una última secuencia de comandos: escribamos un programa que actualice una instancia (registro) cada
vez que se ejecuta, para demostrar que nuestros objetos realmente son persistentes, que sus valores actuales
están disponibles cada vez que se ejecuta un programa de Python . El siguiente archivo, upda tedb.py, imprime la
base de datos y da un aumento a uno de nuestros objetos almacenados cada vez. Si

Paso 7 (Final): Almacenamiento de objetos en una base de datos | 851


Machine Translated by Google

rastreas lo que está pasando aquí, notarás que estamos obteniendo mucha utilidad

“gratis”: la impresión de nuestros objetos emplea automáticamente el método general de sobrecarga __repr__ , y damos aumentos

llamando al método giveRaise que escribimos anteriormente. Este

todo "simplemente funciona" para objetos basados en el modelo de herencia de OOP, incluso cuando viven en
un archivo:

# Archivo actualizadob.py: actualizar el objeto Persona en la base de datos

estantería de importación

db = estantería.open('personadb') # Reabrir estantería con el mismo nombre de archivo

for key in sorted(db): print(key, '\t=>', # Iterar para mostrar los objetos de la base de datos
db[key]) # Imprime con formato personalizado

sue = db['Sue Jones'] # Índice por clave para buscar


sue.giveRaise(.10) db['Sue Jones'] # Actualización en memoria usando el método de la clase
= sue db.close() # Asignar a clave para actualizar en estantería
# Cerrar después de hacer cambios

Debido a que este script imprime la base de datos cuando se inicia, debemos ejecutarlo al menos dos veces

ver cambiar nuestros objetos. Aquí está en acción, mostrando todos los registros y aumentando

Sue paga cada vez que se ejecuta (es un guión bastante bueno para Sue... algo para programar para

ejecutar regularmente como un trabajo cron tal vez?):

C:\código> actualizadob.py
Bob Smith => [Persona: trabajo=Ninguno, nombre=Bob Smith, pago=0]
Sue Jones => [Persona: trabajo=desarrollador, nombre=Sue Jones, pago=100000]
tom jones => [Gerente: trabajo=administrador, nombre=Tom Jones, pago=50000]

C:\código> actualizadob.py
Bob Smith => [Persona: trabajo=Ninguno, nombre=Bob Smith, pago=0]
Sue Jones => [Persona: trabajo=desarrollo, nombre=Sue Jones, pago=110000]
tom jones => [Gerente: trabajo=administrador, nombre=Tom Jones, pago=50000]

C:\código> actualizadob.py
Bob Smith => [Persona: trabajo=Ninguno, nombre=Bob Smith, pago=0]
Sue Jones => => [Persona: trabajo=desarrollador, nombre=Sue Jones, pago=121000]
tom jones [Gerente: trabajo=administrador, nombre=Tom Jones, pago=50000]

C:\código> actualizadob.py
Bob Smith => [Persona: trabajo=Ninguno, nombre=Bob Smith, pago=0]
Sue Jones => => [Persona: trabajo=desarrollador, nombre=Sue Jones, pago=133100]
tom jones [Gerente: trabajo=administrador, nombre=Tom Jones, pago=50000]

Una vez más, lo que vemos aquí es un producto de las herramientas Shelve y Pickle que obtenemos de Python,

y del comportamiento que codificamos en nuestras clases nosotros mismos. Y una vez más, podemos verificar

el trabajo de nuestra secuencia de comandos en el indicador interactivo, el equivalente de la estantería de un cliente de base de datos:

C:\código> python
>>> estantería de importación
>>> db = archivar.open('personadb') >>> rec = # Reabrir base de datos
db['Sue Jones'] # Obtener objeto por clave
>>> grabar

[Persona: trabajo=desarrollador, nombre=Sue Jones, pago=146410]

852 | Capítulo 28: Un ejemplo más realista


Machine Translated by Google

>>> rec.apellido()
'Jones'
>>> rec.pago
146410

Para ver otro ejemplo de persistencia de objetos en este libro, consulte la barra lateral en el Capítulo 31
titulado "Por qué le importará: clases y persistencia" en la página 941. Almacena un objeto compuesto algo más
grande en un archivo plano con pickle en lugar de estantería, pero el efecto
es similar. Para obtener más detalles y ejemplos tanto para pickles como para estantes, consulte también el Capítulo
9 (conceptos básicos de archivo) y el Capítulo 37 (cambios de herramientas de cadena 3.X), otros libros y los manuales
de Python.

Direcciones futuras
Y eso es un final para este tutorial. En este punto, has visto todos los conceptos básicos de Python.
Maquinaria OOP en acción, y ha aprendido formas de evitar la redundancia y sus problemas de mantenimiento
asociados en su código. Has creado clases con todas las funciones que hacen real
trabajar. Como beneficio adicional, los ha convertido en registros reales de la base de datos almacenándolos en
una estantería de Python, por lo que su información vive de forma persistente.

Hay mucho más que podríamos explorar aquí, por supuesto. Por ejemplo, podríamos extender
nuestras clases para hacerlas más realistas, agregarles nuevos tipos de comportamiento, etc.
Dar un aumento, por ejemplo, debería en la práctica verificar que las tasas de aumento salarial estén entre
cero y uno: una extensión que agregaremos cuando conozcamos a los decoradores más adelante en este libro. Tú

también podría convertir este ejemplo en una base de datos de contactos personales, cambiando el estado
información almacenada en los objetos, así como los métodos de las clases utilizados para procesarla. Bien
deja este ejercicio sugerido abierto a tu imaginación.

También podríamos expandir nuestro alcance para usar herramientas que vienen con Python o son gratuitas.
disponible en el mundo de código abierto:

GUI

Tal como está, solo podemos procesar nuestra base de datos con la interfaz basada en comandos del indicador
interactivo y las secuencias de comandos. También podríamos trabajar en la expansión de la usabilidad de
nuestra base de datos de objetos agregando una interfaz gráfica de usuario de escritorio para navegar y
actualizar sus registros. Las GUI se pueden construir de forma portátil con tkinter de Python
(Tkinter en 2.X) soporte de biblioteca estándar o kits de herramientas de terceros como WxPython
y PyQt. tkinter se envía con Python, le permite crear GUI simples rápidamente y es
ideal para aprender técnicas de programación GUI; WxPython y PyQt tienden a ser
más complejos de usar, pero a menudo producen GUI de mayor calidad al final.
sitios web

Aunque las GUI son convenientes y rápidas, la Web es difícil de superar en términos de accesibilidad. También
podríamos implementar un sitio web para navegar y actualizar registros,
en lugar de o además de las GUI y el aviso interactivo. Los sitios web pueden ser
construido con herramientas básicas de secuencias de comandos CGI que vienen con Python, o marcos web
de terceros con todas las funciones, como Django, TurboGears, Pylons,

Direcciones futuras | 853


Machine Translated by Google

web2Py, Zope o App Engine de Google. En la Web, sus datos aún pueden almacenarse en una
estantería, un archivo pickle u otro medio basado en Python; los scripts que lo procesan simplemente
se ejecutan automáticamente en un servidor en respuesta a las solicitudes de los navegadores web
y otros clientes, y producen HTML para interactuar con un usuario, ya sea directamente o interactuando
con las API del marco. Los sistemas Rich Internet Application (RIA) como Silverlight y Pajamas
también intentan combinar la interactividad similar a la GUI con la implementación basada en la web.

servicios web
Aunque los clientes web a menudo pueden analizar la información de las respuestas de los sitios web
(una técnica conocida como "screen scraping"), podríamos ir más allá y proporcionar una forma más
directa de obtener registros en la web a través de una interfaz de servicios web como SOAP o XML.
-Llamadas RPC: API admitidas por Python mismo o por el dominio de código abierto de terceros, que
generalmente asignan datos hacia y desde el formato XML para su transmisión. Para las secuencias
de comandos de Python, dichas API devuelven datos más directamente que el texto incrustado en el
HTML de una página de respuesta.
Bases de
datos Si nuestra base de datos se vuelve crítica o de mayor volumen, eventualmente podríamos
moverla de los estantes a un mecanismo de almacenamiento más completo, como el sistema de base
de datos orientado a objetos (OODB) ZODB de código abierto, o una base de datos relacional basada
en SQL más tradicional. sistema como MySQL, Oracle o PostgreSQL. Python en sí viene con el
sistema de base de datos SQLite en proceso incorporado, pero otras opciones de código abierto
están disponibles gratuitamente en la Web. ZODB, por ejemplo, es similar a la estantería de Python,
pero aborda muchas de sus limitaciones, admite mejor bases de datos más grandes, actualizaciones
simultáneas, procesamiento de transacciones y escritura automática en cambios en la memoria (las
estanterías pueden almacenar objetos en caché y vaciarlos en el disco en el momento del cierre con
su opción de reescritura , pero esto tiene limitaciones: vea otros recursos). Los sistemas basados en
SQL como MySQL ofrecen herramientas de nivel empresarial para el almacenamiento de bases de
datos y se pueden usar directamente desde un script de Python. Como vimos en el Capítulo 9,
MongoDB ofrece un enfoque alternativo que almacena documentos JSON, que son muy parecidos a
los diccionarios y listas de Python, y son neutrales en términos de lenguaje, a diferencia de los datos pickle .
ORM
Si migramos a un sistema de base de datos relacional para el almacenamiento, no tenemos que
sacrificar las herramientas OOP de Python. Los mapeadores relacionales de objetos (ORM) como
SQLObject y SQLAlchemy pueden mapear automáticamente tablas y filas relacionales hacia y desde
clases e instancias de Python, de modo que podamos procesar los datos almacenados usando la
sintaxis de clase de Python normal. Este enfoque proporciona una alternativa a los OODB como
shelve y ZODB y aprovecha el poder de las bases de datos relacionales y la clase de Python.
modelo.

Si bien espero que esta introducción le abra el apetito para futuras exploraciones, todos estos temas están,
por supuesto, más allá del alcance de este tutorial y de este libro en general. Si desea explorar cualquiera
de ellos por su cuenta, consulte la Web, los manuales de la biblioteca estándar de Python y los libros
centrados en aplicaciones, como Programación de Python. En este último yo

854 | Capítulo 28: Un ejemplo más realista


Machine Translated by Google

retome este ejemplo donde nos detuvimos aquí, que muestra cómo agregar una GUI y un sitio web en la
parte superior de la base de datos para permitir la exploración y actualización de registros de instancias.
Espero verte allí eventualmente, pero primero, volvamos a los fundamentos de la clase y terminemos el resto
de la historia central del lenguaje Python.

Resumen del capítulo

En este capítulo, exploramos todos los fundamentos de las clases de Python y la programación orientada a
objetos en acción, a partir de un ejemplo simple pero real, paso a paso. Agregamos constructores, métodos,
sobrecarga de operadores, personalización con subclases y herramientas basadas en la introspección, y
encontramos otros conceptos como composición, delegación y polimorfismo en el camino.

Al final, tomamos los objetos creados por nuestras clases y los hicimos persistentes almacenándolos en una
base de datos de objetos archivados, un sistema fácil de usar para guardar y recuperar objetos nativos de
Python por clave. Mientras exploramos los conceptos básicos de la clase, también encontramos varias
formas de factorizar nuestro código para reducir la redundancia y minimizar los costos de mantenimiento futuros.
Finalmente, analizamos brevemente formas de extender nuestro código con herramientas de programación
de aplicaciones, como GUI y bases de datos, cubiertas en libros de seguimiento.

En los próximos capítulos de esta parte del libro, regresaremos a nuestro estudio de los detalles detrás del
modelo de clases de Python e investigaremos su aplicación a algunos de los conceptos de diseño usados
para combinar clases en programas más grandes. Sin embargo, antes de continuar, analicemos el
cuestionario de este capítulo para repasar lo que cubrimos aquí. Dado que ya hemos realizado mucho trabajo
práctico en este capítulo, cerraremos con un conjunto de preguntas orientadas principalmente a la teoría
diseñadas para que pueda rastrear parte del código y reflexionar sobre algunas de las ideas más importantes
detrás de él.

Pon a prueba tus conocimientos: Cuestionario

1. Cuando buscamos un objeto Manager del estante y lo imprimimos, ¿de dónde viene la lógica del formato
de visualización?

2. Cuando recuperamos un objeto Person de un estante sin importar su módulo, ¿cómo sabe el objeto que
tiene un método giveRaise al que podemos llamar?

3. ¿Por qué es tan importante trasladar el procesamiento a los métodos, en lugar de codificarlo fuera de la
clase?

4. ¿Por qué es mejor personalizar mediante subclases en lugar de copiar el original y


modificando?

5. ¿Por qué es mejor volver a llamar a un método de superclase para ejecutar acciones predeterminadas?
de copiar y modificar su código en una subclase?

6. ¿Por qué es mejor usar herramientas como __dict__ que permiten procesar objetos de forma
nericamente que escribir más código personalizado para cada tipo de clase?

Pon a prueba tus conocimientos: Cuestionario | 855


Machine Translated by Google

7. En términos generales, ¿cuándo elegiría utilizar la incorporación y composición de objetos en lugar de la herencia?

8. ¿Qué tendría que cambiar si los objetos codificados en este capítulo usaran un diccionario de nombres y una lista
de trabajos, como en ejemplos similares anteriores en este libro?

9. ¿Cómo podría modificar las clases de este capítulo para implementar una base de datos de contactos personales en
Python?

Pon a prueba tus conocimientos: respuestas

1. En la versión final de nuestras clases, Manager finalmente hereda su método de impresión __repr__ de AttrDisplay
en el módulo classtools separado y dos niveles más arriba en el árbol de clases. Manager no tiene uno, por lo que
la búsqueda de herencia sube a su superclase Person ; porque tampoco hay __repr__ allí, la búsqueda sube más
alto y lo encuentra en AttrDisplay. Los nombres de clase enumerados entre paréntesis en la línea de encabezado
de una declaración de clase proporcionan los enlaces a superclases superiores.

2. Los estantes (en realidad, el módulo pickle que usan) vuelven a vincular automáticamente una instancia a la clase
desde la que se creó cuando esa instancia se vuelve a cargar en la memoria.
Python vuelve a importar la clase desde su módulo internamente, crea una instancia con sus atributos almacenados
y establece el enlace __class__ de la instancia para que apunte a su clase original.
De esta manera, las instancias cargadas obtienen automáticamente todos sus métodos originales (como lastName,
giveRaise y __repr__), incluso si no hemos importado la clase de la instancia a nuestro alcance.

3. Es importante mover el procesamiento a los métodos para que solo haya una copia para cambiar en el futuro y para
que los métodos se puedan ejecutar en cualquier instancia. Esta es la noción de encapsulación de Python: resumir
la lógica detrás de las interfaces para admitir mejor el mantenimiento futuro del código. Si no lo hace, crea una
redundancia de código que puede multiplicar su esfuerzo de trabajo a medida que el código evoluciona en el futuro.

4. La personalización con subclases reduce el esfuerzo de desarrollo. En OOP, programamos personalizando lo que ya
se ha hecho, en lugar de copiar o cambiar el código existente. Esta es la verdadera "gran idea" en OOP, ya que
podemos ampliar fácilmente nuestro trabajo anterior mediante la codificación de nuevas subclases, podemos
aprovechar lo que ya hemos hecho. Esto es mucho mejor que comenzar desde cero cada vez o introducir varias
copias redundantes de código que quizás deban actualizarse en el futuro.

5. Copiar y modificar código duplica su esfuerzo de trabajo potencial en el futuro, independientemente del contexto. Si
una subclase necesita realizar acciones predeterminadas codificadas en un método de superclase, es mucho mejor
volver a llamar al original a través del nombre de la superclase que copiar su código. Esto también es cierto para
los constructores de superclases.
Una vez más, copiar código crea redundancia, que es un problema importante a medida que evoluciona el código.

6. Las herramientas genéricas pueden evitar soluciones codificadas que deben mantenerse sincronizadas con el resto
de la clase a medida que evoluciona con el tiempo. Un método de impresión __repr__ genérico , por ejemplo, no
necesita actualizarse cada vez que se agrega un nuevo atributo a las instancias en un

856 | Capítulo 28: Un ejemplo más realista


Machine Translated by Google

__init__ constructor. Además, un método de impresión genérico heredado por todas las clases
aparece y debe modificarse en un solo lugar: cambios en la versión genérica
son recogidos por todas las clases que heredan de la clase genérica. De nuevo, eliminando
la redundancia de código reduce el esfuerzo de desarrollo futuro; ese es uno de los principales activos
clases traen a la mesa.

7. La herencia es mejor para codificar extensiones basadas en la personalización directa (como nuestro
Gerente especialidad de Persona). La composición es adecuada para escenarios donde
varios objetos se agregan en un todo y son dirigidos por una clase de capa de controlador.
La herencia pasa las llamadas a la reutilización y la composición pasa al delegado.
La herencia y la composición no se excluyen mutuamente; a menudo, los objetos incrustados en un controlador
son en sí mismos personalizaciones basadas en la herencia.

8. No mucho ya que este fue realmente un prototipo de primer corte, pero el método lastName
tendría que actualizarse para el nuevo formato de nombre; el constructor Person sería
cambiar el trabajo predeterminado a una lista vacía; y la clase Manager probablemente
necesita pasar una lista de trabajos en su constructor en lugar de una sola cadena (autoprueba
el código también cambiaría, por supuesto). La buena noticia es que estos cambios
deben hacerse en un solo lugar: en nuestras clases, donde se encapsulan tales detalles. Los scripts de la base
de datos deberían funcionar como están, ya que los estantes admiten datos anidados arbitrariamente.

9. Las clases de este capítulo podrían usarse como código de “plantilla” repetitivo para
implementar una variedad de tipos de bases de datos. Esencialmente, puede reutilizarlos
modificando los constructores para registrar diferentes atributos y proporcionando cualquier
método que sea apropiado para la aplicación de destino. Por ejemplo, puede usar atributos
como nombre, dirección, cumpleaños, teléfono, correo electrónico, etc. para un contacto .
base de datos, y los métodos apropiados para este fin. Un método llamado sendmail,
por ejemplo, podría usar el módulo smptlib de la biblioteca estándar de Python para enviar un correo electrónico
a uno de los contactos automáticamente cuando se le llama (consulte los manuales de Python o los libros de
nivel de aplicación para obtener más detalles sobre dichas herramientas). La herramienta AttrDisplay que escribimos
aquí podría usarse textualmente para imprimir sus objetos, porque es intencionalmente
genérico. La mayor parte del código de la base de datos de estantería aquí se puede usar para almacenar sus objetos,
también, con cambios menores.

Pon a prueba tus conocimientos: respuestas | 857


Machine Translated by Google
Machine Translated by Google

CAPÍTULO 29

Detalles de codificación de clase

Si aún no ha obtenido todo Python OOP, no se preocupe; Ahora que hemos tenido un primer recorrido,
profundizaremos un poco más y estudiaremos los conceptos presentados anteriormente con más detalle.
En este capítulo y en el siguiente, echaremos otro vistazo a las mecánicas de clase. Aquí, vamos a
estudiar clases, métodos y herencia, formalizar y expandir algunas de las ideas de codificación presentadas
en el Capítulo 27. Debido a que la clase es nuestra última herramienta de espacio de nombres, también
resumiremos los conceptos de espacio de nombres y alcance de Python.

El siguiente capítulo continúa este segundo paso en profundidad sobre la mecánica de clase al cubrir un
aspecto específico: la sobrecarga del operador. Además de presentar detalles adicionales, este capítulo
y el siguiente también nos dan la oportunidad de explorar algunas clases más grandes que las que hemos
estudiado hasta ahora.

Nota de contenido: si ha estado leyendo linealmente, parte de este capítulo será una revisión y un
resumen de los temas presentados en el estudio de caso del capítulo anterior, revisado aquí por temas
de lenguaje con ejemplos más pequeños y autónomos para lectores nuevos en OOP. Otros pueden verse
tentados a saltarse parte de este capítulo, pero asegúrese de ver la cobertura del espacio de nombres
aquí, ya que explica algunas sutilezas en el modelo de clases de Python.

La declaración de clase
Aunque la declaración de clase de Python puede parecer similar a las herramientas en otros lenguajes
OOP en la superficie, en una inspección más cercana, es bastante diferente de lo que algunos
programadores están acostumbrados. Por ejemplo, como en C++, la declaración de clase es la principal
herramienta OOP de Python, pero a diferencia de C++, la clase de Python no es una declaración. Al igual
que una definición, una instrucción de clase es un generador de objetos y una asignación implícita: cuando
se ejecuta, genera un objeto de clase y almacena una referencia a él en el nombre utilizado en el
encabezado. También como una definición, una declaración de clase es un verdadero código ejecutable:
su clase no existe hasta que Python alcanza y ejecuta la declaración de clase que la define. Esto suele
ocurrir al importar el módulo en el que está codificado, pero no antes.

859
Machine Translated by Google

formulario general

clase es una declaración compuesta, con un cuerpo de declaraciones típicamente sangradas que aparecen
debajo del encabezado. En el encabezado, las superclases se enumeran entre paréntesis después de la clase.
nombre, separados por comas. Enumerar más de una superclase conduce a una herencia múltiple, que discutiremos más
formalmente en el Capítulo 31. Aquí está la declaración
forma general:

nombre de la clase (superclase,...): # Asignar a nombre


attr = valor def método (self,...): # Datos de clase compartidos

self.attr = valor # Métodos


# Datos por instancia

Dentro de la declaración de clase , cualquier asignación genera atributos de clase, y especialmente


los métodos con nombre sobrecargan a los operadores; por ejemplo, una función llamada __init__ se llama
en el momento de la construcción del objeto de instancia, si está definido.

Ejemplo

Como hemos visto, las clases son en su mayoría solo espacios de nombres, es decir, herramientas para definir nombres.
(es decir, atributos) que exportan datos y lógica a los clientes. Una declaración de clase define efectivamente un espacio
de nombres. Al igual que en un archivo de módulo, las declaraciones anidadas en una declaración de clase
cuerpo crea sus atributos. Cuando Python ejecuta una declaración de clase (no una llamada a un
clase), ejecuta todas las declaraciones en su cuerpo, de arriba a abajo. Tareas que
Durante este proceso, se crean nombres en el ámbito local de la clase, que se convierten en atributos en el objeto de clase
asociado. Debido a esto, las clases se parecen a ambos módulos .
y funciones:

• Al igual que las funciones, las declaraciones de clase son ámbitos locales donde los nombres creados por anidados
asignaciones en vivo.

• Al igual que los nombres en un módulo, los nombres asignados en una instrucción de clase se convierten en atributos
en un objeto de clase.

La principal distinción de las clases es que sus espacios de nombres también son la base de la herencia en Python; los
atributos de referencia que no se encuentran en una clase o un objeto de instancia son
tomado de otras clases.

Debido a que la clase es una declaración compuesta, cualquier tipo de declaración se puede anidar dentro de su
cuerpo: impresión, asignaciones, if, def, etc. Todas las sentencias dentro de la sentencia de clase se
ejecutan cuando se ejecuta la sentencia de clase (no cuando se llama a la clase más tarde para hacer
una instancia). Por lo general, las declaraciones de asignación dentro de la declaración de clase hacen que los datos
atributos y definiciones anidadas hacen atributos de método. En general, sin embargo, cualquier tipo de
la asignación de nombre en el nivel superior de una declaración de clase crea un atributo con el mismo nombre
del objeto de clase resultante.

Por ejemplo, las asignaciones de objetos simples sin función a atributos de clase producen
atributos de datos, compartidos por todas las instancias:

860 | Capítulo 29: Detalles de codificación de clases


Machine Translated by Google

>>> clase SharedData:


correo no deseado = 42
# Genera un atributo de datos de clase

>>> x = Datos # Hacer dos instancias


Compartidos() >>> y =
Datos Compartidos() >>> x.spam, y.spam
# Heredan y comparten 'spam' (también conocido como SharedData.spam)
(42, 42)

Aquí, debido a que el nombre spam se asigna en el nivel superior de una declaración de clase , se adjunta a la clase y, por lo
tanto, será compartido por todas las instancias. Podemos cambiarlo pasando por el nombre de la clase, y podemos referirnos a
él a través de instancias o la clase: 1 >>> SharedData.spam = 99 >>> x.spam, y.spam, SharedData.spam (99, 99, 99)

Dichos atributos de clase se pueden usar para administrar información que abarca todas las
instancias, por ejemplo, un contador del número de instancias generadas (ampliaremos esta
idea con un ejemplo en el Capítulo 32). Ahora, observe lo que sucede si asignamos el nombre
spam a través de una instancia en lugar de la clase:
>>> x.spam = 88
>>> x.spam, y.spam, SharedData.spam (88,
99, 99)

Las asignaciones a atributos de instancia crean o cambian los nombres en la instancia, en lugar
de en la clase compartida. De manera más general, las búsquedas de herencia ocurren solo en
las referencias de atributos, no en la asignación: la asignación al atributo de un objeto siempre
cambia ese objeto y ningún otro.2 Por ejemplo, y.spam se busca en la clase por herencia, pero
la asignación a x .spam adjunta un nombre a x mismo.

Aquí hay un ejemplo más completo de este comportamiento que almacena el mismo nombre en dos lugares. Supongamos que
ejecutamos la siguiente clase: # Definir clase # Asignar atributo de clase # Asignar nombre de método # Asignar atributo de
class MixedNames: instancia
data = 'spam' def
__init__(self, value): self.data = value

def display(self):
print(self.data, MixedNames.data) # Atributo de instancia, atributo de clase

1. Si ha usado C++, puede reconocer esto como similar a la noción de miembros de datos “estáticos” de C++: miembros que
se almacenan en la clase, independientemente de las instancias. En Python, no es nada especial: todos los atributos de
clase son solo nombres asignados en la instrucción de clase , ya sea que se trate de funciones de referencia (los "métodos"
de C++) o de otra cosa (los "miembros" de C++). En el Capítulo 32, también conoceremos los métodos estáticos de Python
(similares a los de C++), que son simplemente funciones independientes que generalmente procesan atributos de clase.

2. A menos que la clase haya redefinido la operación de asignación de atributos para hacer algo único con el método de
sobrecarga del operador __setattr__ (discutido en el Capítulo 30), o use herramientas de atributo avanzadas como
propiedades y descriptores (discutido en el Capítulo 32 y el Capítulo 38). Gran parte de este capítulo presenta el caso
normal, que es suficiente en este punto del libro, pero como veremos más adelante, los ganchos de Python permiten que
los programas se desvíen de la norma con frecuencia.

La declaración de clase | 861


Machine Translated by Google

Esta clase contiene dos definiciones, que vinculan atributos de clase a funciones de método. También
contiene una declaración de asignación = ; debido a que esta asignación asigna los datos del nombre
dentro de la clase, vive en el ámbito local de la clase y se convierte en un atributo del objeto de la clase.
Como todos los atributos de clase, estos datos son heredados y compartidos por todas las instancias de
la clase que no tienen atributos de datos propios.

Cuando creamos instancias de esta clase, los datos del nombre se adjuntan a esas instancias mediante la
asignación a self.data en el método constructor:
>>> x = NombresMixtos(1) # Hacer dos objetos de
instancia >>> y = MixedNames(2) # Cada uno tiene sus
propios datos >>> x.display(); y.display() # self.data difiere, MixedNames.data es el mismo
1 spam 2 spam

El resultado neto es que los datos viven en dos lugares: en los objetos de instancia (creados por la
asignación self.data en __init__) y en la clase de la que heredan los nombres (creada por la asignación de
datos en la clase). El método de visualización de la clase imprime ambas versiones, calificando primero la
instancia propia y luego la clase.

Mediante el uso de estas técnicas para almacenar atributos en diferentes objetos, determinamos su
alcance de visibilidad. Cuando se adjuntan a las clases, los nombres se comparten; en instancias, los
nombres registran datos por instancia, no comportamiento o datos compartidos. Aunque las búsquedas de
herencia nos buscan nombres, siempre podemos llegar a un atributo en cualquier parte de un árbol
accediendo directamente al objeto deseado.

En el ejemplo anterior, por ejemplo, especificar x.data o self.data devolverá un nombre de instancia, que
normalmente oculta el mismo nombre en la clase; sin embargo, Mixed Names.data toma la versión del
nombre de la clase explícitamente. La siguiente sección describe uno de los roles más comunes para
dichos patrones de codificación y explica más sobre la forma en que lo implementamos en el capítulo
anterior.

Métodos
Como ya conoce las funciones, también conoce los métodos en las clases.
Los métodos son solo objetos de función creados por sentencias def anidadas en el cuerpo de una
sentencia de clase . Desde una perspectiva abstracta, los métodos proporcionan comportamiento para
que los objetos de instancia hereden. Desde una perspectiva de programación, los métodos funcionan
exactamente de la misma manera que las funciones simples, con una excepción crucial: el primer
argumento de un método siempre recibe el objeto de instancia que es el sujeto implícito de la llamada al método.

En otras palabras, Python mapea automáticamente las llamadas de métodos de instancia a las funciones de métodos de
una clase de la siguiente manera. Llamadas a métodos realizadas a través de una instancia, como esta:

instancia.método(args...)

se traducen automáticamente a llamadas de función de método de clase de esta forma:

class.method(instancia, argumentos...)

862 | Capítulo 29: Detalles de codificación de clases


Machine Translated by Google

donde Python determina la clase localizando el nombre del método usando la herencia
procedimiento de busqueda. De hecho, ambas formas de llamada son válidas en Python.

Además de la herencia normal de los nombres de los atributos de los métodos, el primer argumento especial
es la única magia real detrás de las llamadas a métodos. En el método de una clase, el primer argumento es
usualmente llamado self por convención (técnicamente, solo su posición es significativa, no su
nombre). Este argumento proporciona métodos con un gancho de vuelta a la instancia que es el
tema de la llamada—debido a que las clases generan muchos objetos de instancia, necesitan usar
este argumento para administrar datos que varían según la instancia.

Los programadores de C++ pueden reconocer el autoargumento de Python como similar al de C++.
este puntero. Sin embargo, en Python, self siempre es explícito en su código: los métodos deben
siempre vaya a través de uno mismo para obtener o cambiar los atributos de la instancia que se está procesando
por la llamada al método actual. Esta naturaleza explícita del yo es por diseño: la presencia de
este nombre hace que sea obvio que está utilizando nombres de atributos de instancia en su secuencia de comandos,
no nombres en el ámbito local o global.

Ejemplo de método
Para aclarar estos conceptos, pasemos a un ejemplo. Supongamos que definimos lo siguiente
clase:

clase SiguienteClase: # Definir clase


def impresora(auto, texto): # Definir método
auto.mensaje = texto # Cambiar instancia
imprimir(auto.mensaje) # Instancia de acceso

El nombre impresora hace referencia a un objeto de función; debido a que está asignado en el ámbito de la declaración de
clase , se convierte en un atributo de objeto de clase y es heredado por cada instancia realizada
de la clase Normalmente, debido a que los métodos como la impresora están diseñados para procesar instancias, los
llamamos a través de instancias:

>>> x = NextClass() >>> # Hacer instancia


x.printer('llamada de instancia') llamada de # Llamar a su método
instancia
>>> x.message # Instancia cambiada
'llamada de instancia'

Cuando llamamos al método calificando una instancia como esta, la impresora se ubica primero por
herencia, y luego su argumento propio se asigna automáticamente al objeto de instancia
(X); el argumento de texto obtiene la cadena pasada en la llamada ('llamada de instancia'). Darse cuenta de
porque Python pasa automáticamente el primer argumento a sí mismo por nosotros, en realidad solo
tiene que pasar en un argumento. Dentro de la impresora, el nombre self se usa para acceder o configurar
datos por instancia porque hace referencia a la instancia que se está procesando actualmente.

Sin embargo, como hemos visto, los métodos pueden llamarse de dos formas: a través de una instancia oa través de la
clase misma. Por ejemplo, también podemos llamar a la impresora yendo
a través del nombre de la clase, siempre que pasemos una instancia al argumento self explícitamente:

Métodos | 863
Machine Translated by Google

>>> NextClass.printer(x, 'llamada de clase') llamada de # Llamada de clase directa


clase

>>> x.mensaje # Instancia cambiada nuevamente


'llamada de clase'

Las llamadas enrutadas a través de la instancia y la clase tienen exactamente el mismo efecto, siempre que
pasemos el mismo objeto de instancia nosotros mismos en el formulario de clase. De hecho, de forma
predeterminada, recibe un mensaje de error si intenta llamar a un método sin ninguna instancia:

>>> NextClass.printer('llamada incorrecta')


TypeError: se debe llamar al método independiente printer() con la instancia de NextClass...

Llamar a los constructores de superclases

Los métodos normalmente se llaman a través de instancias. Sin embargo, las llamadas a métodos a través de
una clase aparecen en una variedad de funciones especiales. Un escenario común implica el método
constructor. El método __init__ , como todos los atributos, se busca por herencia. Esto significa que en el
momento de la construcción, Python localiza y llama solo a un __init__. Si los constructores de subclases
necesitan garantizar que la lógica de tiempo de construcción de la superclase también se ejecute, generalmente
deben llamar al método __init__ de la superclase explícitamente a través de la clase: class Super: def
__init__(self, x): ...código predeterminado...

clase Sub(Super): def


__init__(self, x, y):
Super.__init__(uno mismo, x) # Ejecutar superclase __init__
...código personalizado... # Hacer mis acciones de inicio

Yo = Sub(1, 2)

Este es uno de los pocos contextos en los que es probable que su código llame directamente a un operador
sobre el método de carga. Naturalmente, debe llamar al constructor de la superclase de esta manera solo si
realmente desea que se ejecute; sin la llamada, la subclase lo reemplaza por completo.
Para una ilustración más realista de esta técnica en acción, vea el ejemplo de la clase Manager en el tutorial
del capítulo anterior.3

Otras posibilidades de llamada de método

Este patrón de llamar a métodos a través de una clase es la base general para extender, en lugar de reemplazar
por completo, el comportamiento del método heredado. Requiere que se pase una instancia explícita porque
todos los métodos lo hacen de forma predeterminada. Técnicamente, esto se debe a que los métodos son
métodos de instancia en ausencia de un código especial.

3. En una nota relacionada, también puede codificar varios métodos __init__ dentro de la misma clase, pero solo se usará la
última definición; consulte el Capítulo 31 para obtener más detalles sobre las definiciones de métodos múltiples.

864 | Capítulo 29: Detalles de codificación de clases


Machine Translated by Google

En el Capítulo 32, también conoceremos una opción más nueva agregada en Python 2.2, métodos
estáticos, que le permiten codificar métodos que no esperan objetos de instancia en sus primeros argumentos.
Dichos métodos pueden actuar como funciones simples sin instancias, con nombres que son locales para
las clases en las que están codificados y pueden usarse para administrar datos de clase. Un concepto
relacionado que veremos en el mismo capítulo, el método de clase, recibe una clase cuando se llama en
lugar de una instancia y puede usarse para administrar datos por clase, y está implícito en las metaclases.

Sin embargo, estas son extensiones avanzadas y generalmente opcionales. Normalmente, una instancia
siempre se debe pasar a un método, ya sea automáticamente cuando se llama a través de una instancia o
manualmente cuando se llama a través de una clase.

Según la barra lateral "¿Qué hay de súper?" en la página 831 del Capítulo 28, Python
también tiene una función súper integrada que permite volver a llamar a los métodos
de una súper clase de manera más genérica, pero postergaremos su presentación
hasta el Capítulo 32 debido a sus desventajas y complejidades. Consulte la barra
lateral mencionada anteriormente para obtener más detalles; esta llamada tiene
compensaciones bien conocidas en el uso básico y un caso de uso avanzado
esotérico que requiere un despliegue universal para ser más efectivo. Debido a estos
problemas, este libro prefiere llamar a las superclases por su nombre explícito en
lugar de super como política; si es nuevo en Python, le recomiendo el mismo enfoque
por ahora, especialmente para su primer paso por OOP. Aprenda la manera simple
ahora, para que pueda compararlo con otros más tarde.

Herencia
Por supuesto, el objetivo del espacio de nombres creado por la declaración de clase es admitir la herencia
de nombres. Esta sección amplía algunos de los mecanismos y funciones de la herencia de atributos en
Python.

Como hemos visto, en Python, la herencia ocurre cuando se califica un objeto e implica buscar en un árbol
de definición de atributos, uno o más espacios de nombres. Cada vez que usa una expresión de la forma
object.attr donde object es una instancia o un objeto de clase, Python busca el árbol de espacio de nombres
de abajo hacia arriba, comenzando con object, buscando el primer atributo que puede encontrar. Esto
incluye referencias a atributos propios en sus métodos.
Debido a que las definiciones más bajas en el árbol reemplazan a las más altas, la herencia forma la base
de la especialización.

Construcción del árbol de atributos

La figura 29-1 resume la forma en que los árboles de espacios de nombres se construyen y se llenan con
nombres. En general:

• Los atributos de instancia se generan mediante asignaciones a atributos propios en métodos. • Los

atributos de clase se crean mediante sentencias (asignaciones) en sentencias de clase .

Herencia | 865
Machine Translated by Google

• Los enlaces de superclase se crean enumerando las clases entre paréntesis en una declaración de clase
encabezamiento.

El resultado neto es un árbol de espacios de nombres de atributos que conduce desde una instancia, a la
clase desde la que se generó, a todas las superclases enumeradas en el encabezado de la clase . Python
busca hacia arriba en este árbol, desde instancias hasta superclases, cada vez que usa la calificación para
obtener un nombre de atributo de un objeto de instancia.4

Figura 29-1. El código del programa crea un árbol de objetos en la memoria para ser buscados por herencia de atributos.

Llamar a una clase crea una nueva instancia que recuerda su clase, ejecutar una declaración de clase crea una nueva clase y las superclases se

enumeran entre paréntesis en el encabezado de la declaración de clase. Cada referencia de atributo desencadena una nueva búsqueda de árbol de abajo

hacia arriba, incluso referencias a atributos propios dentro de los métodos de una clase.

Especialización de métodos heredados El

modelo de herencia de búsqueda de árboles que acabamos de describir resulta ser una excelente forma de
especializar sistemas. Debido a que la herencia encuentra nombres en las subclases antes de verificar las
superclases, las subclases pueden reemplazar el comportamiento predeterminado al redefinir sus superclases.

4. Aquí hay dos puntos importantes: primero, esta descripción no está completa al 100 %, porque también podemos crear atributos
de clase e instancia asignándolos a objetos fuera de las declaraciones de clase , pero ese es un enfoque mucho menos común
y, a veces, más propenso a errores ( los cambios no están aislados a las declaraciones de clase ). En Python, todos los atributos
siempre están accesibles de forma predeterminada. Hablaremos más sobre la privacidad de nombres de atributos en el Capítulo
30 cuando estudiemos __setattr__, en el Capítulo 31 cuando conozcamos __X nombres, y nuevamente en el Capítulo 39, donde
lo implementaremos con un decorador de clase.

En segundo lugar, como también se señaló en el Capítulo 27, la historia completa de la herencia se vuelve más complicada
cuando se agregan a la mezcla temas avanzados como metaclases y descriptores , y por este motivo postergamos una definición
formal hasta el Capítulo 40 . Sin embargo, en el uso común, es simplemente una forma de redefinir y, por lo tanto, personalizar
el comportamiento codificado en las clases.

866 | Capítulo 29: Detalles de codificación de clases


Machine Translated by Google

atributos De hecho, puede construir sistemas completos como jerarquías de clases, que puede ampliar
agregando nuevas subclases externas en lugar de cambiar la lógica existente.

La idea de redefinir los nombres heredados conduce a una variedad de técnicas de especialización.
Por ejemplo, las subclases pueden reemplazar los atributos heredados por completo, proporcionar atributos
que una superclase espera encontrar y ampliar los métodos de la superclase volviendo a llamar a la
superclase desde un método anulado. Ya hemos visto algunos de estos patrones en acción; aquí hay un
ejemplo autónomo de extensión en el trabajo:

>>> class Super: def


método(self): print('in
Super.method')

>>> class Sub(Super): def


method(self): # Método de anulación
print('starting Sub.method') # Añadir acciones aquí
Super.método(auto) # Ejecutar acción predeterminada

print('finalizando Sub.método')

Las llamadas directas a métodos de superclase son el quid de la cuestión aquí. La clase Sub reemplaza la
función de método de Super con su propia versión especializada, pero dentro del reemplazo, Sub vuelve a
llamar a la versión exportada por Super para llevar a cabo el comportamiento predeterminado. En otras
palabras, Sub.method simplemente extiende el comportamiento de Super.method , en lugar de reemplazarlo
por completo: >>> x = Super() >>> x.method() en Super.method

# Crea una instancia de Super


# Ejecuta el método Super.

>>> x = Sub() >>> # Crear una subinstancia #


x.método() Ejecuta Sub.método, llama a Super.método
comenzando Sub.método en
Super.método finalizando
Sub.método

Este patrón de codificación de extensión también se usa comúnmente con constructores; consulte la
sección “Métodos” en la página 862 para ver un ejemplo.

Class Interface Techniques Extension

es solo una forma de interactuar con una superclase. El archivo que se muestra en esta sección,
specialize.py, define varias clases que ilustran una variedad de técnicas comunes:

Super
Define una función de método y un delegado que espera una acción en una subclase.
Inheritor
No proporciona ningún nombre nuevo, por lo que obtiene todo lo definido en Super.
Replacer
Anula el método de Super con una versión propia.

Herencia | 867
Machine Translated by Google

Extender

Personaliza el método de Super anulando y volviendo a llamar para ejecutar el método predeterminado.
Proveedor

Implementa el método de acción esperado por el método de delegado de Super .

Estudie cada una de estas subclases para tener una idea de las diversas formas en que personalizan su
superclase común. Aquí está el archivo:
class Super: def
method(self): print('in
Super.method') def delegar(self): # Comportamiento por defecto

self.action()
# Se espera que sea definido

Heredero de clase (Super): pasar # Heredar método palabra por palabra

class Replacer(Super): def # Reemplazar el método por completo


method(self): print('in
Replacer.method')

class Extender(Super): def # Extender el comportamiento del método

method(self): print('starting
Extender.method')
Super.method(self)
print('ending Extender.method')

clase Proveedor (Super): def # Complete un método requerido


acción (auto): imprimir ('en
Proveedor.acción')

if __name__ == '__main__': for klass


in (Heredero, Reemplazo, Extensor): print('\n' + klass.__name__ +
'...') klass().method() print('\nProvider.. .') x = Proveedor()
x.delegado()

Vale la pena señalar algunas cosas aquí. Primero, observe cómo el código de autoevaluación al final de
este ejemplo crea instancias de tres clases diferentes en un bucle for . Debido a que las clases son
objetos, puede almacenarlas en una tupla y crear instancias de forma genérica sin sintaxis adicional (más
sobre esta idea más adelante). Las clases también tienen el atributo especial __name__ , como los
módulos; está preestablecido en una cadena que contiene el nombre en el encabezado de la clase. Esto
es lo que sucede cuando ejecutamos el archivo:
% python especializarse.py

Heredero... en
Super.método

Reemplazo...
en el método Reemplazo

868 | Capítulo 29: Detalles de codificación de clases


Machine Translated by Google

Extender...
iniciando Extender.method en
Super.method terminando
Extender.method

Proveedor...
en Provider.action

Superclases abstractas De

las clases del ejemplo anterior, Provider puede ser la más crucial de comprender. Cuando llamamos al
método de delegado a través de una instancia de Provider , ocurren dos búsquedas de herencia
independientes:

1. En la llamada inicial de x.delegate , Python encuentra el método de delegado en Super buscando en la


instancia de Provider y superior. La instancia x se pasa al argumento propio del método como de
costumbre.

2. Dentro del método Super.delegate , self.action invoca una búsqueda nueva, independiente en herencia,
de uno mismo y superior. Debido a que self hace referencia a una instancia de Provider , el método de
acción se encuentra en la subclase Provider .

Este tipo de estructura de codificación de "llenar los espacios en blanco" es típico de los marcos OOP. En
un contexto más realista, el método rellenado de esta manera podría manejar un evento en una GUI,
proporcionar datos para representarlos como parte de una página web, procesar el texto de una etiqueta en
un archivo XML, etc.; su subclase proporciona acciones específicas , pero el marco maneja el resto del
trabajo general.

Al menos en términos del método de delegado , la superclase en este ejemplo es lo que a veces se llama
una superclase abstracta: una clase que espera que sus subclases proporcionen partes de su
comportamiento. Si un método esperado no está definido en una subclase, Python genera una excepción
de nombre indefinido cuando falla la búsqueda de herencia.

Los codificadores de clase a veces hacen que los requisitos de la subclase sean más obvios con
declaraciones de afirmación o al generar la excepción NotImplementedError integrada con declaraciones de
aumento . Estudiaremos en profundidad las sentencias que pueden desencadenar excepciones en la
siguiente parte de este libro; como vista previa rápida, aquí está el esquema de aserción en acción:

clase Super:
def delegar(auto):
auto.acción() def
acción(auto): afirmar
Falso, '¡la acción debe definirse!' # Si esta versión se llama

>>> X = Súper()
>>> X.delegado()
AssertionError: ¡la acción debe definirse!

Nos encontraremos con afirmar en el Capítulo 33 y el Capítulo 34; en resumen, si su primera expresión se
evalúa como falsa, genera una excepción con el mensaje de error proporcionado. Aquí, la expresión es

Herencia | 869
Machine Translated by Google

siempre falso para activar un mensaje de error si un método no se redefine, y la herencia localiza la versión
aquí. Alternativamente, algunas clases simplemente lanzan una excepción NotImplemen tedError
directamente en tales stubs de métodos para señalar el error:

class Super:
def delegar(self):
self.action() def
action(self): raise
NotImplementedError('¡la acción debe ser definida!')

>>> X = Súper()
>>> X.delegado()
NotImplementedError: ¡la acción debe definirse!

Para instancias de subclases, aún obtenemos la excepción a menos que la subclase proporcione el
método esperado para reemplazar el predeterminado en la superclase: >>> class Sub(Super): pass

>>> X = Sub()
>>> X.delegado()
NotImplementedError: ¡la acción debe definirse!

>>> class Sub(Super):


def action(self): print('spam')

>>> X = Sub()
>>> X.delegate()
spam

Para ver un ejemplo algo más realista de los conceptos de esta sección en acción, consulte el ejercicio
"Jerarquía de animales del zoológico" (Ejercicio 8) al final del Capítulo 32, y su solución en la "Parte VI,
Clases y OOP" en el Apéndice D. Tales taxonomías son una forma tradicional de presentar OOP, pero
están un poco alejados de las descripciones de trabajo de la mayoría de los desarrolladores (¡con disculpas
a los lectores que trabajan en el zoológico!).

Superclases abstractas en Python 3.X y 2.6+: Vista

previa A partir de Python 2.6 y 3.0, las superclases abstractas de la sección anterior (también conocidas
como "clases base abstractas"), que requieren que las subclases completen los métodos, también pueden
implementarse con clases especiales sintaxis. La forma en que codificamos esto varía ligeramente según la versión.
En Python 3.X, usamos un argumento de palabra clave en un encabezado de clase , junto con una sintaxis
especial de decorador @ , las cuales estudiaremos en detalle más adelante en este libro:

de abc importar ABCMeta, método abstracto

class Super(metaclass=ABCMeta):
@abstractmethod def
method(self, ...): pasar

Pero en Python 2.6 y 2.7, usamos un atributo de clase en su lugar:

870 | Capítulo 29: Detalles de codificación de clases


Machine Translated by Google

class Super:
__metaclass__ = ABCMeta
@abstractmethod def
method(self, ...): pasar

De cualquier manera, el efecto es el mismo: no podemos crear una instancia a menos que el método se defina más abajo
en el árbol de clases. En 3.X, por ejemplo, aquí está la sintaxis especial equivalente al ejemplo de la sección anterior:

>>> desde abc importar ABCMeta, método abstracto


>>>
>>> class Super(metaclass=ABCMeta): def
delegar(self): self.action()
@abstractmethod def
action(self): pasar

>>> X = Súper()
TypeError: no se puede crear una instancia de clase abstracta Super con acción de métodos abstractos

>>> clase Sub(Super): pasar

>>> X = Sub()
TypeError: no se puede instanciar la clase abstracta Sub con la acción de métodos abstractos

>>> class Sub(Super): def


action(self): print('spam')

>>> X = Sub()
>>> X.delegate()
spam

Codificada de esta manera, no se puede instanciar una clase con un método abstracto (es decir, no podemos crear una
instancia llamándola) a menos que todos sus métodos abstractos se hayan definido en subclases. Aunque esto requiere
más código y conocimientos adicionales, la ventaja potencial de este enfoque es que se emiten errores por métodos
faltantes cuando intentamos crear una instancia de la clase, no más tarde cuando intentamos llamar a un método faltante.
Esta característica también se puede usar para definir una interfaz esperada, automáticamente

verificado en las clases de cliente.

Desafortunadamente, este esquema también se basa en dos herramientas de lenguaje avanzado que aún no conocemos :
los decoradores de funciones, presentados en el Capítulo 32 y cubiertos en profundidad en el Capítulo 39, así como las
declaraciones de metaclases, mencionadas en el Capítulo 32 y cubiertas en el Capítulo 40, por lo que vamos a refinar
otras facetas de esta opción aquí. Consulte los manuales estándar de Python para obtener más información sobre esto,
así como las superclases abstractas precodificadas que proporciona Python.

Herencia | 871
Machine Translated by Google

Espacios de nombres: la conclusión


Ahora que hemos examinado los objetos de clase e instancia, la historia del espacio de nombres de Python está
completa. Como referencia, resumiré rápidamente todas las reglas utilizadas para resolver nombres aquí.
Lo primero que debe recordar es que los nombres calificados y no calificados se tratan de manera diferente, y que
algunos ámbitos sirven para inicializar espacios de nombres de objetos:

• Los nombres no calificados (p. ej., X) se ocupan de los

ámbitos. • Los nombres de atributos calificados (p. ej., object.X) utilizan espacios de

nombres de objetos. • Algunos ámbitos inicializan espacios de nombres de objetos (para módulos y clases).

Estos conceptos a veces interactúan; en object.X, por ejemplo, el objeto se busca por ámbitos y luego X se busca
en los objetos de resultado. Dado que los ámbitos y los espacios de nombres son esenciales para comprender el
código de Python, resumamos las reglas con más detalle.

Nombres simples: Global a menos que se asigne

Como hemos aprendido, los nombres simples no calificados siguen la regla de alcance léxico LEGB descrita
cuando exploramos las funciones en el Capítulo 17:

Asignación (X = valor)
Hace que los nombres sean locales de forma predeterminada: crea o cambia el nombre X en el ámbito local
actual, a menos que se declare global (o no local en 3.X).

Referencia (X)
Busca el nombre X en el ámbito local actual, luego todas y cada una de las funciones adjuntas, luego el
ámbito global actual, luego el ámbito integrado, según la regla LEGB.
Las clases adjuntas no se buscan: los nombres de clase se obtienen como atributos de objeto en su lugar.

También según el Capítulo 17, algunas construcciones de casos especiales localizan aún más los nombres (p. ej.,
variables en algunas comprensiones y cláusulas de declaración de prueba ), pero la gran mayoría de los nombres
siguen la regla LEGB.

Nombres de atributos: espacios de nombres de

objetos También hemos visto que los nombres de atributos calificados se refieren a atributos de objetos específicos
y obedecen las reglas para módulos y clases. Para objetos de clase e instancia, las reglas de referencia se amplían
para incluir el procedimiento de búsqueda de herencia:

Asignación (objeto.X = valor)


Crea o modifica el nombre de atributo X en el espacio de nombres del objeto que se está calificando, y nada
más. La escalada del árbol de herencia ocurre solo en la referencia de atributos, no en la asignación de
atributos.

872 | Capítulo 29: Detalles de codificación de clases


Machine Translated by Google

Referencia (objeto.X)
Para objetos basados en clases, busca el nombre de atributo X en el objeto, luego en todas las clases
accesibles por encima de él, utilizando el procedimiento de búsqueda de herencia. Para objetos que
no son de clase, como módulos, obtiene X directamente del objeto .

Como se señaló anteriormente, lo anterior captura el caso normal y típico. Estas reglas de atributos pueden
variar en las clases que utilizan herramientas más avanzadas, especialmente para las clases de estilo
nuevo: una opción en 2.X y el estándar en 3.X, que exploraremos en el Capítulo 32. Por ejemplo, la
herencia de referencia puede ser más rico de lo implícito aquí cuando se implementan metaclases, y las
clases que aprovechan las herramientas de administración de atributos como propiedades, descriptores y
__setattr__ pueden interceptar y enrutar asignaciones de atributos arbitrariamente.

De hecho, también se ejecuta algo de herencia en la asignación, para ubicar descriptores con un método
__set__ en clases de nuevo estilo; tales herramientas anulan las reglas normales tanto para la referencia
como para la asignación. Exploraremos en profundidad las herramientas de administración de atributos en
el Capítulo 38, y formalizaremos la herencia y su uso de descriptores en el Capítulo 40. Por ahora, la
mayoría de los lectores deben enfocarse en las reglas normales dadas aquí, que cubren la mayoría del
código de aplicación de Python.

El “Zen” de los espacios de nombres: las asignaciones clasifican los

nombres Con distintos procedimientos de búsqueda para nombres calificados y no calificados, y múltiples
capas de búsqueda para ambos, a veces puede ser difícil saber adónde irá un nombre. En Python, el lugar
donde asigna un nombre es crucial: determina completamente el ámbito u objeto en el que residirá un
nombre. El archivo manynames.py ilustra cómo este principio se traduce en código y resume las ideas de
espacios de nombres que hemos visto a lo largo de este libro (sin incluir los alcances de casos especiales
oscuros como las comprensiones):

# Archivo muchosnombres.py

X = 11 # Nombre/ atributo global (módulo) (X, o manynames.X)

def f():
imprimir(X) # Acceso global X (11)

definición g():
X = 22 # Variable local (función) (X, oculta el módulo X)
imprimir (X)

clase C: X
= 33 def # Atributo de clase (CX)
m(self): X = 44
self.X = 55 # Variable local en método (X)
# Atributo de instancia (instancia.X)

Este archivo asigna el mismo nombre, X, cinco veces; es ilustrativo, ¡aunque no es exactamente la mejor
práctica! Sin embargo, debido a que este nombre se asigna en cinco ubicaciones diferentes, las cinco X en
este programa son variables completamente diferentes. De arriba a abajo, las asignaciones a X aquí
generan: un atributo de módulo (11), una variable local en una función (22), una clase

espacios de nombres: la conclusión | 873


Machine Translated by Google

atributo (33), una variable local en un método (44) y un atributo de instancia (55). A pesar de que
los cinco se denominan X, el hecho de que todos están asignados en diferentes lugares en la fuente
código o a diferentes objetos hace que todas estas variables sean únicas.

Debería tomarse el tiempo para estudiar este ejemplo cuidadosamente porque recopila ideas que hemos
estado explorando a lo largo de las últimas partes de este libro. Cuando tiene sentido para ti,
habrá alcanzado la iluminación del espacio de nombres de Python. O bien, puede ejecutar el código
y vea lo que sucede: aquí está el resto de este archivo fuente, que crea una instancia e imprime todas las X
que puede obtener:

# muchosnombres.py, continuación

si __nombre__ == '__principal__':
imprimir(X) # 11: módulo (también conocido como archivo externo manynames.X)
f() g() #11: mundial
imprimir(X) #22: locales

# 11: nombre del módulo sin cambios

obj = C() # Hacer instancia

print(obj.X) # 33: nombre de clase heredado por instancia

obj.m() # Adjunte el nombre de atributo X a la instancia ahora


imprimir(obj.X) # 55: instancia

imprimir(CX) # 33: clase (también conocido como obj.X si no hay X en la instancia)

#imprimir(CmX) # FALLA: solo visible en el método


#imprimir(gX) # FALLA: solo visible en función

Los resultados que se imprimen cuando se ejecuta el archivo se anotan en los comentarios del código;
rastree a través de ellos para ver a qué variable llamada X se accede cada vez. Aviso
en particular que podemos pasar por la clase para buscar su atributo (CX), pero podemos
nunca obtenga variables locales en funciones o métodos desde fuera de sus declaraciones de definición .
Los locales son visibles solo para otro código dentro de la definición y, de hecho, solo viven en la memoria
mientras se ejecuta una llamada a la función o método.

Algunos de los nombres definidos por este archivo también son visibles fuera del archivo para otros módulos,
pero recuerde que siempre debemos importar antes de poder acceder a los nombres en otro archivo—
la segregación de nombres es el punto principal de los módulos, después de todo:

# otroarchivo.py

importar muchos nombres

X = 66

print(X) #66: el global aquí


print(muchosnombres.X) # 11: los globales se convierten en atributos después de las importaciones

muchosnombres.f() #11: la X de manynames, no la de aquí!


muchosnombres.g() # 22: función local en otro archivo

imprimir(muchosnombres.CX) # 33: atributo de clase en otro módulo


I = muchosnombres.C()
imprimir (IX) #33: todavía de clase aquí

874 | Capítulo 29: Detalles de codificación de clases


Machine Translated by Google

Im ()
imprimir (IX) #55: ¡ahora de instancia!

Observe aquí cómo manynames.f() imprime la X en manynames, no la X asignada en este archivo: los ámbitos
siempre están determinados por la posición de las asignaciones en su código fuente (es decir, léxicamente) y
nunca están influenciados por qué importa qué o quién importa quién.
Además, tenga en cuenta que la propia X de la instancia no se crea hasta que llamamos a Im(): los atributos,
como todas las variables, surgen cuando se asignan, y no antes. Normalmente creamos atributos de instancia
asignándolos en los métodos constructores de clase __init__ , pero esta no es la única opción.

Finalmente, como aprendimos en el Capítulo 17, también es posible que una función cambie nombres fuera
de sí misma, con declaraciones globales y (en Python 3.X) no locales ; estas declaraciones brindan acceso de
escritura, pero también modifican las reglas de vinculación del espacio de nombres de la asignación:
X = 11 # Global en módulo

def g1():
imprimir(X) # Referencia global en módulo (11)

def g2():
global X
X = 22 # Cambiar global en módulo

definición h1():
X = 33 # Local en funcion
def anidado():
imprimir(X) # Referencia local en ámbito envolvente (33)

definición h2():
X = 33 # Local en funcion
def anidado(): X
no local # Declaración de Python
X = 44 3.X # Cambiar local en el ámbito adjunto

Por supuesto, generalmente no debe usar el mismo nombre para cada variable en su secuencia de comandos,
pero como demuestra este ejemplo, incluso si lo hace, los espacios de nombres de Python funcionarán para
evitar que los nombres usados en un contexto choquen accidentalmente con los usados en otro.

Clases anidadas: revisión de la regla de los ámbitos LEGB El

ejemplo anterior resumió el efecto de las funciones anidadas en los ámbitos, que estudiamos en el Capítulo
17. Resulta que las clases también se pueden anidar: un patrón de codificación útil en algunos tipos de
programas, con implicaciones de ámbito que se derivan naturalmente de lo que ya sabes, pero que pueden no
ser obvios en el primer encuentro. Esta sección ilustra el concepto con un ejemplo.

Aunque normalmente están codificadas en el nivel superior de un módulo, las clases a veces también aparecen
anidadas en funciones que las generan, una variación del tema de la "función de fábrica" (también conocida
como cierre) del Capítulo 17, con funciones de retención de estado similares. Allí notamos

espacios de nombres: la conclusión | 875


Machine Translated by Google

que las declaraciones de clase introducen nuevos ámbitos locales de forma muy parecida a las declaraciones de definición de función,

que siguen la misma regla de búsqueda de alcance LEGB que las definiciones de funciones.

Esta regla se aplica tanto al nivel superior de la propia clase como al nivel superior de
funciones de método anidadas dentro de él. Ambos forman la capa L en esta regla: son normales
ámbitos locales, con acceso a sus nombres, nombres en cualquier función adjunta, globales en
el módulo envolvente y los elementos integrados. Al igual que los módulos, el alcance local de la clase se transforma en
un espacio de nombres de atributo después de ejecutar la declaración de clase .

Aunque las clases tienen acceso a los ámbitos de las funciones adjuntas, no actúan
como ámbitos adjuntos al código anidado dentro de la clase: Python busca funciones adjuntas
para los nombres de referencia, pero nunca para las clases adjuntas. Es decir, una clase es un ámbito local .
y tiene acceso a los ámbitos locales adjuntos, pero no sirve como ámbito local adjunto
a más código anidado. Porque la búsqueda de nombres usados en funciones de método salta
la clase envolvente, los atributos de clase deben obtenerse como atributos de objeto usando inheri
tancia

Por ejemplo, en la siguiente función de anidado , todas las referencias a X se enrutan al global
alcance excepto el último, que recoge una redefinición de alcance local (el código de la sección está en
archivo classscope.py, y el resultado de cada ejemplo se describe en sus dos últimos comentarios):
X=1

def nester():
imprimir # Globales: 1
(X) clase C:
print(X) def # Globales: 1
método1(self):
print(X) def # Globales: 1
método2(self):
X=3 # Oculta global
imprimir(X) # Locales: 3
yo = c()
I.método1()
I.método2()

imprimir # Globales: 1
(X) nester # Descanso: 1, 1, 1, 3
() imprimir ('-' * 40)

Sin embargo, observe lo que sucede cuando reasignamos el mismo nombre en una función anidada
capas: las redefiniciones de X crean locales que ocultan aquellos en ámbitos adjuntos, al igual que para
funciones anidadas simples; la capa de clase envolvente no cambia esta regla, y de hecho
es irrelevante para ello:

X=1

def nester():
X=2 # Oculta global
imprimir(X) # locales: 2
clase C:
imprimir (X) # En definición adjunta (nester): 2

876 | Capítulo 29: Detalles de codificación de clases


Machine Translated by Google

def método1(auto):
print(X) def # En definición adjunta (nester): 2
método2(self):
X=3 # Oculta encerrando (nester)
imprimir (X) # Locales: 3
yo = c()
I.método1()
I.método2()

imprimir (X) # Globales: 1


nester () # Descanso: 2, 2, 2, 3
imprimir ('-' * 40)

Y esto es lo que sucede cuando reasignamos el mismo nombre en varias paradas a lo largo del
manera: las asignaciones en los ámbitos locales de funciones y clases ocultan globales o cierran funciones locales
del mismo nombre, independientemente del anidamiento involucrado:
X=1

def nester():
X=2 # Oculta global
imprimir(X) # locales: 2
clase C:
X=3 # Class local hides nester's: CX o IX (sin alcance)
imprimir(X) # Locales: 3
def metodo1(auto):
imprimir (X) # En definición adjunta (¡no 3 en clase!): 2
print(self.X) # Clase local heredada: 3
def método2(auto):
X=4 # Oculta envolvente (nester, no clase)
imprimir(X) # Locales: 4
self.X = 5 # Oculta clase
print(self.X) # Ubicado en la instancia: 5
yo = c()
I.método1()
I.método2()

imprimir (X) # Globales: 1


nester () # Descanso: 2, 3, 2, 3, 4, 5
imprimir ('-' * 40)

Lo que es más importante, las reglas de búsqueda para nombres simples como X nunca buscan encerrando
declaraciones de clase : solo definiciones, módulos e integrados (¡es la regla LEGB, no CLEGB!).
En el método 1, por ejemplo, X se encuentra en una definición fuera de la clase envolvente que tiene el mismo
nombre en su ámbito local. Para llegar a los nombres asignados en la clase (por ejemplo, métodos), debemos
obténgalos como atributos de objeto de clase o instancia, a través de self.X en este caso.

Lo crea o no, veremos casos de uso para este patrón de codificación de clases anidadas más adelante en este
libro, especialmente en algunos de los decoradores del Capítulo 39 . En este rol, la función envolvente
por lo general, ambos sirven como una fábrica de clases y proporcionan un estado retenido para su uso posterior en el
clase adjunta o sus métodos.

espacios de nombres: la conclusión | 877


Machine Translated by Google

Diccionarios de espacio de nombres: Revisión

En el Capítulo 23, aprendimos que los espacios de nombres de módulos tienen una implementación concreta como
diccionarios, expuestos con el atributo __dict__ incorporado . En el Capítulo 27 y el Capítulo 28, aprendimos que lo
mismo se aplica a los objetos de clase e instancia—atributo
la calificación es principalmente una operación de indexación de diccionario interna, y la herencia de atributos es en gran
medida una cuestión de búsqueda en diccionarios vinculados. De hecho, dentro de Python, los objetos de instancia y
clase son en su mayoría solo diccionarios con enlaces entre ellos. Pitón
expone estos diccionarios, así como sus enlaces, para su uso en roles avanzados (por ejemplo, para
herramientas de codificación).

Pusimos a trabajar algunas de estas herramientas en el capítulo anterior, pero para resumir y ayudar
mejor entiende cómo funcionan los atributos internamente, trabajemos a través de un interactivo
session que rastrea la forma en que crecen los diccionarios de espacio de nombres cuando las clases están involucradas.
Ahora que sabemos más sobre métodos y superclases, también podemos embellecer el
cobertura aquí para una mejor vista. Primero, definamos una superclase y una subclase con métodos que almacenarán
datos en sus instancias:

>>> clase Súper:


def hola(yo):
self.data1 = 'correo no deseado'

>>> Clase Sub(Súper):


def hola(auto):
self.data2 = 'huevos'

Cuando creamos una instancia de la subclase, la instancia comienza con un vacío


diccionario de espacio de nombres, pero tiene enlaces a la clase para la búsqueda de herencia a
seguir. De hecho, el árbol de herencia está explícitamente disponible en atributos especiales, que
puedes inspeccionar. Las instancias tienen un atributo __class__ que enlaza con su clase, y las clases tienen un atributo
__bases__ que es una tupla que contiene enlaces a superclases superiores (estoy
ejecutando esto en Python 3.3; sus formatos de nombre, atributos internos y órdenes clave pueden
variar):

>>> X = Sub()
>>> X.__dict__ {} # Dict de espacio de nombres de instancia

>>> X.__clase__ # Clase de instancia


<clase '__principal__.Sub'>
>>> Sub.__bases__ # Superclases de clase
(<clase '__main__.Super'>,)
>>> Super.__bases__ # () tupla vacía en Python 2.X
(<clase 'objeto'>,)

A medida que las clases se asignan a atributos propios , rellenan los objetos de instancia, es decir, los atributos terminan
en los diccionarios de espacios de nombres de atributos de las instancias, no en los de las clases.
El espacio de nombres de un objeto de instancia registra datos que pueden variar de una instancia a otra,
y self es un gancho en ese espacio de nombres:

>>> Y = Sub()

878 | Capítulo 29: Detalles de codificación de clases


Machine Translated by Google

>>> X.hola()
>>> X.__dict__
{'datos1': 'correo no deseado'}

>>> X.hola()
>>> X.__dict__
{'datos2': 'huevos', 'datos1': 'correo no deseado'}

>>> list(Sub.__dict__.keys())
['__qualname__', '__module__', '__doc__', 'hola'] >>>
list(Super.__dict__.keys()) ['__module__', 'hola ', '__dict__',
'__qualname__', '__doc__', '__weakref__']

>>> Y.__dict__ {}

Observe los nombres de guiones bajos adicionales en los diccionarios de clase; Python los configura
automáticamente y podemos filtrarlos con las expresiones generadoras que vimos en los capítulos 27 y
28 que no repetiremos aquí. La mayoría no se usan en los programas típicos, pero hay herramientas que
usan algunos de ellos (p. ej., __doc__ contiene las cadenas de documentación discutidas en el Capítulo
15).

Además, observe que Y, una segunda instancia creada al comienzo de esta serie, todavía tiene un
diccionario de espacio de nombres vacío al final, aunque el diccionario de X se haya llenado con
asignaciones en los métodos. Nuevamente, cada instancia tiene un diccionario de espacios de nombres
independiente, que comienza vacío y puede registrar atributos completamente diferentes a los registrados
por los diccionarios de espacios de nombres de otras instancias de la misma clase.

Debido a que los atributos son en realidad claves de diccionario dentro de Python, en realidad hay dos formas de obtener
y asignar sus valores: por calificación o por indexación clave: >>> X.data1, X.__dict__['data1'] ('spam', ' correo no

deseado')

>>> X.data3 = 'brindis'


>>> X.__dict__
{'datos2': 'huevos', 'datos3': 'brindis', 'datos1': 'spam'}

>>> X.__dict__['datos3'] = 'jamón'


>>> X.data3
'jamón'

Sin embargo, esta equivalencia solo se aplica a los atributos realmente adjuntos a la instancia .
Debido a que la calificación de obtención de atributos también realiza una búsqueda de herencia, puede
acceder a los atributos heredados que la indexación del diccionario de espacio de nombres no puede. El
atributo heredado X.hello, por ejemplo, no puede ser accedido por X.__dict__['hello'].

Experimente con estos atributos especiales por su cuenta para tener una mejor idea de cómo los espacios
de nombres realmente hacen su negocio de atributos. También intente ejecutar estos objetos a través de
la función dir que conocimos en los dos capítulos anteriores: dir(X) es similar a X.__dict__.keys(), pero dir
ordena su lista e incluye algunos heredados e integrados en

espacios de nombres: la conclusión | 879


Machine Translated by Google

tributos. Incluso si nunca los usará en los tipos de programas que escribe, ver que son solo
diccionarios normales puede ayudar a solidificar los espacios de nombres en general.

En el Capítulo 32, también aprenderemos acerca de las ranuras, una nueva característica de clase de estilo

algo avanzada que almacena atributos en instancias, pero no en sus diccionarios de espacios de nombres. Es

tentador tratarlos como atributos de clase y, de hecho, aparecen en espacios de nombres de clase donde

administran los valores por instancia. Sin embargo, como veremos, las ranuras pueden evitar que se cree un

__dict__ en la instancia por completo, un potencial que las herramientas genéricas a veces deben tener en

cuenta mediante el uso de herramientas independientes del almacenamiento, como dir y getattr.

Vínculos de espacio de nombres: un


trepador de árboles La sección anterior demostró los atributos especiales de instancia y clase
__class__ y __bases__ , sin explicar realmente por qué podrían interesarle. En resumen, estos
atributos le permiten inspeccionar las jerarquías de herencia dentro de su propio código. Por
ejemplo, se pueden usar para mostrar un árbol de clases, como en el siguiente ejemplo de
Python 3.X y 2.X:
#!pitón
"""

classtree.py: sube árboles de herencia usando enlaces de espacios de


nombres, mostrando superclases más altas con sangría para la altura
"""

def classtree(cls, indent): print('.' *


indent + cls.__name__) # Imprime aquí el nombre de la clase para
supercls en cls.__bases__: # Recurre a todas lasvisitar
superclases
super >#una
Puede
vez
classtree(superclases, sangría+3)

def árbol de instancia(inst):


print('Árbol de %s' % inst) árbol # Mostrar instancia
de clase(inst.__clase__, 3) # Sube a su clase

def selftest(): clase


A: pasar la clase B(A):
pasar la clase C(A):
pasar la clase D(B,C):
pasar la clase E: pasar
la clase F(D,E): pasar
el
árbol de instancia(B()) )
árbol de instancia (F ())

si __nombre__ == '__principal__': autodiagnóstico()

La función classtree en este script es recursiva: imprime el nombre de una clase usando
__name__, luego sube a las superclases llamándose a sí misma. Esto permite que la función
atraviese árboles de clases con formas arbitrarias; la recursividad sube a la cima y se detiene en

880 | Capítulo 29: Detalles de codificación de clases


Machine Translated by Google

superclases raíz que tienen atributos __bases__ vacíos . Cuando se usa la recursividad, cada nivel activo de una función
obtiene su propia copia del ámbito local; aquí, esto significa que cls y sangría son diferentes en cada nivel de classtree .

La mayor parte de este archivo es código de autodiagnóstico. Cuando se ejecuta de forma independiente en Python 2.X,
crea un árbol de clases vacío, crea dos instancias a partir de él e imprime sus estructuras de árbol de clases:

C:\code> c:\python27\python classtree.py Árbol


de <__main__.B instancia en 0x00000000022C3A88> ...B ......A

Árbol de <__main__.F instancia en 0x00000000022C3A88>


...F

......D .........B ............A .........C .......... ..A


......MI

Cuando se ejecuta en Python 3.X, el árbol incluye la superclase de objeto implícita que se agrega automáticamente
sobre las clases raíz independientes (es decir, las más altas), porque todas las clases tienen un "nuevo estilo" en 3.X;
más información sobre este cambio en el Capítulo 32 : C:\code> c:\python33\python classtree.py Árbol de

<__main__.selftest.<locals>.B objeto en 0x00000000029216A0> ...B ......A

.........objeto Árbol
de <__main__.selftest.<locales>.F objeto en
0x00000000029216A0> ...F ......D .........B ... .........A ...............objeto .........C ............A

...............objeto ......E .........objeto

Aquí, la sangría marcada por puntos se usa para indicar la altura del árbol de clase. Por supuesto, podríamos
mejorar este formato de salida y tal vez incluso dibujarlo en una pantalla GUI. Sin embargo, aun así, podemos
importar estas funciones en cualquier lugar donde queramos una visualización rápida de un árbol de clase físico: C:
\code> c:\python33\python >>> class Emp: pass

>>> clase Persona(Emp): pasar

>>> bob = Persona()

>>> import classtree


>>> classtree.instancetree(bob)

espacios de nombres: la conclusión | 881


Machine Translated by Google

Árbol de <__main__.Persona objeto en


0x000000000298B6D8> ...Persona ......Emp .........objeto

Independientemente de si alguna vez codificará o usará tales herramientas, este ejemplo demuestra una de las muchas
formas en que puede hacer uso de atributos especiales que exponen las funciones internas del intérprete. Verá otra cuando
codifiquemos las herramientas de visualización de clases de uso general de lister.py en la sección “Herencia múltiple:
Clases “ mixtas” del Capítulo 31 ” en la página 956 ; allí, ampliaremos esta técnica para mostrar también atributos en cada
una de ellas. objeto en un árbol de clases y funcionan como una superclase común.

En la última parte de este libro, revisaremos tales herramientas en el contexto de la creación de herramientas de Python en
general, para codificar herramientas que implementan privacidad de atributos, validación de argumentos y más.
Si bien no está en la descripción del trabajo de todos los programadores de Python, el acceso a las funciones internas
permite herramientas de desarrollo poderosas.

Cadenas de documentación revisadas


El ejemplo de la última sección incluye una cadena de documentación para su módulo, pero recuerde que las cadenas de
documentación también se pueden usar para componentes de clase. Las docstrings, que cubrimos en detalle en el Capítulo
15, son cadenas literales que aparecen en la parte superior de varias estructuras y Python las guarda automáticamente en
los atributos __doc__ de los objetos correspondientes . Esto funciona para archivos de módulos, definiciones de funciones
y clases y métodos.

Ahora que sabemos más sobre las clases y los métodos, el siguiente archivo, docstr.py, proporciona un ejemplo rápido
pero completo que resume los lugares donde las cadenas de documentos pueden aparecer en su código. Todos estos
pueden ser bloques entre comillas triples o literales de una sola línea más simples como los que se muestran aquí:

"Soy: docstr.__doc__"

def func(args):
"Soy: docstr.func.__doc__"
pasar

class spam:
"Soy: spam.__doc__ o docstr.spam.__doc__ o self.__doc__" def
method(self): "Soy: spam.method.__doc__ o self.method.__doc__"
print(self.__doc__) print (auto.método.__doc__)

La principal ventaja de las cadenas de documentación es que permanecen en tiempo de ejecución.

Por lo tanto, si se ha codificado como una cadena de documentos, puede calificar un objeto con su
atributo __doc__ para obtener su documentación (imprimir el resultado interpreta los saltos de línea si es
una cadena de varias líneas): >>> import docstr >>> docstr.__doc__

882 | Capítulo 29: Detalles de codificación de clases


Machine Translated by Google

'Soy: docstr.__doc__' >>>


docstr.func.__doc__ 'Soy:
docstr.func.__doc__' >>>
docstr.spam.__doc__ 'Soy: spam.__doc__
o docstr.spam.__doc__ o yo mismo. __doc__' >>> docstr.spam.method.__doc__ 'Soy:
spam.method.__doc__ o self.method.__doc__'

>>> x = docstr.spam() >>>


x.método()
Soy: spam.__doc__ o docstr.spam.__doc__ o self.__doc__ Soy: spam.method.__doc__ o
self.method.__doc__

Una discusión de la herramienta PyDoc , que sabe cómo formatear todas estas cadenas en informes y páginas
web, aparece en el Capítulo 15. Aquí está ejecutando su función de ayuda en nuestro código bajo Python 2.X
(Python 3.X muestra atributos adicionales heredados de la superclase de objetos implícitos en el modelo de clase
de nuevo estilo; ejecútelo por su cuenta para ver los extras de 3.X y busque más información sobre esta diferencia
en el Capítulo 32): >>> help(docstr)

Ayuda sobre el módulo docstr:

NOMBRE

docstr - Soy: docstr.__doc__

EXPEDIENTE

c:\código\docstr.py

CLASES

correo no deseado

spam de clase |
Soy: spam.__doc__ o docstr.spam.__doc__ o self.__doc__ | | Métodos definidos aquí: | | método(auto)
| Soy: spam.method.__doc__ o self.method.__doc__

FUNCIONES

func(argumentos)
Yo soy: docstr.func.__doc__

Las cadenas de documentación están disponibles en tiempo de ejecución, pero son menos flexibles sintácticamente
que los comentarios # , que pueden aparecer en cualquier parte de un programa. Ambos formularios son
herramientas útiles, y cualquier documentación del programa es buena (¡siempre que sea precisa, por supuesto!).
Como se indicó anteriormente, la regla general de "práctica recomendada" de Python es usar cadenas de
documentos para la documentación funcional (lo que hacen sus objetos) y comentarios con marcas de hash para
obtener más documentación a nivel micro (cómo funcionan los bits arcanos de código).

Cadenas de documentación revisadas | 883


Machine Translated by Google

Clases Versus Módulos


Finalmente, terminemos este capítulo comparando brevemente los temas de las dos últimas partes de este libro:
módulos y clases. Debido a que ambos se refieren a espacios de nombres, la distinción puede ser confusa. En
breve:

• Módulos

— Implementar paquetes de datos/lógica

— Se crean con archivos de Python o extensiones de otros idiomas

— Se utilizan al ser importados

— Formar el nivel superior en la estructura del programa Python


• Clases

— Implementar nuevos objetos con todas las funciones


— Se crean con sentencias de clase

— Se usan al ser llamados —

Siempre viven dentro de un módulo Las

clases también admiten funciones adicionales que los módulos no admiten, como la sobrecarga de operadores, la
generación de instancias múltiples y la herencia. Aunque tanto las clases como los módulos son espacios de
nombres, ya debería poder darse cuenta de que son cosas muy diferentes. Necesitamos avanzar para ver cuán
diferentes pueden ser las clases.

Resumen del capítulo


Este capítulo nos llevó a un segundo recorrido más profundo por los mecanismos de programación orientada a
objetos del lenguaje Python. Aprendimos más sobre las clases, los métodos y la herencia, y terminamos la historia
de los espacios de nombres y los ámbitos en Python extendiéndola para cubrir su aplicación a las clases. En el
camino, analizamos algunos conceptos más avanzados, como superclases abstractas, atributos de datos de
clase, diccionarios y enlaces de espacios de nombres, y llamadas manuales a métodos y constructores de
superclases.

Ahora que hemos aprendido todo sobre la mecánica de la codificación de clases en Python, el Capítulo 30 se
centra en una faceta específica de esa mecánica: la sobrecarga de operadores. Después de eso, exploraremos
los patrones de diseño comunes, analizando algunas de las formas en que las clases se usan y combinan
comúnmente para optimizar la reutilización del código. Sin embargo, antes de seguir leyendo, asegúrese de
completar el cuestionario habitual del capítulo para revisar lo que hemos cubierto aquí.

Pon a prueba tus conocimientos: Cuestionario

1. ¿Qué es una superclase abstracta?

2. ¿Qué sucede cuando aparece una declaración de asignación simple en el nivel superior de un
declaración de clase ?

884 | Capítulo 29: Detalles de codificación de clases


Machine Translated by Google

3. ¿Por qué una clase podría necesitar llamar manualmente al método __init__ en una superclase?
4. ¿Cómo puede aumentar, en lugar de reemplazar por completo, un método heredado?
5. ¿En qué se diferencia el ámbito local de una clase del de una función?
6. ¿Cuál...era la capital de Asiria?

Pon a prueba tus conocimientos: respuestas

1. Una superclase abstracta es una clase que llama a un método, pero no lo hereda ni lo define; espera
que una subclase rellene el método. Esto se usa a menudo como una forma de generalizar clases
cuando el comportamiento no se puede predecir hasta que se codifica una subclase más específica.
Los marcos OOP también usan esto como una forma de enviar operaciones personalizables y
definidas por el cliente.
2. Cuando aparece una declaración de asignación simple (X = Y) en el nivel superior de una declaración
de clase , adjunta un atributo de datos a la clase (Class.X). Como todos los atributos de clase, esto
será compartido por todas las instancias; Sin embargo, los atributos de datos no son funciones de
método invocables.
3. Una clase debe llamar manualmente al método __init__ en una superclase si define un constructor
__init__ propio y aún desea que se ejecute el código de construcción de la superclase. Python mismo
ejecuta automáticamente solo un constructor, el más bajo del árbol. Los constructores de superclases
generalmente se llaman a través del nombre de la clase, pasando la instancia de self manualmente:
Superclass.__init__(self, ...).

4. Para aumentar en lugar de reemplazar completamente un método heredado, redefínalo


en una subclase, pero vuelva a llamar a la versión del método de la superclase
manualmente desde la nueva versión del método en la subclase. Es decir, pase la
instancia de self a la versión del método de la superclase manualmente: Superclass.method(self, ...).
5. Una clase es un ámbito local y tiene acceso a los ámbitos locales adjuntos, pero no sirve como ámbito
local adjunto al código anidado adicional. Al igual que los módulos, el alcance local de la clase se
transforma en un espacio de nombres de atributo después de ejecutar la declaración de la clase .

6. Ashur (o Qalat Sherqat), Calah (o Nimrud), el efímero Dur Sharrukin (o Khorsabad), y finalmente
Nínive.

Pon a prueba tus conocimientos: respuestas | 885


Machine Translated by Google
Machine Translated by Google

CAPÍTULO 30

Sobrecarga del operador

Este capítulo continúa con nuestro estudio en profundidad de la mecánica de clases centrándose en la sobrecarga
del operador. Vimos brevemente la sobrecarga de operadores en capítulos anteriores; aquí, completaremos más
detalles y veremos un puñado de métodos de sobrecarga de uso común.
Aunque no demostraremos cada uno de los muchos métodos de sobrecarga de operadores disponibles, los que
codificaremos aquí son una muestra representativa lo suficientemente grande como para descubrir las posibilidades
de esta característica de la clase Python.

Los basicos
En realidad, la "sobrecarga de operadores" simplemente significa interceptar operaciones integradas en los métodos
de una clase: Python invoca automáticamente sus métodos cuando aparecen instancias de la clase en operaciones
integradas, y el valor de retorno de su método se convierte en el resultado de la operación correspondiente. Aquí
hay una revisión de las ideas clave detrás de la sobrecarga:

• La sobrecarga de operadores permite que las clases intercepten las operaciones normales de

Python. • Las clases pueden sobrecargar todos los operadores de expresión de Python. • Las

clases también pueden sobrecargar las operaciones integradas, como la impresión, las llamadas a funciones, el
acceso a atributos, etc. • La sobrecarga hace que las instancias de clase actúen más como tipos integrados.

• La sobrecarga se implementa proporcionando métodos con nombres especiales en una clase.

En otras palabras, cuando se proporcionan ciertos métodos con nombres especiales en una clase, Python los llama
automáticamente cuando aparecen instancias de la clase en sus expresiones asociadas. Su clase proporciona el
comportamiento de la operación correspondiente para los objetos de instancia creados a partir de ella.

Como hemos aprendido, los métodos de sobrecarga de operadores nunca son necesarios y, por lo general, no
tienen valores predeterminados (aparte de algunos que algunas clases obtienen del objeto); si no codifica o hereda
uno, solo significa que su clase no admite la operación correspondiente. Sin embargo, cuando se usan, estos
métodos permiten que las clases emulen las interfaces de los objetos incorporados y, por lo tanto, parezcan más
consistentes.

887
Machine Translated by Google

Constructores y expresiones: __init__ y __sub__ Como revisión,


considere el siguiente ejemplo simple: su clase Number , codificada en el archivo number.py,
proporciona un método para interceptar la construcción de instancias (__init__), así como uno
para capturar expresiones de resta (__sub__ ). Métodos especiales como estos son los ganchos
que le permiten conectarse a operaciones integradas:
# Número de archivo.py

numero de clase:
def __init__(self, start): self.data = # En número (inicio)
start def __sub__(self, other):
return Number(self.data - other) # En instancia - otro
# El resultado es una nueva instancia

>>> del número de importación Número # Obtener clase del módulo


>>> X = Número(5) # Número.__init__(X, 5)
>>> Y = X - 2 # Número.__sub__(X, 2)
>>> Y.datos 3 # Y es una nueva instancia de Número

Como ya hemos aprendido, el método constructor __init__ que se ve en este código es el


método de sobrecarga de operadores más utilizado en Python; está presente en la mayoría de
las clases y se usa para inicializar el objeto de instancia recién creado usando cualquier
argumento pasado al nombre de la clase. El método __sub__ juega el rol de operador binario
que __add__ hizo en la introducción del Capítulo 27, interceptando expresiones de resta y
devolviendo una nueva instancia de la clase como resultado (y ejecutando __init__ en el camino).

Ya hemos estudiado __init__ y operadores binarios básicos como __sub__ con cierta
profundidad, por lo que no repetiremos su uso aquí. En este capítulo, recorreremos algunas de
las otras herramientas disponibles en este dominio y veremos código de ejemplo que las aplica en
casos de uso común.

Técnicamente, la creación de instancias activa primero el método __new__ , que crea y


devuelve el nuevo objeto de instancia, que luego se pasa a __init__ para la inicialización.
Sin embargo, dado que __new__ tiene una implementación integrada y se redefine solo
en roles muy limitados, casi todas las clases de Python se inicializan definiendo un
método __init__ . Veremos un caso de uso para __nuevo__ cuando estudiemos las
metaclases en el Capítulo 40; aunque es raro, a veces también se usa para personalizar
la creación de instancias de mutable
tipos

Métodos comunes de sobrecarga de operadores


Casi todo lo que puede hacer con objetos integrados, como números enteros y listas, tiene un
método correspondiente con un nombre especial para sobrecargar en las clases. La tabla 30-1
enumera algunos de los más comunes; hay muchos más. De hecho, muchos métodos de
sobrecarga vienen en varias versiones (p. ej., __add__, __radd__ y __iadd__ para la suma), que

888 | Capítulo 30: Sobrecarga del operador


Machine Translated by Google

es una de las razones por las que hay tantos. Consulte otros libros de Python, o el manual de referencia del
lenguaje Python, para obtener una lista exhaustiva de los nombres de métodos especiales disponibles.

Tabla 30-1. Métodos comunes de sobrecarga de operadores

Método Implementos Pedido

Constructor Creación de objetos: X = Clase (argumentos)


__en eso__
Incinerador de basuras
__del__ Recuperación de objetos de X

__agregar__ Operador + X + Y, X += Y si no __iadd__

__o__ Operador | (O bit a bit) X | Y, X |= Y si no __io__

__repr__, __str__ Impresión, conversiones imprimir(X), repr(X), str(X)

Llamadas de función
__llamar__ X(*argumentos, **cargos)

__getattr__
Obtención de atributo X.indefinido

__setattr__ Asignación de atributos X.cualquiera = valor


Eliminación de atributos
__delattr__ del X.any
Obtención de atributo
__getattribute__ X.cualquiera

__obtiene el objeto__ Indexación, rebanado, iteración X[clave], X[i:j], bucles for y otras iteraciones si no
__iter__

__setitem__ Asignación de índices y sectores X[clave] = valor, X[i:j] = iterable


Eliminación de índices y sectores
__delitem__ del X[tecla], del X[i:j]

__len__ Longitud len(X), pruebas de verdad si no __bool__

Pruebas booleanas bool(X), pruebas de verdad (llamadas __no cero__ en 2.X)


__bool__

__lt__, __gt__, comparaciones X < Y, X > Y, X <= Y, X >= Y, X == Y, X != Y


__le__, __ge__, (o bien __cmp__ solo en 2.X)

__eq__, __ne__

__radd__ Operadores del lado derecho Otro + X

__añado__ Operadores aumentados in situ X += Y (o bien __añadir__)


Contextos de iteración
__iter__, __siguiente__ I=itero(X), siguiente(I); para bucles, si no hay __con
tains__, todas las comprensiones, map(F,X), otros

(__next__ se nombra a continuación en 2.X)

__contiene__ Prueba de membresía elemento en X (cualquier iterable)

__índice__ Valor entero hex(X), bin(X), oct(X), O[X], O[X:] (reemplaza 2.X
__oct__, __hex__)

__enter__, __exit__ Administrador de contexto (Capítulo 34) con obj como var:

__obtener__, __establecer__, Atributos de descriptor (Capítulo 38) X.attr, X.attr = valor, del X.attr

__Eliminar__

__nuevo__ Creación (Capítulo 40) Creación de objetos, antes de __init__

Todos los métodos de sobrecarga tienen nombres que comienzan y terminan con dos guiones bajos para mantener
distinguirlos de otros nombres que defina en sus clases. Las asignaciones de especial

Los fundamentos | 889


Machine Translated by Google

los nombres de métodos para expresiones u operaciones están predefinidos por el lenguaje Python y
documentados en su totalidad en el manual del lenguaje estándar y otros recursos de referencia.
Por ejemplo, el nombre __add__ siempre se asigna a expresiones + por definición del lenguaje Python,
independientemente de lo que realmente haga el código del método __add__ .

Los métodos de sobrecarga de operadores se pueden heredar de las superclases si no se definen, como
cualquier otro método. Los métodos de sobrecarga de operadores también son todos opcionales: si no
codifica o hereda uno, esa operación simplemente no es compatible con su clase, e intentarlo generará una
excepción. Algunas operaciones integradas, como la impresión, tienen fallas predeterminadas (heredadas
de la clase de objeto implícita en Python 3.X), pero la mayoría de las funciones integradas fallan para
instancias de clase si no está presente el método de sobrecarga del operador correspondiente.

La mayoría de los métodos de sobrecarga se usan solo en programas avanzados que requieren que los
objetos se comporten como elementos integrados, aunque el constructor __init__ que ya conocemos tiende
a aparecer en la mayoría de las clases. Exploremos algunos de los métodos adicionales en la Tabla 30-1
por ejemplo.

Aunque las expresiones desencadenan métodos de operador, tenga cuidado de no


asumir que hay una ventaja de velocidad al eliminar al intermediario y llamar al método
de operador directamente. De hecho, llamar directamente al método del operador puede
ser el doble de lento, presumiblemente debido a la sobrecarga de una llamada de función,
que Python evita u optimiza en los casos integrados.

Esta es la historia de len y __len__ usando el iniciador de Windows del Apéndice B y las
técnicas de temporización del Capítulo 21 en Python 3.3 y 2.7: en ambos, llamar a
__len__ directamente toma el doble de tiempo:

c:\code> py ÿ3 -m timeit -n 1000 -r 5 -s


"L = lista(rango(100))" "x = L.__len__()"
1000 bucles, lo mejor de 5: 0,134 usec por bucle

c:\code> py ÿ3 -m timeit -n 1000 -r 5 -s


"L = lista(rango(100))" "x = len(L)"
1000 bucles, lo mejor de 5: 0,063 usec por bucle

c:\code> py ÿ2 -m timeit -n 1000 -r 5 -s


"L = lista(rango(100))" "x = L.__len__()"
1000 bucles, lo mejor de 5: 0,117 usec por bucle

c:\code> py ÿ2 -m timeit -n 1000 -r 5 -s


"L = lista(rango(100))" "x = len(L)"
1000 bucles, lo mejor de 5: 0,0596 usec por bucle

Esto no es tan artificial como puede parecer. De hecho, ¡he encontrado recomendaciones
para usar la alternativa más lenta en nombre de la velocidad en una destacada institución
de investigación!

Indexación y división: __getitem__ y __setitem__


Nuestro primer conjunto de métodos permite que sus clases imiten algunos de los comportamientos de
secuencias y asignaciones. Si está definido en una clase (o heredado por ella), el método __getitem__ se llama

890 | Capítulo 30: Sobrecarga del operador


Machine Translated by Google

automáticamente para las operaciones de indexación de instancias. Cuando una instancia X aparece en una
expresión de dexing como X[i], Python llama al método __getitem__ heredado por la instancia, pasando X al
primer argumento y el índice entre paréntesis al segundo argumento.

Por ejemplo, la siguiente clase devuelve el cuadrado de un valor de índice, quizás atípico, pero ilustrativo del
mecanismo en general:
>>> indexador de clases:
def __getitem__(self, índice): índice
de retorno ** 2

>>> X = Indexador()
>>> X[2] 4 # X[i] llama a X.__getitem__(i)

>>> for i in range(5):


print(X[i], end=' ') # Ejecuta __getitem__(X, i) cada vez

0 1 4 9 16

Intercepción de sectores

Curiosamente, además de la indexación, __getitem__ también se llama para expresiones de sector, siempre en
3.X y condicionalmente en 2.X si no proporciona métodos de sector más específicos. Hablando formalmente,
los tipos incorporados manejan el corte de la misma manera. Aquí, por ejemplo, se está rebanando en el trabajo
en una lista integrada, usando límites superior e inferior y un paso (consulte el Capítulo 7 si necesita un repaso
sobre el rebanado):

>>> L = [5, 6, 7, 8, 9]
>>> L[2:4] # Rebanada con sintaxis de rebanada: 2..(4-1)
[7, 8]
>>> L[1:]
[6, 7, 8, 9]
>>> L[:-1] [5,
6, 7, 8]
>>> L[::2] [5,
7, 9]

En realidad, sin embargo, los límites de división se agrupan en un objeto de división y se pasan a la
implementación de indexación de la lista. De hecho, siempre puede pasar un objeto de división manualmente;
la sintaxis de división es principalmente azúcar sintáctica para indexar con un objeto de división:

>>> L[rebanada(2, 4)] # Rebanada con objetos rebanados


[7, 8]
>>> L[rebanada(1, Ninguno)]
[6, 7, 8, 9]
>>> L[rebanada(Ninguno,
ÿ1)] [5, 6, 7, 8]
>>> L[segmento(Ninguno, Ninguno,
2)] [5, 7, 9]

Esto es importante en las clases con un método __getitem__ : en 3.X, el método se llamará tanto para la
indexación básica (con un índice) como para el corte (con un objeto de corte). nuestro anterior

Indexación y segmentación: __getitem__ y __setitem__ | 891


Machine Translated by Google

La clase no manejará el corte porque sus matemáticas asumen que se pasan índices enteros, pero la siguiente
clase sí lo hará. Cuando se llama para la indexación, el argumento es un número entero como antes:
>>> class Indexer: data =
[5, 6, 7, 8, 9] def __getitem__(self,
index): # Llamado para index o slice print('getitem:', index) return self.data[index]

# Realizar índice o corte

>>> X = Indexador()
>>> X[0] # La indexación envía a __getitem__ un número entero
obtener elemento:
05
>>> X[1]
obtener elemento:
16

>>> X[-1]
obtener elemento: ÿ1
9

Sin embargo, cuando se llama para dividir, el método recibe un objeto de división, que simplemente se pasa al
indexador de lista incrustado en una nueva expresión de índice:

>>> X[2:4] # Slicing envía a __getitem__ un objeto de corte


getitem: segmento(2, 4, Ninguno) [7, 8]

>>> X[1:]
getitem: segmento(1, Ninguno, Ninguno) [6,
7, 8, 9]
>>> X[:-1]
getitem: segmento(Ninguno, ÿ1, Ninguno) [5,
6, 7, 8]
>>> X[::2]
getitem: segmento(Ninguno, Ninguno, 2) [5,
7, 9]

Cuando sea necesario, __getitem__ puede probar el tipo de su argumento y extraer los límites del objeto de
división: los objetos de división tienen atributos de inicio, parada y paso, cualquiera de los cuales puede ser
Ninguno si se omite:

>>> class Indexer: def

__getitem__(self, index): if isinstance(index,


int): # Modo de uso de prueba
imprimir ('indexación', índice) más:

print('rebanar', index.start, index.stop, index.step)

>>> X = Indexador()
>>> X[99]
indexación 99
>>> X[1:99:2]
rebanado 1 99 2
>>> X[1:]
rebanar 1 Ninguno Ninguno

892 | Capítulo 30: Sobrecarga del operador


Machine Translated by Google

Si se usa, el método de asignación de índice __setitem__ intercepta de manera similar tanto las
asignaciones de índice como las de segmento: en 3.X (y generalmente en 2.X) recibe un objeto de
segmento para este último, que puede transferirse a otra asignación de índice o usarse directamente en lo mismo
camino:

clase IndexSetter:
def __setitem__(self, index, value): # Índice de intercepción o asignación de corte
...
self.datos[índice] = valor # Asignar índice o segmento

De hecho, se puede llamar automáticamente a __getitem__ incluso en más contextos que indexar y
dividir; también es una opción alternativa de iteración , como veremos en un momento. Primero, sin
embargo, echemos un vistazo rápido al sabor de 2.X de estas operaciones para los lectores de 2.X, y
aclaremos un posible punto de confusión en esta categoría.

Segmentación e indexación en Python 2.X

Solo en Python 2.X, las clases también pueden definir métodos __getslice__ y __setslice__ para
interceptar búsquedas y asignaciones de segmentos específicamente. Si se definen, a estos métodos
se les pasan los límites de la expresión de sector y se prefieren a __getitem__ y __seti tem__ para
sectores de dos límites. Sin embargo, en todos los demás casos, este contexto funciona igual que en
3.X; por ejemplo, aún se crea un objeto de división y se pasa a __getitem__ si no se encuentra __get
slice__ o se usa una forma de división extendida de tres límites: C:\code> c:\python27\python >>>
class Slicer: def __getitem__( self, índice): imprimir índice def __getslice__(self, i, j):
imprimir i, j def __setslice__(self, i, j,seq): imprimir i, j,seq

>>> Rebanadora()[1] # Ejecuta __getitem__ con int, como 3.X


1
>>> Slicer()[1:9] 1 9 # Ejecuta __getslice__ si está presente, de lo contrario __getitem__
>>> Slicer()[1:9:2] #
Ejecuta __getitem__ con slice(), ¡como 3.X! rebanada (1, 9, 2)

Estos métodos específicos de segmento se eliminan en 3.X, por lo que incluso en 2.X generalmente
debe usar __getitem__ y __setitem__ en su lugar y permitir índices y objetos de segmento como
argumentos, tanto para la compatibilidad con versiones anteriores como para evitar tener que manejar
dos- y rebanadas de tres límites de manera diferente. En la mayoría de las clases, esto funciona sin
ningún código especial, porque los métodos de indexación pueden pasar manualmente el objeto de
división entre corchetes de otra expresión de índice, como en el ejemplo de la sección anterior.
Consulte la sección “Pertenencia: __contains__, __iter__ y __getitem__” en la página 906 para ver otro
ejemplo de intercepción de corte en funcionamiento.

Indexación y segmentación: __getitem__ y __setitem__ | 893


Machine Translated by Google

¡Pero el __index__ de 3.X no está indexando!


En una nota relacionada, no confunda el método __index__ (quizás desafortunadamente llamado) en Python 3.X
para la intercepción de índices: este método devuelve un valor entero para una instancia cuando es necesario y
lo usan los integrados que se convierten en cadenas de dígitos. (y en retrospectiva, podría haber sido mejor
llamado __asindex__):
>>> clase C:
def __index__(auto):
devuelve 255

>>> X = C()
>>> # Valor entero
hexadecimal(X)
'0xff' >>>
bin(X)
'0b11111111'
>>> oct(X) '0o377'

Aunque este método no intercepta la indexación de instancias como __getitem__, también se usa en contextos
que requieren un número entero, incluida la indexación:

>>> ('C' * 256)[255]


'C'
>>> ('C' * 256)[X] # Como índice (no X[i])
'C'
>>> ('C' * 256)[X:] # Como índice (no X[i:])
'C'

Este método funciona de la misma manera en Python 2.X, excepto que no se llama para las funciones integradas
hexadecimal y octava ; use __hex__ y __oct__ en 2.X (solo) en su lugar para interceptar estas llamadas.

Iteración de índice: __getitem__


Aquí hay un gancho que no siempre es obvio para los principiantes, pero resulta sorprendentemente útil. A falta
de métodos de iteración más específicos que veremos en la siguiente sección, la declaración for funciona al
indexar repetidamente una secuencia de cero a índices más altos, hasta que se detecta una excepción IndexError
fuera de los límites . Debido a eso, __geti tem__ también resulta ser una forma de sobrecargar la iteración en
Python; si se define este método, los bucles for llaman al __getitem__ de la clase cada vez, con compensaciones
cada vez más altas.

Es un caso de "código uno, obtenga uno gratis": cualquier objeto integrado o definido por el usuario que responda
a la indexación también responde a la iteración del bucle for :

>>> clase StepperIndex:


def __getitem__(self, i): return
self.data[i]

>>> X = Índice escalonado() # X es un objeto StepperIndex


>>> X.datos = "Correo no deseado"

894 | Capítulo 30: Sobrecarga del operador


Machine Translated by Google

>>>
>>> X[1] # Indexación de llamadas __getitem__
'p' >>>
para elemento en X: # for loops call __getitem__ #
imprimir(elemento, fin=' ') for indexes items 0..N

Correo no deseado

De hecho, es realmente un caso de "código uno, obtenga un montón gratis". Cualquier clase que admita
bucles for admite automáticamente todos los contextos de iteración en Python, muchos de los cuales hemos
visto en capítulos anteriores (los contextos de iteración se presentaron en el Capítulo 14). Por ejemplo, la
prueba de pertenencia, las listas de comprensión, el mapa integrado, las asignaciones de listas y tuplas y los
constructores de tipos también llamarán a __getitem__ automáticamente, si está definido:

>>> 'p' en X # Todos llaman a __getitem__ también


Verdadero

>>> [c por c en X] # Comprensión de lista


['Correo no deseado']

>>> lista(mapa(str.superior, X)) # mapear llamadas (use list() en 3.X)


['CORREO NO DESEADO']

>>> (a, b, c, d) = X >>> a, # Asignaciones de secuencias


c, d
('S', 'a', 'm')

>>> lista(X), tupla(X), ''.join(X) # Y así...


(['S', 'p', 'a', 'm'], ('S', 'p', 'a', 'm'), 'Correo basura')

>>> X
<__objeto principal__.StepperIndex en 0x000000000297B630>

En la práctica, esta técnica se puede utilizar para crear objetos que proporcionen una interfaz de secuencia
y para agregar lógica a las operaciones de tipo de secuencia integradas; revisaremos esta idea cuando
ampliemos los tipos incorporados en el Capítulo 32.

Objetos iterables: __iter__ y __next__


Aunque la técnica __getitem__ de la sección anterior funciona, en realidad es solo una alternativa para la
iteración. Hoy, todos los contextos de iteración en Python probarán primero el método __iter__ , antes de
probar __getitem__. Es decir, prefieren el protocolo de iteración del que aprendimos en el Capítulo 14 a la
indexación repetida de un objeto; solo si el objeto no es compatible con el protocolo de iteración, se intenta
indexar en su lugar. En términos generales, también debería preferir __iter__ : admite contextos de iteración
general mejor que __getitem__ .

Técnicamente, los contextos de iteración funcionan pasando un objeto iterable a la función integrada iter
para invocar un método __iter__ , que se espera que devuelva un objeto iterador.
Si se proporciona, Python llama repetidamente al método __next__ de este objeto iterador para producir
elementos hasta que se genera una excepción StopIteration . Una siguiente función incorporada también es

Objetos iterables: __iter__ y __next__ | 895


Machine Translated by Google

disponible como una conveniencia para las iteraciones manuales—next(I) es lo mismo que I.__next__(). Para una
revisión de los elementos esenciales de este modelo, consulte la Figura 14-1 en el Capítulo 14.

A esta interfaz de objeto iterable se le da prioridad y se intenta primero. Solo si no se encuentra dicho método
__iter__ , Python recurre al esquema __getitem__ e indexa repetidamente por compensaciones como antes, hasta
que se genera una excepción IndexError .

Nota sobre el sesgo de la versión: como se describe en el Capítulo 14, si usa Python
2.X, el método de iterador I.__next__() que se acaba de describir se denomina I.next()
en su Python, y el next(I) incorporado es presente para portabilidad: llama a I.next() en
2.X e I.__next__() en 3.X. La iteración funciona igual en 2.X en todos los demás aspectos.

Iterables definidos por el usuario

En el esquema __iter__ , las clases implementan iterables definidos por el usuario simplemente implementando el
protocolo de iteración presentado en el Capítulo 14 y elaborado en el Capítulo 20. Por ejemplo, el siguiente archivo
usa una clase para definir un iterable definido por el usuario que genera cuadrados a pedido. , en lugar de todo a la
vez (según la nota anterior, en Python 2.X define next en lugar de __next__, e imprime con una coma final como de
costumbre):

# Archivo cuadrados.py

class Squares: def


__init__(self, start, stop): # Guarda el estado cuando se crea self.value = start - 1
self.stop = stop def __iter__(self): return self def __next__(self): if self.value
== self.stop: aumentar StopIteration self.value += 1 return self.value ** 2
# Obtener objeto iterador en iter

# Devuelve un cuadrado en cada iteración


# También llamado por el siguiente integrado

Cuando se importan, sus instancias pueden aparecer en contextos de iteración como elementos integrados:

% python
>>> from squares import Squares >>> for
i in Squares(1, 5): print(i, end=' ') # para llamadas iter, que llama a __iter__
# Cada iteración llama a __next__

1 4 9 16 25

Aquí, el objeto iterador devuelto por __iter__ es simplemente la instancia propia, porque el método __next__ es
parte de esta clase en sí. En escenarios más complejos, el objeto iterador se puede definir como una clase y un
objeto separados con su propia información de estado para admitir múltiples iteraciones activas sobre los mismos
datos (veremos un ejemplo de esto en un momento). El final de la iteración se señala con una declaración de
aumento de Python, presentada en el Capítulo 29 y cubierta en su totalidad en la siguiente parte de este libro, pero
que simplemente

896 | Capítulo 30: Sobrecarga del operador


Machine Translated by Google

genera una excepción como si Python mismo lo hubiera hecho. Las iteraciones manuales funcionan igual en los iterables definidos
por el usuario que en los tipos integrados: >>> X = Squares(1, 5)

# Iterar manualmente: qué hacen los


>>> I = iter(X) >>> bucles # iter llama a __iter__ # next
siguiente(I) llama a __next__ (en 3.X)
1
>>> siguiente(yo)

4 ...más omitido... >>>


siguiente(I)
25
>>> siguiente(yo) # Puede detectar esto en la declaración de prueba

Detener iteración

Una codificación equivalente de este iterable con __getitem__ podría ser menos natural, porque el for luego
iteraría a través de todos los desplazamientos cero y superiores; las compensaciones pasadas solo estarían
indirectamente relacionadas con el rango de valores producidos (0..N necesitaría mapear para iniciar... detener).
Debido a que los objetos __iter__ retienen el estado administrado explícitamente entre las próximas llamadas,
pueden ser más generales que __getitem__.

Por otro lado, los iterables basados en __iter__ a veces pueden ser más complejos y menos funcionales que
los basados en __getitem__. Realmente están diseñados para la iteración, no para la indexación aleatoria; de
hecho, no sobrecargan la expresión de indexación en absoluto, aunque puede recopilar sus elementos en una
secuencia, como una lista, para habilitar otras operaciones:

>>> X = Cuadrados(1, 5)
>>> X[1]
TypeError: el objeto 'Cuadrados' no admite la indexación >>> lista (X) [1]

Exploraciones únicas frente a

exploraciones múltiples El esquema __iter__ también es la implementación de todos los demás contextos de
iteración que vimos en acción para el método __getitem__ : pruebas de pertenencia, constructores de tipos,
asignación de secuencias, etc. Sin embargo, a diferencia de nuestro ejemplo anterior de __getitem__ , también
debemos tener en cuenta que el __iter__ de una clase puede estar diseñado para un solo recorrido , no para
muchos. Las clases eligen el comportamiento de escaneo explícitamente en su código.

Por ejemplo, debido a que el __iter__ de la clase Squares actual siempre devuelve self con solo una copia del
estado de iteración, es una iteración única; una vez que haya iterado sobre una instancia de esa clase, estará
vacía. Llamar a __iter__ nuevamente en la misma instancia devuelve self nuevamente, en cualquier estado que
haya quedado. Por lo general, debe crear un nuevo objeto de instancia iterable para cada nueva iteración:

>>> X = Cuadrados(1, 5) >>> # Hacer un iterable con estado


[n para n en X] [1, 4, 9, 16, # Agota elementos: __iter__ se devuelve a sí mismo
25] >>> [n para n en X] []
# Ahora está vacío: __iter__ devuelve el mismo yo

Objetos iterables: __iter__ y __next__ | 897


Machine Translated by Google

>>> [n por n en Cuadrados(1, 5)] [1, 4, 9, # Hacer un nuevo objeto iterable


16, 25] >>> lista(Cuadrados(1, 3)) [1, 4, 9]
# Un nuevo objeto para cada nueva llamada __iter__

Para admitir varias iteraciones de manera más directa, también podríamos recodificar este ejemplo con una clase
adicional u otra técnica, como lo haremos en un momento. Sin embargo, tal como está, al crear una nueva instancia
para cada iteración, obtiene una copia nueva del estado de la iteración:

>>> 36 en Cuadrados(1, 10) # Otros contextos de iteración


Verdadero

>>> a, b, c = Cuadrados(1, 3) >>> a, # Cada uno llama a __iter__ y luego a __next__


b, c (1, 4, 9) >>> ':'.join(map(str,
Cuadrados(1, 5)) ) '1:4:9:16:25'

Al igual que los elementos incorporados de escaneo único, como el mapa, la conversión a una lista también admite
escaneos múltiples, pero agrega costos de rendimiento de tiempo y espacio, que pueden o no ser significativos para
un programa determinado:

>>> X = Cuadrados(1, 5) >>>


tupla(X), tupla(X) ((1, 4, 9, 16, # Iterador agotado en la segunda tupla()
25), ())

>>> X = lista(Cuadrados(1, 5)) >>>


tupla(X), tupla(X) ((1, 4, 9, 16, 25), (1,
4, 9, 16, 25) )

Mejoraremos esto para admitir múltiples escaneos más directamente adelante, después de un poco de comparación
y contraste.

Clases versus generadores

Tenga en cuenta que el ejemplo anterior probablemente sería más simple si estuviera codificado con funciones
generadoras o expresiones, herramientas presentadas en el Capítulo 20 que producen automáticamente objetos
iterables y retienen el estado de la variable local entre iteraciones: >>> def gsquares(start, stop): for i en el rango

(inicio, parada + 1): rendimiento i ** 2

>>> for i in gsquares(1, 5): print(i,


end=' ')

1 4 9 16 25

>>> for i in (x ** 2 for x in range(1, 6)): print(i, end=' ')

1 4 9 16 25

A diferencia de las clases, las funciones y expresiones del generador guardan implícitamente su estado y crean los
métodos necesarios para cumplir con el protocolo de iteración, con ventajas obvias.

898 | Capítulo 30: Sobrecarga del operador


Machine Translated by Google

en concisión de código para ejemplos más simples como estos. Por otro lado, los atributos y métodos
más explícitos de la clase, la estructura adicional, las jerarquías de herencia y el soporte para múltiples
comportamientos pueden ser más adecuados para casos de uso más ricos.

Por supuesto, para este ejemplo artificial, de hecho podría omitir ambas técnicas y simplemente usar un
bucle for , un mapa o una lista de comprensión para construir la lista de una sola vez. Salvo que los datos
de rendimiento indiquen lo contrario, la forma mejor y más rápida de realizar una tarea en Python suele
ser también la más sencilla:

>>> [x ** 2 para x en el rango (1, 6)]


[1, 4, 9, 16, 25]

Sin embargo, las clases pueden ser mejores para modelar iteraciones más complejas, especialmente
cuando pueden beneficiarse de los activos de las clases en general. Un iterable que produce elementos
en una base de datos compleja o resultado de un servicio web, por ejemplo, podría aprovechar al máximo
las clases. La siguiente sección explora otro caso de uso para las clases en el usuario
iterables definidos.

Múltiples iteradores en un objeto

Anteriormente, mencioné que el objeto iterador (con __next__) producido por un iterable puede definirse
como una clase separada con su propia información de estado para admitir más directamente múltiples
iteraciones activas sobre los mismos datos. Considere lo que sucede cuando cruzamos un tipo
incorporado como una cadena:

>>> S = 'as'
>>> para x en S:
para y en S:
print(x + y, end=' ')

aa ac ae ca cc ce ea ec ee

Aquí, el ciclo externo toma un iterador de la cadena llamando a iter, y cada ciclo anidado hace lo mismo
para obtener un iterador independiente. Debido a que cada iterador activo tiene su propia información de
estado, cada bucle puede mantener su propia posición en la cadena, independientemente de cualquier
otro bucle activo. Además, no estamos obligados a crear una nueva cadena o convertir a una lista cada
vez; el objeto de una sola cadena en sí admite múltiples escaneos.

Vimos ejemplos relacionados anteriormente, en el Capítulo 14 y el Capítulo 20. Por ejemplo, las funciones
y expresiones del generador, así como funciones integradas como map y zip, demostraron ser objetos
de un solo iterador, por lo que admiten un solo escaneo activo. Por el contrario, el rango integrado y otros
tipos integrados, como las listas, admiten varios iteradores activos con posiciones independientes.

Cuando codificamos iterables definidos por el usuario con clases, depende de nosotros decidir si
admitiremos una sola iteración activa o muchas. Para lograr el efecto de múltiples iteradores, __iter__
simplemente necesita definir un nuevo objeto con estado para el iterador, en lugar de devolver self para
cada solicitud de iterador.

Objetos iterables: __iter__ y __next__ | 899


Machine Translated by Google

La siguiente clase SkipObject , por ejemplo, define un objeto iterable que salta cada
otro elemento en las iteraciones. Debido a que su objeto iterador se crea de nuevo a partir de un complemento
clase para cada iteración, admite múltiples bucles activos directamente (esto es omisión de archivo por.py en los
ejemplos del libro):

#!python3
# Archivo skipper.py

clase SaltarObjeto:
def __init__(self, envuelto): # Guardar elemento para ser utilizado

self.envuelto = envuelto
def __iter__(uno mismo):
devuelve SkipIterator(self.wrapped) # Nuevo iterador cada vez

clase SkipIterator:
def __init__(auto, envuelto):
self.envuelto = envuelto # Información del estado del iterador
self.offset = 0
def __siguiente__(uno mismo):
if self.offset >= len(self.wrapped): aumentar # Terminar iteraciones
StopIteration
más:
item = self.envuelto[self.offset] self.offset # más volver y saltar
+= 2
Devolver objeto

si __nombre__ == '__principal__':
alfa = 'abcdef'
patrón = SaltarObjeto(alfa) # Hacer objeto contenedor
I = iter(skipper) # Hacer un iterador en él
print(siguiente(I), siguiente(I), siguiente(I)) # Desplazamientos de visita 0, 2, 4

para x en skipper: # para llamadas __iter__ automáticamente


para y en patrón: # Los fors anidados llaman a __iter__ de nuevo cada vez
print(x + y, end=' ') # Cada iterador tiene su propio estado, desplazamiento

Una nota rápida de portabilidad: tal como está, este es un código solo 3.X. Para que sea compatible con 2.X, importe
la función de impresión 3.X , y use next en lugar de __next__ para uso exclusivo de 2.X, o alias
los dos nombres en el alcance de la clase para uso dual 2.X/3.X (archivo skipper_2x.py en el
los ejemplos del libro lo hacen):

#!pitón
de __futuro__ importar print_function # Compatibilidad 2.X/3.X
...
clase SkipIterator:
...
def __siguiente__(uno mismo):
...
siguiente = __siguiente__ # Compatibilidad 2.X/3.X

Cuando se ejecuta la versión adecuada en Python, este ejemplo funciona como el anidado
bucles con cuerdas incorporadas. Cada bucle activo tiene su propia posición en la cadena porque
cada uno obtiene un objeto iterador independiente que registra su propia información de estado:

900 | Capítulo 30: Sobrecarga del operador


Machine Translated by Google

% python skipper.py
as
aa ac ae ca cc ce ea ec ee

Por el contrario, nuestro ejemplo anterior de Squares solo admite una iteración activa, a menos que llamemos
a Squares nuevamente en bucles anidados para obtener nuevos objetos. Aquí, solo hay un SkipOb ject iterable,
con múltiples objetos iteradores creados a partir de él.

Clases frente a sectores

Como antes, podríamos lograr resultados similares con herramientas integradas, por ejemplo, dividir con un
tercer límite para omitir elementos:

>>> S = 'abcdef'
>>> para x en S[::2]:
para y en S[::2]: # Objetos nuevos en cada iteración
print(x + y, end=' ')

aa ac ae ca cc ce ea ec ee

Esto no es exactamente lo mismo, sin embargo, por dos razones. Primero, cada expresión de segmento aquí
almacenará físicamente la lista de resultados de una sola vez en la memoria; iterables, por otro lado, producen
solo un valor a la vez, lo que puede ahorrar espacio sustancial para listas de resultados grandes.
En segundo lugar, los cortes producen nuevos objetos, por lo que no estamos realmente iterando sobre el
mismo objeto en varios lugares aquí. Para estar más cerca de la clase, necesitaríamos hacer un solo objeto
para atravesarlo cortando antes de tiempo:

>>> S = 'abcdef'
>>> S = S[::2]
>>>
S 'as'
>>> para x en S:
para y en S: # Mismo objeto, nuevos iteradores
print(x + y, end=' ')

aa ac ae ca cc ce ea ec ee

Esto es más similar a nuestra solución basada en clases, pero aún almacena el resultado del corte en la
memoria de una sola vez (hoy en día no hay una forma de generador de corte integrado), y solo es equivalente
para este caso particular de omitir cualquier otro elemento. .

Debido a que los iterables definidos por el usuario codificados con clases pueden hacer cualquier cosa que una
clase pueda hacer, son mucho más generales de lo que puede implicar este ejemplo. Aunque tal generalidad
no es necesaria en todas las aplicaciones, los iterables definidos por el usuario son una herramienta poderosa:
nos permiten hacer que los objetos arbitrarios se vean y se sientan como las otras secuencias e iterables que
hemos conocido en este libro. Podríamos usar esta técnica con un objeto de base de datos, por ejemplo, para
admitir iteraciones sobre búsquedas de bases de datos grandes, con múltiples cursores en el mismo resultado
de consulta.

Objetos iterables: __iter__ y __next__ | 901


Machine Translated by Google

Alternativa de codificación: __iter__ más rendimiento

Y ahora, por algo completamente implícito, pero potencialmente útil de todos modos. En
algunas aplicaciones, es posible minimizar los requisitos de codificación para iterables definidos por el usuario al
combinar el método __iter__ que estamos explorando aquí y el generador de rendimiento
declaración de función que estudiamos en el Capítulo 20. Debido a que las funciones generadoras automáticamente
guardan el estado de la variable local y crean los métodos iteradores requeridos, cumplen este rol
así, y complementar la retención estatal y otras utilidades que obtenemos de las clases.

Como repaso, recuerde que cualquier función que contenga una declaración de rendimiento se convierte en una
función generadora. Cuando se le llama, devuelve un nuevo objeto generador con retención automática del alcance
local y la posición del código, un método __iter__ creado automáticamente
que simplemente se devuelve a sí mismo, y un método __next__ creado automáticamente (siguiente en 2.X)
que inicia la función o la reanuda donde la dejó por última vez:

>>> def gen(x):


para i en el rango (x): rendimiento i ** 2

>>> G = gen(5) # Crear un generador con __iter__ y __next__


>>> G.__iter__() == G # Ambos métodos existen en el mismo objeto
Verdadero
>>> I = iter(G) >>> # Ejecuta __iter__: el generador se devuelve a sí mismo
siguiente(I), siguiente(I) # Ejecuta __next__ (siguiente en 2.X)
(0, 1)
>>> lista(gen(5)) [0, # Los contextos de iteración ejecutan automáticamente iter y next
1, 4, 9, 16]

Esto sigue siendo cierto incluso si la función generadora con un rendimiento resulta ser un método
llamado __iter__: siempre que sea invocado por una herramienta de contexto de iteración, dicho método
devolver un nuevo objeto generador con el requisito __next__. Como bono adicional, generador
las funciones codificadas como métodos en las clases tienen acceso al estado guardado tanto en atributos de

instancia como en variables de alcance local.

Por ejemplo, la siguiente clase es equivalente al iterable definido por el usuario de Squares inicial
codificamos anteriormente en squares.py.

# Archivo squares_yield.py

clase Cuadrados: # __iter__ + generador de rendimiento


def __init__(self, start, stop): self.start = start # __next__ es automático/ implícito

self.stop = detener
def __iter__(uno mismo):
para el valor en el rango (self.start, self.stop + 1):
valor de rendimiento ** 2

No hay necesidad de alias junto a __next__ para la compatibilidad con 2.X aquí, porque esto
El método ahora está automatizado e implícito mediante el uso de yield. Como antes, bucles for y
otras herramientas de iteración iteran a través de instancias de esta clase automáticamente:

% pitón
>>> from squares_yield import Squares
>>> for i in Squares(1, 5): print(i, end=' ')

902 | Capítulo 30: Sobrecarga del operador


Machine Translated by Google

1 4 9 16 25

Y como de costumbre, podemos mirar debajo del capó para ver cómo funciona esto realmente en
contextos de iteración. Ejecutar nuestra instancia de clase a través de iter obtiene el resultado de llamar
a __iter__ como de costumbre, pero en este caso el resultado es un objeto generador con un __next__
creado automáticamente del mismo tipo que siempre obtenemos cuando llamamos a una función
generadora que contiene un rendimiento. La única diferencia aquí es que la función del generador se
llama automáticamente en iter. Invocar la siguiente interfaz del objeto de resultado produce resultados a pedido:

>>> S = Cuadrados(1, 5) # Ejecuta __init__: la clase guarda el estado de la instancia


>>> S
<squares_yield.Objeto Squares en 0x000000000294B630>

>>> I = iter(S) # Ejecuta __iter__: devuelve un generador


>>> I
<objeto generador __iter__ en 0x00000000029A8CF0> >>>
siguiente(I)
1
>>> siguiente(yo) # Ejecuta el __siguiente__ del generador

4 ...etc...
>>> siguiente(yo) # El generador tiene tanto instancia como estado de alcance local
Detener iteración

También puede ser útil notar que podríamos nombrar el método generador de otra manera que no sea
__iter__ y llamarlo manualmente para iterar—Squares(1,5).gen(), por ejemplo. El uso del nombre
__iter__ invocado automáticamente por las herramientas de iteración simplemente omite un paso
manual de obtención y llamada de atributos:

clase Cuadrados: # No equivalente a __iter__ (squares_manual.py)


def __init__(...):
...
def gen(self): para
valor en rango(self.start, self.stop + 1): valor de rendimiento ** 2

% python
>>> from squares_manual import Squares >>> for
i in Squares(1, 5).gen(): print(i, end=' ') ...mismos resultados...

>>> S = Cuadrados(1, 5)
>>> I = iter(S.gen()) >>> # Generador de llamadas manualmente para iterable/ iterador
siguiente(I) ...mismos
resultados...

Codificar el generador como __iter__ elimina al intermediario en su código, aunque ambos esquemas
finalmente terminan creando un nuevo objeto generador para cada iteración:

• Con __iter__, la iteración desencadena __iter__, que devuelve un nuevo generador con
__Siguiente__.

Objetos iterables: __iter__ y __next__ | 903


Machine Translated by Google

• Sin __iter__, su código llama para hacer un generador, que se devuelve a sí mismo para
__iter__.
Consulte el Capítulo 20 para obtener más información sobre el rendimiento y los generadores si esto es
desconcertante, y compárelo con la versión __next__ más explícita en squares.py anterior. Notarás que esta
nueva versión de squares_yield.py es 4 líneas más corta (7 versus 11). En cierto sentido, este esquema reduce
los requisitos de codificación de clases al igual que las funciones de cierre del Capítulo 17, pero en este caso
lo hace con una combinación de técnicas funcionales y OOP, en lugar de una alternativa a las clases. Por
ejemplo, el método del generador aún aprovecha los atributos propios.

Esto también puede parecer demasiados niveles de magia para algunos observadores: se basa tanto en el
protocolo de iteración como en la creación de objetos de los generadores, los cuales son altamente implícitos
(en contradicción con los temas de Python de larga data: vea importar esto).
Dejando a un lado las opiniones, también es importante comprender el sabor sin rendimiento de las iterables
de clase, porque es explícito, general y, a veces, de alcance más amplio.

Aún así, la técnica __iter__/yield puede resultar efectiva en los casos en que se aplique. También viene con
una ventaja sustancial, como se explica en la siguiente sección.

Múltiples iteradores con

rendimiento Además de la concisión del código, la clase iterable definida por el usuario de la sección anterior
basada en la combinación __iter__/yield tiene una importante ventaja adicional: también admite múltiples
iteradores activos automáticamente. Esto se deriva naturalmente del hecho de que cada llamada a __iter__ es
una llamada a una función generadora, que devuelve un nuevo generador con su propia copia del alcance
local para la retención de estado:

% python
>>> from squares_yield import Squares # Uso de __iter__/ yield Squares
>>> S = Cuadrados(1, 5)
>>> I = iter(S) >>>
siguiente(I); siguiente yo)
1
4
>>> J = iter(S) >>> # Con rendimiento, múltiples iteradores automáticos
siguiente(J)
1
>>> siguiente(yo) # I es independiente de J: propio estado local
9

Aunque las funciones del generador son iterables de un solo escaneo, las llamadas implícitas a __iter__ en
contextos de iteración hacen que los nuevos generadores admitan nuevos escaneos independientes:

>>> S = Cuadrados(1, 3)
>>> para i en S: para j en # Cada uno para llamadas __iter__
S: print('%s:%s'
% (i, j), end=' ')

1:1 1:4 1:9 4:1 4:4 4:9 9:1 9:4 9:9

904 | Capítulo 30: Sobrecarga del operador


Machine Translated by Google

Para hacer lo mismo sin yield se requiere una clase suplementaria que almacene el estado del iterador de
forma explícita y manual, utilizando las técnicas de la sección anterior (y crece a 15 líneas: 8 más que con
yield):

# Archivo cuadrados_nonyield.py

class Squares: def


__init__(self, start, stop): self.start = start # Generador sin rendimiento
self.stop = stop def __iter__(self): # Multiscans: objeto extra
return SquaresIter(self.start, self.stop)

class SquaresIter: def


__init__(self, start, stop): self.value = start -
1 self.stop = stop def __next__(self):
if self.value == self.stop: aumentar
StopIteration self.value += 1 return valor
propio ** 2

Esto funciona igual que la versión yield multiscan, pero con más código y más explícito:

% python
>>> from squares_nonyield import Squares >>> for
i in Squares(1, 5): print(i, end=' ')

1 4 9 16 25
>>>
>>> S = Cuadrados(1, 5)
>>> I = iter(S) >>>
siguiente(I); siguiente yo)
1
4
>>> J = iter(S) >>> # Múltiples iteradores sin rendimiento
siguiente(J)
1
>>> siguiente(I)
9

>>> S = Cuadrados(1, 3)
>>> para i en S: para j en # Cada para llamadas __iter___
S: print('%s:%s'
% (i, j), end=' ')

1:1 1:4 1:9 4:1 4:4 4:9 9:1 9:4 9:9

Finalmente, el enfoque basado en generador podría eliminar de manera similar la necesidad de una clase de
iterador adicional en el ejemplo anterior de saltador de elementos de file skipper.py, gracias a sus métodos
automáticos y retención de estado variable local (y se registra en 9 líneas en comparación con el original).
dieciséis):

Objetos iterables: __iter__ y __next__ | 905


Machine Translated by Google

# Archivo skipper_yield.py

clase SkipObject: def # Otro __iter__ + generador de rendimiento


__init__(self, envuelto): self.wrapped = # Alcance de la instancia retenido normalmente
envuelto def __iter__(self): offset # Estado de ámbito local guardado automáticamente

= 0 while offset < len(self.wrapped):


item = self.wrapped[offset] offset
+= 2 yield item

Esto funciona igual que la versión multiescaneo sin rendimiento , pero con menos código y menos explícito:

% python
>>> from skipper_yield import SkipObject >>> skipper
= SkipObject('abcdef')
>>> I = iter(skipper) >>>
siguiente(I); siguiente yo); siguiente (I)
'a' 'c' 'e'

>>> para x en patrón: para # cada uno para llamadas __iter__: nuevo generador automático
y en patrón: print(x +
y, end=' ')

aa ac ae ca cc ce ea ec ee

Por supuesto, todos estos son ejemplos artificiales que podrían reemplazarse con herramientas más simples como
comprensiones, y su código puede o no escalarse a tareas más realistas.
Estudie estas alternativas para ver cómo se comparan. Como suele suceder en la programación, ¡la mejor herramienta
para el trabajo probablemente será la mejor herramienta para su trabajo!

Membresía: __contains__, __iter__ y __getitem__


La historia de la iteración es aún más rica de lo que hemos visto hasta ahora. La sobrecarga de operadores a menudo
tiene capas: las clases pueden proporcionar métodos específicos o alternativas más generales que se usan como
opciones de respaldo. Por ejemplo: • Las comparaciones en Python 2.X usan métodos específicos como __lt__ para

“menor que” si está presente, o bien el __cmp__ general. Python 3.X usa solo métodos específicos, no __cmp__,
como se explica más adelante en este capítulo.

• De manera similar, las pruebas booleanas prueban primero con un __bool__ específico (para dar un resultado
verdadero/falso explícito ) y, si está ausente, recurren al __len__ más general (una longitud distinta de cero
significa verdadero). Como también veremos más adelante en este capítulo, Python 2.X funciona igual pero
usa el nombre __nonzero__ en lugar de __bool__.

En el dominio de las iteraciones, las clases pueden implementar el operador de pertenencia como una iteración,
utilizando los métodos __iter__ o __getitem__ . Sin embargo, para admitir una membresía más específica, las clases
pueden codificar un método __contains__ ; cuando está presente, este

906 | Capítulo 30: Sobrecarga del operador


Machine Translated by Google

Se prefiere el método sobre __iter__, que se prefiere sobre __getitem__. El __con


El método tains__ debe definir la membresía como aplicada a las claves para un mapeo (y puede
usar búsquedas rápidas) y como una búsqueda de secuencias.

Considere la siguiente clase, cuyo archivo ha sido instrumentado para uso dual 2.X/3.X
utilizando las técnicas descritas anteriormente. Codifica los tres métodos y prueba la membresía.
y varios contextos de iteración aplicados a una instancia. Sus métodos imprimen mensajes de rastreo
cuando se llama:

# El archivo contiene.py
de __futuro__ importar print_function # Compatibilidad 2.X/ 3.X

iters de clase:
def __init__(uno mismo, valor):
self.data = valor

def __getitem__(self, i): print('get[%s]:' # Respaldo para la iteración


% i, end='') return self.data[i] # También para índice, corte

def __iter__(self): # Preferido para iteración


print('iter=> ', end='') self.ix = 0 # Permite solo un iterador activo

regresar a sí mismo

def __siguiente__(uno mismo):


imprimir('siguiente:', fin='')
if self.ix == len(self.data): aumentar StopIteration
item = self.datos[self.ix]
self.ix += 1
Devolver objeto

def __contiene__(uno mismo, x): # Preferido por 'en'


imprimir('contiene: ', end='')
devolver x en self.data
siguiente = __siguiente__ # Compatibilidad 2.X/ 3.X

si __nombre__ == '__principal__':
X = Iters([1, 2, 3, 4, 5]) print(3 en X) # Hacer instancia
para i en X: # Afiliación
# para bucles
imprimir(i, fin=' | ')

impresión()
imprimir ([i ** 2 para i en X]) imprimir # Otros contextos de iteración
(lista (mapa (bin, X)))

I = iter(X) while # Iteración manual (lo que hacen otros contextos)


True:
probar:
imprimir(siguiente(I), fin=' @ ')
excepto StopIteration:
descanso

Membresía: __contains__, __iter__ y __getitem__ | 907


Machine Translated by Google

Tal como está, la clase en este archivo tiene un __iter__ que admite múltiples escaneos, pero solo un
solo escaneo puede estar activo en cualquier momento (por ejemplo, los bucles anidados no
funcionarán), porque cada intento de iteración restablece el cursor de escaneo al frente. Ahora que
conoce el rendimiento en los métodos de iteración, debería poder decir que lo siguiente es equivalente
pero permite múltiples escaneos activos, y juzgue por sí mismo si su naturaleza más implícita vale la
pena el soporte de escaneo anidado y seis líneas afeitadas (esto es en el archivo contiene_rendimiento.py):
iters de clase:
def __init__(self, valor): self.data =
valor

def __getitem__(self, i): print('get[%s]:' # Respaldo para la iteración


% i, end='') return self.data[i] # También para índice, corte

def __iter__(self): # Preferido para iteración


print('iter=> next:', end='') for x in self.data: # Permite múltiples iteradores activos #
yield x print('siguiente:', end='') no __next__ to alias to next

def __contiene__(uno mismo, x): # Preferido por 'en'


print('contiene: ', end='') devuelve x en
self.data

Tanto en Python 3.X como en 2.X, cuando cualquiera de las versiones de este archivo ejecuta su salida
es la siguiente: el __contains__ específico intercepta la membresía, el __iter__ general captura otros
contextos de iteración de modo que __next__ (ya sea codificado explícitamente o implícito en el
rendimiento) es llamado repetidamente, y __getitem__ nunca se llama:
contiene: True iter=>
next:1 | siguiente:2 | siguiente:3 | siguiente:4 | siguiente:5 | siguiente: iter=>
siguiente:siguiente:siguiente:siguiente:siguiente:siguiente:[1, 4, 9, 16, 25] iter=>
siguiente:siguiente:siguiente:siguiente:siguiente:siguiente:['0b1', '0b10 ', '0b11', '0b100', '0b101'] iter=> siguiente:1
@ siguiente:2 @ siguiente:3 @ siguiente:4 @ siguiente:5 @ siguiente:

Sin embargo, observe lo que sucede con la salida de este código si comentamos su método
__contains__ ; la membresía ahora se enruta al __iter__ general en su lugar:
iter=>
siguiente:siguiente:siguiente:Verdadero iter=> siguiente:1 | siguiente:2 |
siguiente:3 | siguiente:4 | siguiente:5 | siguiente: iter=>
siguiente:siguiente:siguiente:siguiente:siguiente:siguiente:[1, 4, 9, 16, 25] iter=>
siguiente:siguiente:siguiente:siguiente:siguiente:siguiente:['0b1', '0b10 ', '0b11', '0b100', '0b101'] iter=> siguiente:1 @ siguiente:2 @ siguiente:3 @ siguien

Y finalmente, aquí está el resultado si tanto __contains__ como __iter__ están comentados: se llama al
respaldo de indexación __getitem__ con índices sucesivamente más altos hasta que genera IndexError,
para membresía y otros contextos de iteración:
obtener[0]:obtener[1]:obtener[2]:Verdadero
obtener[0]:1 | obtener[1]:2 | obtener[2]:3 | obtener[3]:4 | obtener[4]:5 | obtener[5]:
obtener[0]:obtener[1]:obtener[2]:obtener[3]:obtener[4]:obtener[5]:[1, 4, 9, 16, 25]
obtener[0] :get[1]:get[2]:get[3]:get[4]:get[5]:['0b1', '0b10', '0b11', '0b100','0b101'] get[0 ]:1 @ obtener[1]:2 @ obtener[2]:3
@ obtener[3]:4 @ obtener[4]:5 @ obtener[5]:

908 | Capítulo 30: Sobrecarga del operador


Machine Translated by Google

Como hemos visto, el método __getitem__ es aún más general: además de las iteraciones, también
intercepta la indexación explícita y el corte. Las expresiones de división activan __getitem__ con
un objeto de segmento que contiene límites, tanto para tipos incorporados como para clases definidas por el usuario, por lo que
el corte es automático en nuestra clase:

>>> from contiene iters de importación


>>> X = Iters('spam') # Indexación
>>> X[0] # __getitem__(0)
obtener[0]:'s'

>>> 'correo no # Sintaxis de corte


deseado'[1:] 'pam'
>>> 'spam'[segmento(1, Ninguno)] 'pam' # Rebanar objeto

>>> X[1:] # __getitem__(rebanada(..))


get[segmento(1, Ninguno, Ninguno)]:'pam'
>>> X[:-1]
get[segmento(Ninguno, ÿ1, Ninguno)]:'spa'

>>> lista(X) # ¡Y la iteración también!


iter=> siguiente:siguiente:siguiente:siguiente:siguiente:['s', 'p', 'a', 'm']

Sin embargo, en casos de uso de iteración más realistas que no están orientados a la secuencia, el
El método __iter__ puede ser más fácil de escribir ya que no debe administrar un índice entero, y
__contains__ permite la optimización de membresía como un caso especial.

Acceso a atributos: __getattr__ y __setattr__


En Python, las clases también pueden interceptar el acceso a atributos básicos (también conocido como calificación) cuando
necesario o útil. Específicamente, para un objeto creado a partir de una clase, el código también puede
implementar la expresión del operador de punto object.attribute , para contextos de referencia, asignación
y eliminación. Vimos un ejemplo limitado en esta categoría en el Capítulo 28, pero revisaremos y
ampliaremos el tema aquí.

Referencia de atributos
El método __getattr__ intercepta referencias de atributos. Se llama con el atributo
nombre como una cadena cada vez que intenta calificar una instancia con un indefinido (inexistente)
Nombre del Atributo. No se llama si Python puede encontrar el atributo usando su árbol de herencia.
procedimiento de busqueda.

Debido a su comportamiento, __getattr__ es útil como gancho para responder al atributo


solicitudes de forma genérica. Se usa comúnmente para delegar llamadas a integrados (o
“envueltos”) de un objeto de controlador proxy, del tipo presentado en la introducción a la delegación del
Capítulo 28 . Este método también se puede utilizar para adaptar las clases a un
interfaz, o agregar accesores para atributos de datos después del hecho: lógica en un método que
valida o calcula un atributo después de que ya se está utilizando con notación de punto simple.

Acceso a atributos: __getattr__ y __setattr__ | 909


Machine Translated by Google

El mecanismo básico que subyace a estos objetivos es sencillo: la siguiente clase captura las referencias
de atributos, calcula el valor de una de forma dinámica y activa un error para otras que no son compatibles
con la declaración de aumento descrita anteriormente en este capítulo para los iteradores (y cubierta por
completo en la Parte VII). : >>> class Empty: def __getattr__(self, attrname): if attrname == 'edad':
return 40 else: raise AttributeError(attrname)
# En self.indefinido

>>> X = Vacío()
>>> X.edad
40 >>>

X.nombre ...texto de error omitido...


Error de atributo: nombre

Aquí, la clase Empty y su instancia X no tienen atributos reales propios, por lo que el acceso a X.age se
enruta al método __getattr__ ; a self se le asigna la instancia (X) y a attrname se le asigna la cadena de
nombre de atributo indefinido ('edad'). La clase hace que la edad parezca un atributo real al devolver un
valor real como resultado de la expresión de calificación X.age (40). En efecto, la edad se convierte en un
atributo computado dinámicamente : su valor se forma ejecutando código, no recuperando un objeto.

Para los atributos que la clase no sabe cómo manejar, __getattr__ genera la excepción integrada
AttributeError para decirle a Python que estos son nombres indefinidos de buena fe; preguntar por X.name
desencadena el error. Verá __getattr__ nuevamente cuando veamos la delegación y las propiedades en
funcionamiento en los próximos dos capítulos; pasemos a las herramientas relacionadas aquí.

Asignación y eliminación de atributos En el

mismo departamento, el __setattr__ intercepta todas las asignaciones de atributos. Si este método se define
o se hereda, self.attr = value se convierte en self.__setattr__('attr', value). Al igual que __getattr__, esto le
permite a su clase detectar cambios de atributos y validar
o transformar como se desee.

Sin embargo, este método es un poco más complicado de usar, porque la asignación a cualquier atributo
propio dentro de __setattr__ llama a __setattr__ nuevamente, lo que puede causar un bucle de recurrencia
infinito (¡y una excepción de desbordamiento de pila bastante rápida!). De hecho, esto se aplica a todas las
asignaciones de autoatributos en cualquier parte de la clase: todas se enrutan a __setattr__, incluso
aquellas en otros métodos, y aquellas a nombres que no sean los que pueden haber activado __setattr__
en primer lugar. Recuerde, esto captura todas las asignaciones de atributos.

Si desea utilizar este método, puede evitar los bucles codificando atributos de instancias como asignaciones
a claves de diccionario de atributos. Es decir, use self.__dict__['name'] = x, no self.name = x; debido a que
no está asignando a __dict__ en sí mismo, esto evita el ciclo:

910 | Capítulo 30: Sobrecarga del operador


Machine Translated by Google

>>> clase control de acceso:


def __setattr__(self, attr, value): if attr ==
'edad': self.__dict__[attr] = valor + 10
else: raise AttributeError(attr + # No self.name=val o setattr

'
No permitido')

>>> X = Control de acceso()


>>> X.edad = 40 # Llamadas __setattr__
>>> X.edad 50
>>> X.nombre =
'Bob' ...texto omitido...

AttributeError: nombre no permitido

Si cambia la asignación __dict__ en esto a cualquiera de las siguientes, activa el bucle de recursión infinita y la
excepción: tanto la notación de puntos como su función integrada equivalente setattr (la asignación análoga de
getattr) fallan cuando la edad se asigna fuera de la clase :

self.edad = valor + 10 # Bucles


setattr(self, atributo, valor + 10) # Bucles (attr es 'edad')

Una asignación a otro nombre dentro de la clase también desencadena una llamada __setattr__ recursiva , aunque
en esta clase termina menos dramáticamente en la excepción manual AttributeError :

yo.otro = 99 # Se repite pero no se repite: falla

También es posible evitar bucles recursivos en una clase que usa __setattr__ enrutando cualquier asignación de
atributos a una superclase superior con una llamada, en lugar de asignar claves en __dict__: self.__dict__[attr] =
valor + 10 objeto.__setattr__(self, attr , valor + 10)

# OK: no se repite
# OK: no se repite (solo estilo nuevo)

Sin embargo, debido a que la forma de objeto requiere el uso de clases de estilo nuevo en 2.X, pospondremos los
detalles de esta forma hasta la revisión más profunda del Capítulo 38 sobre la administración de atributos en general.

Un tercer método de gestión de atributos, __delattr__, se pasa la cadena de nombre de atributo y se invoca en todas
las eliminaciones de atributos (es decir, del object.attr). Al igual que __setattr__, debe evitar los bucles recursivos al
enrutar las eliminaciones de atributos con la clase de uso a través de __dict__ o una superclase.

Como veremos en el Capítulo 32, los atributos implementados con funciones de


clase de nuevo estilo, como espacios y propiedades , no se almacenan físicamente
en el diccionario de espacio de nombres __dict__ de la instancia (¡y los espacios
pueden incluso impedir su existencia por completo!). Debido a esto, el código que
desea admitir dichos atributos debe codificar __setattr__ para asignar con el
esquema object.__setattr__ que se muestra aquí, no mediante la indexación
self.__dict__ a menos que se sepa que las clases de sujeto almacenan todos sus
datos en la instancia misma. En el Capítulo 38 también veremos que el nuevo estilo __getattribute__

Acceso a atributos: __getattr__ y __setattr__ | 911


Machine Translated by Google

tiene requisitos similares. Este cambio es obligatorio en Python 3.X, pero también se
aplica a 2.X si se usan clases de nuevo estilo.

Otras herramientas de administración de

atributos Estos tres métodos de sobrecarga de acceso a atributos le permiten controlar o especializar el
acceso a atributos en sus objetos. Tienden a desempeñar papeles muy especializados, algunos de los cuales
exploraremos más adelante en este libro. Para ver otro ejemplo de __getattr__ en funcionamiento, consulte
person-composite.py del Capítulo 28 . Y para referencia futura, tenga en cuenta que hay otras formas de
administrar el acceso a los atributos en Python:

• El método __getattribute__ intercepta todas las extracciones de atributos, no solo aquellas que no están
definidas, pero al usarlo debe tener más cuidado que con __get attr__ para evitar bucles.

• La función incorporada de propiedad nos permite asociar métodos con operaciones de búsqueda y
establecimiento en un atributo de clase específico . • Los descriptores proporcionan un protocolo para

asociar los métodos __get__ y __set__ de un


clase con accesos a un atributo de clase específico .

• Los atributos de las ranuras se declaran en clases pero crean almacenamiento implícito en cada instancia.

Debido a que estas son herramientas un tanto avanzadas que no son de interés para todos los programadores
de Python, aplazaremos un vistazo a las propiedades hasta el Capítulo 32 y una cobertura detallada de todas
las técnicas de administración de atributos hasta el Capítulo 38.

Emulación de la privacidad para atributos de instancia: Parte 1

Como otro caso de uso para dichas herramientas, el siguiente código, archivo private0.py, generaliza el
ejemplo anterior para permitir que cada subclase tenga su propia lista de nombres privados que no se pueden
asignar a sus instancias ( y usa una clase de excepción definida por el usuario, que tendrá que tomar con fe
hasta la Parte VII): class PrivateExc(Exception): pass

# Más sobre excepciones en la Parte VII

class Privacy: def


__setattr__(self, attrname, value): if attrname in # On self.atributo = valor
self.privates: raise PrivateExc(attrname, self) else:
self.__dict__[attrname] = valor # Hacer, subir definido por el usuario excepto

# Evite los bucles usando la tecla dict

clase Test1(Privacidad):
privados = ['edad']

class Test2(Privacidad):
privados = ['nombre', 'pagar'] def
__init__(self): self.__dict__['name'] =
'Tom' # ¡Para hacerlo mejor, vea el Capítulo 39!

912 | Capítulo 30: Sobrecarga del operador


Machine Translated by Google

si __nombre__ == '__principal__':
x = Prueba1() y = Prueba2()

x.nombre = 'Bob' # Obras


#y.nombre = 'Sue' # falla
print(x.nombre)

y.edad = 30 # Obras
#x.edad = 40 # falla
print(y.edad)

De hecho, esta es una solución de primer corte para una implementación de privacidad de atributos
en Python, que no permite cambios en los nombres de atributos fuera de una clase. Aunque Python
no admite declaraciones privadas per se, técnicas como esta pueden emular gran parte de su
propósito.

Sin embargo, esta es una solución parcial e incluso torpe; para que sea más efectivo, debemos
aumentarlo para permitir que las clases establezcan sus atributos privados de manera más natural,
sin tener que pasar por __dict__ cada vez, como debe hacer el constructor aquí para evitar activar
__setattr__ y una excepción. Un enfoque mejor y más completo podría requerir una clase
contenedora ("proxy") para verificar los accesos a atributos privados realizados solo fuera de la
clase, y un __getattr__ para validar también las recuperaciones de atributos.

Pospondremos una solución más completa para atribuir la privacidad hasta el Capítulo 39, donde
usaremos decoradores de clase para interceptar y validar atributos de manera más general. Aunque
la privacidad se puede emular de esta manera, casi nunca se pone en práctica. Los programadores
de Python pueden escribir marcos y aplicaciones OOP grandes sin declaraciones privadas, un
hallazgo interesante sobre los controles de acceso en general que está más allá del alcance de
nuestros propósitos aquí.

Aun así, la captura de referencias y asignaciones de atributos suele ser una técnica útil; admite la
delegación, una técnica de diseño que permite que los objetos del controlador envuelvan objetos
incrustados, agreguen nuevos comportamientos y enruten otras operaciones de regreso a los objetos
envueltos. Debido a que involucran temas de diseño, revisaremos las clases de delegación y
envoltura en el próximo capítulo.

Representación de cadenas: __repr__ y __str__ Nuestros próximos

métodos se ocupan de los formatos de visualización, un tema que ya hemos explorado en capítulos
anteriores, pero que resumiremos y formalizaremos aquí. Como revisión, el siguiente código ejecuta
el constructor __init__ y el método de sobrecarga __add__ , los cuales ya hemos visto (+ es una
operación en el lugar aquí, solo para mostrar que puede serlo; según el Capítulo 27, un puede
preferirse el método mencionado). Como hemos aprendido, la visualización predeterminada de
objetos de instancia para una clase como esta no es generalmente útil ni estéticamente bonita:
>>> sumador de
clases: def __init__(self, valor=0):
self.data = valor # Inicializar datos

Representación de cadenas: __repr__ y __str__ | 913


Machine Translated by Google

def __add__(uno mismo, otro):


self.data += otro # Agregue otro en su lugar (¿mal formato?)

>>> x = sumador() # Pantallas predeterminadas

>>> imprimir(x)
<__main__.objeto sumador en 0x00000000029736D8>
>>> x

<__main__.objeto sumador en 0x00000000029736D8>

Pero codificar o heredar métodos de representación de cadenas nos permite personalizar la visualización, como a
continuación, que define un método __repr__ en una subclase que devuelve
una representación de cadena para sus instancias.

>>> class addrepr(sumador): def # Heredar __init__, __add__


__repr__(self): return # Agregar representación de cadena
'addrepr(%s)' % self.data # Convertir a cadena como código

>>> x = adrepr(2) # Ejecuta __init__


>>> x + 1 # Ejecuta __add__ (¿x.add() mejor?)
>>> x # Ejecuta __repr__
dirección(3)
>>> imprimir(x) # Ejecuta __repr__
addrepr(3)
>>> str(x), repr(x) # Ejecuta __repr__ para ambos
('addrepr(3)', 'addrepr(3)')

Si está definido, __repr__ (o su pariente cercano, __str__) se llama automáticamente cuando la clase
las instancias se imprimen o se convierten en cadenas. Estos métodos le permiten definir una mejor
formato de visualización para sus objetos que la visualización de la instancia predeterminada. Aquí, __repr__ usa
formato de cadena básico para convertir el objeto self.data administrado en una cadena más amigable para los
humanos para su visualización.

¿Por qué dos métodos de visualización?

Hasta ahora, lo que hemos visto es en gran medida revisión. Pero aunque estos métodos son generalmente sencillos
de usar, sus roles y comportamiento tienen algunas implicaciones sutiles tanto para el diseño
y codificación. En particular, Python proporciona dos métodos de visualización para admitir alternativas
Expositores para diferentes públicos:

• __str__ se prueba primero para la operación de impresión y la función incorporada str (el equivalente interno de
la cual se ejecuta la impresión ). Por lo general, debe devolver un fácil de usar
monitor.

__repr__ se usa en todos los demás contextos: para ecos interactivos, la función repr y
apariencias anidadas, así como por print y str si no hay __str__ presente. Debería
generalmente devuelve una cadena como código que podría usarse para volver a crear el objeto, o una
visualización detallada para los desarrolladores.

Es decir, __repr__ se usa en todas partes, excepto por print y str cuando se define __str__ .
Esto significa que puede codificar un __repr__ para definir un único formato de visualización utilizado en todas partes,

914 | Capítulo 30: Sobrecarga del operador


Machine Translated by Google

y puede codificar un __str__ para admitir print y str exclusivamente, o para proporcionarles una visualización
alternativa.

Como se señaló en el Capítulo 28, las herramientas generales también pueden preferir __str__ para dejar a
otras clases la opción de agregar una pantalla __repr__ alternativa para usar en otros contextos, siempre
que las pantallas print y str sean suficientes para la herramienta. Por el contrario, una herramienta general
que codifica un __repr__ todavía deja a los clientes la opción de agregar pantallas alternativas con un
__str__ para imprimir y str. En otras palabras, si codifica cualquiera de los dos, el otro está disponible para
una visualización adicional. En los casos en que la elección no está clara, generalmente se prefiere __str__
para pantallas más grandes y fáciles de usar, y __repr__ para pantallas de nivel inferior o como código y
roles con todo incluido.

Escribamos algo de código para ilustrar las distinciones de estos dos métodos en términos más concretos.
El ejemplo anterior en esta sección mostró cómo __repr__ se usa como opción alternativa en muchos
contextos. Sin embargo, mientras que la impresión recurre a __repr__ si no se define __str__ , lo contrario
no es cierto: otros contextos, como los ecos interactivos, usan solo __repr__ y no intentan __str__ en
absoluto:

>>> class addstr(sumador): def


__str__(self): return # __str__ pero no __repr__
'[Valor: %s]' % self.data # Convertir a buena cadena

>>> x = suma(3)
>>> x + 1
>>> x # Predeterminado __repr__
<__main__.objeto addstr en 0x00000000029738D0> >>> imprimir(x)
# Ejecuta __str__
[Valor: 4] >>>
cadena(x), repr(x)
('[Valor: 4]', '<__main__.objeto addstr en 0x00000000029738D0>')

Debido a esto, __repr__ puede ser mejor si desea una visualización única para todos los contextos. Sin
embargo, al definir ambos métodos, puede admitir diferentes pantallas en diferentes contextos, por ejemplo,
una pantalla de usuario final con __str__ y una pantalla de bajo nivel para que los programadores la usen
durante el desarrollo con __repr__. En efecto, __str__ simplemente anula __repr__ para obtener contextos
de visualización más fáciles de usar:

>>> class addboth(sumador): def


__str__(self): return
'[Valor: %s]' % self.data def __repr__(self): # Cadena fácil de usar
return 'addboth(%s)' % self.data
# Cadena como código

>>> x = sumar ambos(4)


>>> x + 1
>>> x # Ejecuta __repr__
sumar
ambos(5) >>> imprimir(x) # Ejecuta __str__
[Valor: 5] >>>
cadena(x), repr(x)
('[Valor: 5]', 'añadir ambos (5)')

Representación de cadenas: __repr__ y __str__ | 915


Machine Translated by Google

Mostrar notas de uso Aunque

generalmente es simple de usar, debo mencionar aquí tres notas de uso con respecto a estos métodos. Primero, tenga
en cuenta que __str__ y __repr__ deben devolver cadenas; otros tipos de resultados no se convierten y generan errores,
así que asegúrese de ejecutarlos a través de un convertidor de cadena (por ejemplo, str o %) si es necesario.

En segundo lugar, dependiendo de la lógica de conversión de cadenas de un contenedor, la visualización fácil de usar
de __str__ solo se puede aplicar cuando los objetos aparecen en el nivel superior de una operación de impresión; los
objetos anidados en objetos más grandes aún pueden imprimirse con su __repr__ o su valor predeterminado. A
continuación se ilustran ambos puntos:

>>> clase Impresora:


def __init__(self, val): self.val =
val def __str__(self): return
str(self.val) # Usado para la instancia misma
# Convertir a un resultado de cadena

>>> objs = [Impresora(2), Impresora(3)] >>> for x


en objs: print(x) # __str__ se ejecuta cuando se imprime la instancia
# ¡Pero no cuando la instancia está en una lista!
2
3
>>> print(objs)
[<__main__.Objeto de impresora en 0x000000000297AB38>, <__main__.Objeto de impresora ...etc...>] >>> objs
[<__main__.Objeto de impresora en 0x000000000297AB38>, <__main__. Objeto de impresora ...etc...>]

Para garantizar que se ejecute una visualización personalizada en todos los contextos, independientemente del
contenedor, codifique __repr__, no __str__; el primero se ejecuta en todos los casos si el último no se aplica, incluidas
las apariencias anidadas:

>>> clase Impresora: def


__init__(self, val): self.val = val

def __repr__(auto): return # __repr__ usado por impresión si no hay


str(auto.val) __str__ # __repr__ usado si se repite o anida

>>> objs = [Impresora(2), Impresora(3)] >>> for x


en objs: print(x) # No __str__: ejecuta __repr__

2
3
>>> imprimir(objs) # Ejecuta __repr__, no ___str__
[2, 3] >>> objs [2, 3]

En tercer lugar, y quizás lo más sutil, los métodos de visualización también tienen el potencial de desencadenar bucles
de recurrencia infinitos en contextos excepcionales: debido a que las visualizaciones de algunos objetos incluyen
visualizaciones de otros objetos, no es imposible que una visualización pueda desencadenar la visualización de un
objeto que se está visualizando. , y por lo tanto bucle. Esto es lo suficientemente raro y oscuro como para omitirlo aquí, pero observe

916 | Capítulo 30: Sobrecarga del operador


Machine Translated by Google

para ver un ejemplo de este potencial de bucle para estos métodos en una nota cerca del final del próximo
capítulo en su clase de ejemplo listinherited.py , donde __repr__ puede hacer un bucle.

En la práctica, __str__ y su pariente más inclusivo __repr__ parecen ser los segundos métodos de
sobrecarga de operadores más utilizados en los scripts de Python, detrás de __init__. Siempre que pueda
imprimir un objeto y ver una visualización personalizada, es probable que una de estas dos herramientas
esté en uso. Para obtener ejemplos adicionales de estas herramientas en funcionamiento y las
compensaciones de diseño que implican, consulte el estudio de caso del Capítulo 28 y las mezclas de
listas de clases del Capítulo 31, así como su función en las clases de excepción del Capítulo 35, donde se requiere __str__ sobr
__repr__.

Usos en el lado derecho y en el lugar: __radd__ y __iadd__


Nuestro próximo grupo de métodos de sobrecarga amplía la funcionalidad de los métodos de operadores
binarios como __add__ y __sub__ (llamados por + y -), que ya hemos visto.
Como se mencionó anteriormente, parte de la razón por la que hay tantos métodos de sobrecarga de
operadores es porque vienen en múltiples sabores: para cada expresión binaria, podemos implementar
una variante izquierda, derecha e in situ . Aunque los valores predeterminados también se aplican si no
codifica los tres, los roles de sus objetos dictan cuántas variantes necesitará codificar.

Adición del lado derecho


Por ejemplo, los métodos __add__ codificados hasta ahora técnicamente no admiten el uso de objetos de
instancia en el lado derecho del operador + :
>>> sumador de clase:
def __init__(self, valor=0):
self.data = valor def
__add__(self, otro):
volver self.data + otro

>>> x = sumador(5)
>>> x + 2

7 >>> 2 + x
TypeError: tipos de operandos no admitidos para +: 'int' y 'Adder'

Para implementar expresiones más generales y, por lo tanto, admitir operadores de estilo conmutativo ,
codifique también el método __radd__ . Python llama a __radd__ solo cuando el objeto del lado derecho
del + es la instancia de su clase, pero el objeto de la izquierda no es una instancia de su clase. En cambio,
se llama al método __add__ para el objeto de la izquierda en todos los demás casos (las cinco clases de
Commuter de esta sección están codificadas en el archivo commuter.py en los ejemplos del libro, junto
con una autoevaluación):
class Commuter1:
def __init__(self, val): self.val
= val def __add__(self,
otro): print('add', self.val, otro)

Usos en el lado derecho y en el lugar: __radd__ y __iadd__ | 917


Machine Translated by Google

volver self.val + otro


def __radd__(self, otro):
print('radd', self.val, otro) return otro +
self.val

>>> from viajero import Viajero1 >>> x =


Viajero1(88) >>> y = Viajero1(99)

>>> x + 1 # __add__: instancia + no instancia


suma 88 1
89
>>> 1 + y # __radd__: no instancia + instancia
radd 99 1
100 #
object __add__: instancia + instancia,
at 0x00000000029B39E8> raddactiva
99 88__radd__
187 >>> x + y add 88 <commuter.Commuter1

Observe cómo se invierte el orden en __radd__: el yo está realmente a la derecha del + y el otro está
a la izquierda. También tenga en cuenta que x e y son instancias de la misma clase aquí; cuando las
instancias de diferentes clases aparecen mezcladas en una expresión, Python prefiere la clase de la
izquierda. Cuando sumamos las dos instancias, Python ejecuta __add__, que a su vez activa __radd__
al simplificar el operando izquierdo.

Reutilización de __add__ en

__radd__ Para operaciones realmente conmutativas que no requieren mayúsculas y minúsculas por
posición, a veces también es suficiente reutilizar __add__ para __radd__: ya sea llamando a __add__
directamente; cambiando el orden y volviendo a agregar para activar __add__ indirectamente; o
simplemente asignando __radd__ para que sea un alias para __add__ en el nivel superior de la
declaración de clase (es decir, en el ámbito de la clase). Las siguientes alternativas implementan estos
tres esquemas y devuelven los mismos resultados que el original, aunque la última ahorra una llamada
o envío adicional y, por lo tanto, puede ser más rápida (en total, __radd__ se ejecuta cuando self está
en el lado derecho de un +):
clase Commuter2:
def __init__(self, val): self.val
= val
def __add__(self, otro):
print('add', self.val, otro) return
self.val + otro
def __radd__(uno mismo, otro):
volver self.__add__(otro) # Llamar a __add__ explícitamente

clase Commuter3:
def __init__(self, val): self.val
= val
def __add__(self, otro):
print('add', self.val, otro) return
self.val + otro
def __radd__(uno mismo, otro):

918 | Capítulo 30: Sobrecarga del operador


Machine Translated by Google

volver yo + otro # Intercambiar orden y volver a agregar

class Commuter4: def


__init__(self, val): self.val = val
def __add__(self, otro):
print('add', self.val, other) return
self.val + other __radd__ = __add__

# Alias: elimina al intermediario

En todos estos, las apariencias de las instancias del lado derecho activan el método __add__ único y
compartido , pasando el operando derecho a self, para que se trate igual que una apariencia del lado izquierdo.
Ejecute estos por su cuenta para obtener más información; sus valores devueltos son los mismos que los originales.

Propagación del tipo

de clase En clases más realistas en las que el tipo de clase puede necesitar propagarse en los resultados,
las cosas pueden volverse más complicadas: es posible que se requieran pruebas de tipo para saber si es
seguro convertir y así evitar el anidamiento. Por ejemplo, sin la prueba isinstance a continuación, podríamos
terminar con un Commuter5 cuyo valor es otro Commuter5 cuando se agregan dos instancias y __add__
desencadena __radd__:

class Commuter5: def # Propagar el tipo de clase en los resultados


__init__(self, val): self.val = val
def __add__(self, otro): if
isinstance(otro, Commuter5): otro
= otro.val return Commuter5(self.val + otro) # Prueba de tipo para evitar el anidamiento de objetos
def __radd__(self , otro): return
Commuter5(otro + self.val) def __str__(self): # Else + result is another Commuter
return '<Commuter5: %s>' % self.val

>>> from viajero import Viajero5 >>> x =


Viajero5(88) >>> y = Viajero5(99) >>> print(x
+ 10)
# El resultado es otra instancia de Commuter
<Viajero5: 98>
>>> imprimir(10 + y)
<Viajero5: 109>

>>> z = x + y >>> # No anidado: no recurre a __radd__


imprimir(z)
<Viajero5: 187> >>>
imprimir(z + 10)
<Viajero5: 197> >>>
imprimir(z + z)
<Viajero5: 374> >>>
imprimir(z + z + 1)
<Viajero5: 375>

La necesidad de la prueba de tipo isinstance aquí es muy sutil: descomente, ejecute y rastree para ver por
qué es necesaria. Si lo hace, verá que la última parte de la prueba anterior

Usos en el lado derecho y en el lugar: __radd__ y __iadd__ | 919


Machine Translated by Google

termina con objetos diferentes y anidados, que aún hacen los cálculos correctamente, pero inician llamadas
recursivas sin sentido para simplificar sus valores, y las llamadas de constructor adicionales generan
resultados:

>>> z = x + y # Con la prueba isinstance comentada


>>> imprimir(z)
<Viaje5: <Viaje5: 187>>
>>> imprimir(z + 10)
<Viaje5: <Viaje5: 197>> >>> print(z +
z)
<Viaje5: <Viaje5: <Viaje5: <Viaje5: 374>>>> >>> print(z + z + 1)

<Viaje5: <Viaje5: <Viaje5: <Viaje5: 375>>>>

Para probar, el resto de commuter.py se ve y funciona así: las clases pueden aparecer en tuplas de forma
natural:

#!python
from __future__ import print_function ...clases # Compatibilidad 2.X/ 3.X
definidas aquí...

si __nombre__ == '__principal__':
for klass in (Commuter1, Commuter2, Commuter3, Commuter4, Commuter5): print('-' * 60) x
= klass(88) y = klass(99) print(x + 1) print(1 + y) print(x + y)

c:\code> commuter.py
-------------------------------------------------- ----------

suma 88
1 89 rad
99 1 100

añadir 88 <__principal__. Objeto Commuter1 en 0x000000000297F2B0>


radd 99 88 187

-------------------------------------------------- ----------

...etc...

Hay demasiadas variaciones de codificación para explorar aquí, así que experimente con estas clases por
su cuenta para obtener más información; Aliasing __radd__ a __add__ en Commuter5, por ejemplo, guarda
una línea, pero no evita el anidamiento de objetos sin isinstance. Ver también los manuales de Python para
una discusión de otras opciones en este dominio; por ejemplo, las clases también pueden devolver el objeto
especial NotImplemented para operandos no admitidos para influir en la selección del método (esto se trata
como si el método no estuviera definido).

Adición en el lugar
Para implementar también la suma aumentada += en el lugar, codifique un __iadd__ o un __add__. Este
último se utiliza si el primero está ausente. De hecho, el Commuter de la sección anterior

920 | Capítulo 30: Sobrecarga del operador


Machine Translated by Google

las clases ya admiten += por este motivo: Python ejecuta __add__ y asigna el resultado manualmente. Sin
embargo, el método __iadd__ permite codificar cambios en el lugar más eficientes cuando corresponda:

>>> Número de clase:


def __init__(self, val): self.val =
val
def __iadd__(self, otro): self.val # __iadd__ explícito: x += y
+= otro return self # Por lo general, regresa a sí mismo

>>> x = Número(5)
>>> x += 1
>>> x += 1
>>> x.val
7

Para objetos mutables, este método a menudo puede especializarse para cambios más rápidos en el lugar:

>>> y = Número([1]) >>> # Cambio en el lugar más rápido que +


y += [2] >>> y += [3] >>>
y.val [1, 2, 3]

El método __add__ normal se ejecuta como respaldo, pero es posible que no pueda optimizarse en el lugar
casos:

>>> Número de clase:


def __init__(self, val): self.val =
val
def __add__(self, other): # __add__ fallback: x = (x + y) return Number(self.val + other)
# Propaga el tipo de clase

>>> x = Número(5)
>>> x += 1
>>> x += 1 # Y += hace concatenación aquí
>>> x.val
7

Aunque nos hemos centrado en + aquí, tenga en cuenta que cada operador binario tiene métodos similares de
sobrecarga en el lado derecho y en el lugar que funcionan de la misma manera (por ejemplo, __mul__,
__rmul__ y __imul__). Aún así, los métodos del lado derecho son un tema avanzado y tienden a ser poco
comunes en la práctica; solo los codifica cuando necesita que los operadores sean conmutativos, y solo si
necesita admitir tales operadores. Por ejemplo, una clase Vector puede usar estas herramientas, pero una
clase Empleado o Botón probablemente no lo haría.

Expresiones de llamada: __call__


En nuestro siguiente método de sobrecarga: se llama al método __call__ cuando se llama a su instancia. No,
esta no es una definición circular: si se define, Python ejecuta un método __call__ para las expresiones de
llamada de función aplicadas a sus instancias, pasando cualquier posición

Expresiones de llamada: __call__ | 921


Machine Translated by Google

Se enviaron argumentos opcionales o de palabras clave. Esto permite que las instancias se ajusten a una API basada
en funciones:

>>> clase Callee:


def __call__(self, *pargs, **kargs): # Interceptar llamadas de instancias
print('Llamado:', pargs, kargs) # Aceptar argumentos arbitrarios

>>> C = Calle ()
>>> C(1, 2, 3) # C es un objeto invocable
Llamado: (1, 2, 3) {}
>>> C(1, 2, 3, x=4, y=5)
Llamado: (1, 2, 3) {'y': 5, 'x': 4}

De manera más formal, todos los modos de paso de argumentos que exploramos en el Capítulo 18 son compatibles con
el método __call__ : todo lo que se pasa a la instancia se pasa a esta.
método, junto con el argumento de instancia implícito habitual. Por ejemplo, el método
definiciones:

clase C:
def __call__(self, a, b, c=5, d=6): ... # Normales y valores predeterminados

clase C:
def __call__(self, *pargs, **kargs): ... # Recoger argumentos arbitrarios

clase C:
def __call__(self, *pargs, d=6, **kargs): ... # 3.X argumento solo de palabra clave

todos coinciden con todas las siguientes llamadas de instancia:

X = C()
X(1, 2) # Omitir valores predeterminados

X(1, 2, 3, 4) # Posicionales
X(a=1, b=2, d=4) # Palabras clave
X(*[1, 2], **dict(c=3, d=4)) # Desempaquetar argumentos arbitrarios
X(1, *(2,), c=3, **dict(d=4)) # Modos mixtos

Consulte el Capítulo 18 para obtener un repaso de los argumentos de función. El efecto neto es que las clases y
las instancias con __llamada__ admiten exactamente la misma sintaxis y semántica de argumentos que
funciones y métodos normales.

Interceptar una expresión de llamada como esta permite que las instancias de clase emulen la apariencia
de cosas como funciones, pero también conservan información de estado para usar durante las llamadas. Nosotros vimos
un ejemplo similar al siguiente mientras explora los ámbitos en el Capítulo 17, pero
ahora debería estar lo suficientemente familiarizado con la sobrecarga de operadores para comprender este patrón
mejor:

>>> clase Prod:


def __init__(auto, valor): # Aceptar solo un argumento
auto.valor = valor
def __call__(uno mismo, otro):
devolver self.value * otro

>>> x = Prod(2) # "Recuerda" 2 en estado


>>> x(3) # 3 (aprobado) * 2 (estado)
6

922 | Capítulo 30: Sobrecarga del operador


Machine Translated by Google

>>> x(4) 8

En este ejemplo, la __llamada__ puede parecer un poco gratuita a primera vista. Un método simple puede
proporcionar una utilidad similar:
>>> clase Prod:
def __init__(auto, valor): auto.valor
= valor
def comp(self, other): return
self.value * other

>>> x = Prod(3)
>>> x.comp(3)
9
>>> x.comp(4)
12

Sin embargo, __call__ puede volverse más útil cuando interactúa con API (es decir, bibliotecas) que
esperan funciones: nos permite codificar objetos que se ajustan a una interfaz de llamada de función
esperada, pero también conservan información de estado y otros activos de clase como en herencia . De
hecho, puede ser el tercer método de sobrecarga de operadores más utilizado, detrás del constructor
__init__ y las alternativas de formato de visualización __str__ y __repr__ .

Interfaces de función y código basado en devolución de llamada

Como ejemplo, el kit de herramientas GUI de tkinter (denominado Tkinter en Python 2.X) le permite registrar
funciones como controladores de eventos (también conocidas como devoluciones de llamada) : cuando
ocurren eventos, tkinter llama a los objetos registrados. Si desea que un controlador de eventos conserve
el estado entre eventos, puede registrar el método vinculado de una clase o una instancia que se ajuste a
la interfaz esperada con __call__.

En el código de la sección anterior, por ejemplo, tanto x.comp del segundo ejemplo como x del primero
pueden pasar como objetos similares a funciones de esta manera. Las funciones de cierre del Capítulo 17
con el estado en los ámbitos adjuntos pueden lograr efectos similares, pero no brindan tanto soporte para
múltiples operaciones o personalización.

Tendré más que decir sobre los métodos vinculados en el próximo capítulo, pero por ahora, aquí hay un
ejemplo hipotético de __call__ aplicado al dominio GUI. La siguiente clase define un objeto que admite una
interfaz de llamada de función, pero también tiene información de estado que recuerda el color al que debe
cambiar un botón cuando se presiona más tarde:
Devolución de llamada de clase:

def __init__(self, color): self.color = # Función + información de estado


color
def __call__(self): # Llamadas de soporte sin argumentos
print('girar', self.color)

Expresiones de llamada: __call__ | 923


Machine Translated by Google

Ahora, en el contexto de una GUI, podemos registrar instancias de esta clase como controladores de eventos
para los botones, aunque la GUI espera poder invocar controladores de eventos tan simples
funciones sin argumentos:

# Manejadores

cb1 = Devolución de llamada('azul') # Recuerda azul

cb2 = Devolución de llamada('verde') # Recuerda verde

B1 = Botón (comando = cb1) # Registrar manejadores


B2 = Botón (comando = cb2)

Cuando se presiona el botón más tarde, el objeto de instancia se llama como una función simple con
sin argumentos, exactamente como en las siguientes llamadas. Porque conserva el estado como instancia.
atributos, sin embargo, recuerda qué hacer: se convierte en un objeto de función con estado :

# Eventos

cb1() # Imprime 'volverse azul'


cb2() # Imprime 'volverse verde'

De hecho, muchos consideran que estas clases son la mejor manera de retener información de estado en el
Lenguaje Python (según los principios pitónicos generalmente aceptados, al menos). Con la programación orientada a objetos, el
el estado recordado se hace explícito con asignaciones de atributos. esto es diferente a
otras técnicas de retención de estado (p. ej., variables globales, referencias de ámbito de función envolvente y argumentos
mutables predeterminados), que se basan en un comportamiento más limitado o implícito.
Además, la estructura agregada y la personalización en las clases van más allá de la retención del estado.

Por otro lado, herramientas como las funciones de cierre son útiles en la retención de estado básico.
roles también, y la declaración no local de 3.X hace que los ámbitos adjuntos sean una alternativa viable en
más programas Revisaremos tales compensaciones cuando comencemos a codificar decoradores sustanciales
en el Capítulo 39, pero aquí hay un equivalente de cierre rápido :

def callback(color): def oncall(): # Alcance envolvente versus attrs

imprimir ('girar', color)


volver de guardia

cb3 = devolución de llamada ('amarillo') # Manejador a registrar


cb3 () # En el evento: imprime 'se vuelve amarillo'

Antes de continuar, hay otras dos formas en que los programadores de Python a veces vinculan
información a una función de devolución de llamada como esta. Una opción es usar argumentos predeterminados en
funciones lambda :

cb4 = (lambda color='red': 'turn' + color) # Los valores predeterminados también conservan el estado
imprimir (cb4 ())

La otra es usar métodos enlazados de una clase, un poco como una vista previa, pero lo suficientemente simple como para
introducir aquí. Un objeto de método enlazado es un tipo de objeto que recuerda tanto el
auto instancia y la función referenciada. Por lo tanto, este objeto puede llamarse más tarde como
una función simple sin una instancia:

924 | Capítulo 30: Sobrecarga del operador


Machine Translated by Google

Devolución de llamada de clase:

def __init__(uno mismo, color): # Clase con información de estado


self.color = color

def changeColor(self): print('girar', # Un método con nombre normal

self.color)

cb1 = Devolución de llamada('azul')


cb2 = Devolución de llamada('amarillo')

B1 = Botón (comando = cb1.cambiarColor) # Método enlazado: referencia, no llamar


B2 = Botón (comando = cb2.cambiarColor) # Recuerda función + par propio

En este caso, cuando se presiona este botón más tarde, es como si la GUI hiciera esto, lo que invoca el
método changeColor de la instancia para procesar la información de estado del objeto, en lugar de la
instancia misma:

cb1 = Devolución de llamada('azul')


obj = cb1.cambiarColor obj() # Controlador de eventos registrado
# En el evento, las impresiones 'se vuelven azules'

Tenga en cuenta que no se requiere una lambda aquí, porque una referencia de método enlazada por sí
misma ya difiere una llamada hasta más tarde. Esta técnica es más simple, pero quizás menos general que
sobrecargar llamadas con __call__. Una vez más, busque más información sobre los métodos enlazados en
el próximo capítulo.

También verá otro ejemplo de __llamada__ en el Capítulo 32, donde lo usaremos para implementar algo
conocido como decorador de funciones: un objeto invocable que se usa a menudo para agregar una capa
de lógica sobre una función incrustada. Debido a que __call__ nos permite adjuntar información de estado
a un objeto invocable, es una técnica de implementación natural para una función que debe recordar llamar
a otra función cuando se llama a sí misma. Para obtener más ejemplos de __call__ , consulte los ejemplos
de vista previa de retención de estado en el Capítulo 17 y los decoradores y metaclases más avanzados de
los Capítulos 39 y 40.

Comparaciones: __lt__, __gt__ y otros Nuestro siguiente lote de métodos

de sobrecarga admite comparaciones. Como se sugiere en la Tabla 30-1, las clases pueden definir métodos
para capturar los seis operadores de comparación: <, >, <=, >=, == y !=. Estos métodos son generalmente
sencillos de usar, pero tenga en cuenta las siguientes calificaciones: • A diferencia de los pares __add__/
__radd__ discutidos anteriormente, no hay variantes del lado derecho de los métodos de comparación. En

cambio, los métodos reflexivos se usan cuando solo un operando admite la comparación (por ejemplo,
__lt__ y __gt__ son el reflejo del otro). • No hay relaciones implícitas entre los operadores de
comparación. La verdad de == no implica que != sea falso, por ejemplo, por lo que tanto __eq__ como

__ne__ deben definirse para garantizar que ambos operadores se comporten correctamente.

• En Python 2.X, todas las comparaciones utilizan un método __cmp__ si no se definen métodos de
comparación más específicos; devuelve un número que es menor, igual o

Comparaciones: __lt__, __gt__ y otros | 925


Machine Translated by Google

mayor que cero, para señalar los resultados menor que, igual y mayor que para la comparación de sus
dos argumentos (uno mismo y otro operando). Este método suele utilizar
el cmp(x, y) incorporado para calcular su resultado. Tanto el método __cmp__ como el
La función incorporada cmp se elimina en Python 3.X: use los métodos más específicos
en cambio.

No tenemos espacio para una exploración en profundidad de los métodos de comparación, pero como una rápida
introducción, considere la siguiente clase y código de prueba:
clase C:
datos = 'correo no deseado'

def __gt__(uno mismo, otro): # Versión 3.X y 2.X


volver self.data > otro
def __lt__(uno mismo, otro):
return self.data < otro

X = C()
print(X > 'jamón') # Verdadero (ejecuta __gt__)
print(X < 'jamón') # Falso (ejecuta __lt__)

Cuando se ejecuta bajo Python 3.X o 2.X, las impresiones al final muestran los resultados esperados
notaron en sus comentarios, porque los métodos de la clase interceptan e implementan expresiones de
comparación. Consulte los manuales de Python y otros recursos de referencia para obtener más
detalles en esta categoría; por ejemplo, __lt__ se usa para ordenar en Python3.X, y en cuanto a
operadores de expresiones binarias, estos métodos también pueden devolver NotImplemented para argumentos
no admitidos.

El método __cmp__ en Python 2.X


Solo en Python 2.X, el método __cmp__ se usa como respaldo si se utilizan métodos más específicos.
no están definidos: su resultado entero se utiliza para evaluar el operador que se está ejecutando. Lo siguiente
produce el mismo resultado que el código de la sección anterior bajo 2.X, por ejemplo, pero
falla en 3.X porque __cmp__ ya no se usa:
clase C:
data = 'spam' def # 2.X solo
__cmp__(self, otro): return # __cmp__ no se usa en 3.X
cmp(self.data, other) # cmp no definido en 3.X

X = C()
print(X > 'jamón') # Verdadero (ejecuta __cmp__)
print(X < 'jamón') # Falso (ejecuta __cmp__)

Note que esto falla en 3.X porque __cmp__ ya no es especial, no porque el cmp
la función incorporada ya no está presente. Si cambiamos la clase anterior a la siguiente para
intente simular la llamada cmp , el código aún funciona en 2.X pero falla en 3.X:
clase C:
datos = 'correo no deseado'

def __cmp__(uno mismo, otro):


return (self.data > otro) - (self.data <otro)

926 | Capítulo 30: Sobrecarga del operador


Machine Translated by Google

Entonces, es posible que se pregunte por qué acabo de mostrarle un método de comparación que ya no
es compatible con 3.X. Si bien sería más fácil borrar el historial por completo, este libro está diseñado
para admitir lectores 2.X y 3.X. Debido a que __cmp__ puede aparecer en el código 2.X, los lectores
deben reutilizarlo o mantenerlo, es un juego justo en este libro. Además, __cmp__ se eliminó más
abruptamente que el método __getslice__ descrito anteriormente, por lo que puede durar más tiempo.
Sin embargo, si usa 3.X o le preocupa ejecutar su código bajo 3.X en el futuro, no use más __cmp__ :
use los métodos de comparación más específicos en su lugar.

Pruebas booleanas: __bool__ y __len__ El siguiente conjunto de

métodos es realmente útil (¡sí, juego de palabras!). Como hemos aprendido, cada objeto es inherentemente
verdadero o falso en Python. Cuando codifica clases, puede definir lo que esto significa para sus objetos
codificando métodos que dan los valores verdadero o falso de las instancias a pedido. Los nombres de
estos métodos difieren según la línea de Python; esta sección comienza con la historia de 3.X, luego
muestra el equivalente de 2.X.

Como se mencionó brevemente anteriormente, en contextos booleanos, Python primero intenta __bool__
para obtener un valor booleano directo; si falta ese método, Python intenta __len__ para inferir un valor
de verdad a partir de la longitud del objeto. El primero de estos generalmente usa el estado del objeto u
otra información para producir un resultado booleano. En 3.X:
>>> clase Verdad:
def __bool__(auto): devuelve Verdadero

>>> X = Verdad()
>>> if X: print('¡sí!')

¡sí!

>>> clase Verdad:


def __bool__(self): devuelve Falso

>>> X = Verdad()
>>> bool(X)
Falso

Si falta este método, Python recurre a la longitud porque un objeto que no está vacío se considera
verdadero (es decir, una longitud distinta de cero se considera que el objeto es verdadero, y una longitud
cero significa que es falso):
>>> clase Verdad:
def __len__(auto): devuelve 0

>>> X = Verdad()
>>> si no es X: print('¡no!')

¡no!

Si ambos métodos están presentes, Python prefiere __bool__ sobre __len__, porque es más específico:

Pruebas booleanas: __bool__ y __len__ | 927


Machine Translated by Google

>>> clase Verdad:


def __bool__(self): devuelve True # 3.X intenta __bool__ primero
def __len__(self): devuelve 0 # 2.X intenta __len__ primero

>>> X = Verdad()
>>> if X: print('¡sí!')

¡sí!

Si no se define ningún método de verdad, el objeto se considera verdadero en vano (aunque cualquier
posible implicación para los lectores más inclinados a la metafísica es estrictamente una coincidencia):

>>> clase Verdad:


pasar

>>> X = Verdad()
>>> bool(X)
Verdadero

Al menos esa es la Verdad en 3.X. Estos ejemplos no generarán excepciones en 2.X, pero algunos de sus
resultados pueden parecer un poco extraños (y desencadenar una crisis existencial o dos) a menos que lea
la siguiente sección.

Métodos booleanos en Python 2.X Por

desgracia, no es tan dramático como se anuncia: los usuarios de Python 2.X simplemente usan __nonzero__
en lugar de __bool__ en todo el código de la sección anterior. Python 3.X cambió el nombre del método 2.X
__nonzero__ a __bool__, pero las pruebas booleanas funcionan igual; tanto 3.X como 2.X usan __len__
como alternativa.

Sutilmente, si no usa el nombre 2.X, la primera prueba en la sección anterior funcionará igual para usted de
todos modos, pero solo porque __bool__ no se reconoce como un nombre de método especial en 2.X, y los
objetos se consideran cierto por defecto! Para presenciar esta diferencia de versión en vivo, debe devolver
Falso:

C:\code> c:\python33\python
>>> class C: def __bool__(self):
print('in bool') return
False

>>> X = C()
>>> bool(X)
en bool
Falso
>>> si X: imprime(99)

en bool

Esto funciona como se anuncia en 3.X. Sin embargo, en 2.X, __bool__ se ignora y el objeto siempre se
considera verdadero de forma predeterminada:

928 | Capítulo 30: Sobrecarga del operador


Machine Translated by Google

C:\code> c:\python27\python >>>


class C: def __bool__(self): print('in
bool') return False

>>> X = C()
>>> bool(X)
Verdadero

>>> si X: imprime(99)

99

La historia corta aquí: en 2.X, use __no cero__ para valores booleanos, o devuelva 0 desde el método alternativo __len__ para
designar falso: C:\code> c:\python27\python >>> class C: def __nonzero__(self) : print('in nonzero') return Falso

# Devuelve int (o Verdadero/ Falso, igual que 1/0)

>>> X = C()
>>> bool(X) en
distinto de cero
Falso
>>> si X: imprime(99)

en distinto de cero

Pero tenga en cuenta que __nonzero__ solo funciona en 2.X; si se usa en 3.X, se ignorará
silenciosamente y el objeto se clasificará como verdadero de forma predeterminada, ¡al igual que
usar __bool__ de 3.X en 2.X!

Y ahora que hemos logrado cruzar al reino de la filosofía, pasemos a ver un último contexto
sobrecargado: la desaparición del objeto.

Destrucción de objetos: __del__


Es hora de cerrar este capítulo y aprender a hacer lo mismo con nuestros objetos de clase.
Hemos visto cómo se llama al constructor __init__ cada vez que se genera una instancia (y notamos
cómo __new__ se ejecuta primero para crear el objeto). Su contraparte, el método destructor
__del__ , se ejecuta automáticamente cuando se recupera el espacio de una instancia (es decir, en
el momento de la “recolección de basura”):
>>> clase Vida:
def __init__(self, nombre='desconocido'):
print('Hola ' + nombre)
nombre
self.nombre
def live(self):
=
print(self.nombre) def __del__(self):
print('Adiós ' + self .nombre)

Destrucción de objetos: __del__ | 929


Machine Translated by Google

>>> brian = Vida('Brian')


hola brian
>>> brian.vivir()
brian
>>> brian = 'loretta'
Adiós Brian

Aquí, cuando a brian se le asigna una cadena, perdemos la última referencia a la instancia de Life y activamos
su método destructor. Esto funciona y puede ser útil para implementar algunas actividades de limpieza, como
terminar una conexión con el servidor. Sin embargo, los destructores no se usan con tanta frecuencia en
Python como en algunos lenguajes de programación orientada a objetos, por varias razones que se describen
en la siguiente sección.

Notas de uso del destructor


El método destructor funciona como está documentado, pero tiene algunas advertencias bien conocidas y
algunos rincones completamente oscuros que hacen que sea algo raro de ver en el código de Python:

• Necesidad: por un lado, los destructores pueden no ser tan útiles en Python como lo son en otros
lenguajes OOP. Debido a que Python reclama automáticamente todo el espacio de memoria que tiene
una instancia cuando se reclama la instancia, los destructores no son necesarios para la administración
del espacio. En la implementación actual de CPython de Python, tampoco es necesario cerrar los
objetos de archivo retenidos por la instancia en los destructores porque se cierran automáticamente
cuando se recuperan. Sin embargo, como se mencionó en el Capítulo 9, a veces es mejor ejecutar
métodos de cierre de archivos de todos modos, porque este comportamiento de cierre automático
puede variar en implementaciones alternativas de Python (p. ej., Jython). • Previsibilidad: por otro lado,

no siempre se puede predecir fácilmente cuándo se recuperará una instancia. En algunos casos, puede
haber referencias persistentes a sus objetos en las tablas del sistema que evitan que los destructores
se ejecuten cuando su programa espera que se activen. Python tampoco garantiza que se llamará a los
métodos destructores para los objetos que aún existen cuando el intérprete sale. • Excepciones: De
hecho, __del__ puede ser complicado de usar por razones aún más sutiles. Las excepciones generadas

dentro de él, por ejemplo, simplemente imprimen un mensaje de advertencia en sys.stderr (el flujo de error
estándar) en lugar de desencadenar un evento de excepción, debido al contexto impredecible en el que
el recolector de elementos no utilizados lo ejecuta; no siempre es posible. para saber dónde se debe
entregar tal excepción. • Ciclos: además, las referencias cíclicas (también conocidas como circulares)
entre objetos pueden evitar que se realice la recolección de elementos no utilizados cuando se espera

que ocurra. Un detector de ciclo opcional, habilitado por defecto, puede recolectar automáticamente tales
objetos eventualmente, pero solo si no tienen métodos __del__ . Dado que esto es relativamente
oscuro, ignoraremos más detalles aquí; consulte la cobertura de los manuales estándar de Python tanto
de __del__ como del módulo del recolector de basura gc para obtener más información.

Debido a estas desventajas, a menudo es mejor codificar las actividades de finalización en un método
llamado explícitamente (por ejemplo, apagado). Como se describe en la siguiente parte del libro, el

930 | Capítulo 30: Sobrecarga del operador


Machine Translated by Google

La declaración try/finally también admite acciones de finalización, al igual que la declaración with para
objetos que admiten su modelo de administrador de contexto.

Resumen del capítulo

Esos son tantos ejemplos de sobrecarga para los que tenemos espacio aquí. La mayoría de los otros métodos
de sobrecarga de operadores funcionan de manera similar a los que hemos explorado, y todos son solo
ganchos para interceptar operaciones de tipos incorporados. Algunos métodos de sobrecarga, por ejemplo,
tienen listas de argumentos únicas o valores de retorno, pero el patrón de uso general es el mismo. Veremos
algunos otros en acción más adelante en el libro:

• El Capítulo 34 usa __enter__ y __exit__ con administradores de contexto de declaraciones . • El

capítulo 38 utiliza los métodos de obtención /establecimiento del descriptor de clase __get__ y __set__ .

• El Capítulo 40 utiliza el método de creación de objetos __nuevo__ en el contexto de las metaclases.

Además, algunos de los métodos que hemos estudiado aquí, como __call__ y __str__, se emplearán en
ejemplos posteriores de este libro. Sin embargo, para una cobertura completa, me remito a otras fuentes de
documentación: consulte el manual del lenguaje estándar de Python o los libros de referencia para obtener
detalles sobre métodos de sobrecarga adicionales.

En el próximo capítulo, dejamos atrás el ámbito de la mecánica de clases para explorar patrones de diseño
comunes: las formas en que las clases se usan y combinan comúnmente para optimizar la reutilización del
código. Después de eso, examinaremos un puñado de temas avanzados y pasaremos a las excepciones, el
último tema central de este libro. Sin embargo, antes de seguir leyendo, tómese un momento para resolver el
cuestionario del capítulo a continuación para revisar los conceptos que hemos cubierto.

Pon a prueba tus conocimientos: Cuestionario

1. ¿Qué dos métodos de sobrecarga de operadores puede usar para admitir la iteración en su
clases?

2. ¿Qué dos métodos de sobrecarga de operadores manejan la impresión y en qué contextos?

3. ¿Cómo puedes interceptar operaciones de división en una clase?

4. ¿Cómo puedes captar la suma en el lugar en una clase?

5. ¿Cuándo debe proporcionar sobrecarga de operadores?

Pon a prueba tus conocimientos: respuestas

1. Las clases pueden soportar la iteración al definir (o heredar) __getitem__ o __iter__.


En todos los contextos de iteración, Python intenta usar __iter__ primero, que devuelve un objeto que
admite el protocolo de iteración con un método __next__ : si la búsqueda de herencia no encuentra
__iter__ , Python recurre al método de indexación __getitem__ ,

Pon a prueba tus conocimientos: respuestas | 931


Machine Translated by Google

que se llama repetidamente, con índices sucesivamente más altos. Si se usa, la declaración de
rendimiento puede crear el método __next__ automáticamente.
2. Los métodos __str__ y __repr__ implementan visualizaciones de impresión de objetos. El primero es
llamado por las funciones incorporadas print y str ; el último es llamado por print y str si no hay
__str__, y siempre por los ecos interactivos integrados repr y las apariencias anidadas. Es decir,
__repr__ se usa en todas partes, excepto por print y str cuando se define __str__ . Un __str__
generalmente se usa para pantallas fáciles de usar; __repr__ proporciona detalles adicionales o la
forma de código del objeto.
3. El método de indexación __getitem__ captura la división : se llama con un objeto de división, en lugar
de un índice entero simple, y los objetos de división se pueden pasar o inspeccionar según sea
necesario. En Python 2.X, __getslice__ (desaparecido en 3.X) también puede usarse para cortes de
dos límites.

4. La suma en el lugar intenta __iadd__ primero y __add__ con una asignación en segundo lugar. El
mismo patrón es válido para todos los operadores binarios. El método __radd__ también está
disponible para la suma del lado derecho.
5. Cuando una clase coincide naturalmente, o necesita emular, las interfaces de un tipo integrado.
Por ejemplo, las colecciones pueden imitar secuencias o interfaces de mapeo, y las funciones de
llamada pueden estar codificadas para usarse con una API que espera una función. Sin embargo,
por lo general, no debería implementar operadores de expresión si no se asignan naturalmente a
sus objetos de forma natural y lógica; en su lugar, use métodos con nombres normales.

932 | Capítulo 30: Sobrecarga del operador


Machine Translated by Google

CAPÍTULO 31

Diseñando con Clases

Hasta ahora, en esta parte del libro, nos hemos concentrado en usar la herramienta OOP de Python, la clase. Pero la
programación orientada a objetos también se trata de problemas de diseño, es decir, cómo usar clases para modelar
objetos útiles. Este capítulo abordará algunas ideas centrales de programación orientada a objetos y presentará
algunos ejemplos adicionales que son más realistas que muchos mostrados hasta ahora.

En el camino, codificaremos algunos patrones de diseño OOP comunes en Python, como la herencia, la composición,
la delegación y las fábricas. También investigaremos algunos conceptos de clase centrados en el diseño, como los
atributos pseudoprivados, la herencia múltiple y los métodos enlazados.

Una nota por adelantado: algunos de los términos de diseño mencionados aquí requieren más explicación de la que
puedo proporcionar en este libro. Si este material despierta su curiosidad, sugiero explorar un texto sobre diseño OOP
o patrones de diseño como siguiente paso. Como veremos, la buena noticia es que Python hace que muchos patrones
de diseño tradicionales sean triviales.

Python y programación orientada a objetos

Comencemos con una revisión: la implementación de Python de OOP se puede resumir en tres ideas:

Herencia

La herencia se basa en la búsqueda de atributos en Python (en expresiones X.name ).

Polimorfismo En
X.method, el significado de método depende del tipo (clase) del sujeto objeto X.

Los métodos de
encapsulación y los operadores implementan el comportamiento, aunque la ocultación de datos es una convención
predeterminada.

A estas alturas, deberías tener una buena idea de lo que es la herencia en Python. También hemos hablado sobre el
polimorfismo de Python ya unas cuantas veces; fluye de la falta de declaraciones de tipo de Python. Dado que los
atributos siempre se resuelven en tiempo de ejecución, los objetos que

933
Machine Translated by Google

implementar las mismas interfaces son automáticamente intercambiables; los clientes no necesitan saber qué
tipo de objetos están implementando los métodos que llaman.

Encapsular significa empaquetar en Python, es decir, ocultar detalles de implementación detrás de la interfaz
de un objeto. No significa privacidad forzada, aunque eso se puede implementar con código, como veremos
en el Capítulo 39. No obstante, la encapsulación está disponible y es útil en Python: permite cambiar la
implementación de la interfaz de un objeto sin afectar a los usuarios de Python. ese objeto

Polimorfismo significa interfaces, no signaturas de llamada Algunos

lenguajes OOP también definen polimorfismo para significar funciones de sobrecarga basadas en las
signaturas de tipo de sus argumentos: el número pasado y/o sus tipos. Debido a que no hay declaraciones de
tipo en Python, este concepto realmente no se aplica; como hemos visto, el polimorfismo en Python se basa
en interfaces de objetos, no en tipos.

Si está ansioso por sus días de C ++, puede intentar sobrecargar los métodos por sus listas de argumentos,
como este:

clase
C: def meth(self, x):
...
def met(self, x, y, z):
...
Este código se ejecutará, pero debido a que la definición simplemente asigna un objeto a un nombre en el
ámbito de la clase, la última definición de la función del método es la única que se conservará.
Dicho de otra manera, es como si dijeras X = 1 y luego X = 2; X será 2. Por lo tanto, solo puede haber una
definición de nombre de método.

Si realmente son necesarios, siempre puede codificar selecciones basadas en tipos usando las ideas de
prueba de tipos que vimos en el Capítulo 4 y el Capítulo 9, o las herramientas de lista de argumentos
presentadas en el Capítulo 18:

clase C:
def meth(self, *argumentos):
if len(argumentos) == 1: # Rama en argumentos numéricos
...
elif tipo(arg[0]) == int: # Rama en tipos de argumentos (o isinstance())
...
Sin embargo, normalmente no debería hacer esto, no es la forma de Python. Como se describe en el Capítulo
16, debe escribir su código para esperar solo una interfaz de objeto, no un tipo de datos específico. De esa
forma, será útil para una categoría más amplia de tipos y aplicaciones, tanto ahora como en el futuro:

clase
C: def meth(self,
x): x.operation() # Supongamos que x hace lo correcto

934 | Capítulo 31: Diseño con clases


Machine Translated by Google

Por lo general, también se considera mejor usar nombres de métodos distintos para operaciones distintas, en lugar de
confiar en firmas de llamada (sin importar el idioma en el que se codifica).

Aunque el modelo de objetos de Python es sencillo, gran parte del arte de la programación orientada a objetos está en la
forma en que combinamos las clases para lograr los objetivos de un programa. La siguiente sección comienza un recorrido
por algunas de las formas en que los programas más grandes utilizan las clases para su beneficio.

OOP y Herencia: Relaciones “Is-a”


Ya hemos explorado la mecánica de la herencia en profundidad, pero ahora me gustaría mostrarles un ejemplo de cómo se
puede usar para modelar las relaciones del mundo real. Desde el punto de vista de un programador, la herencia se inicia
mediante calificaciones de atributos, que desencadenan búsquedas de nombres en instancias, sus clases y luego cualquier
superclase. Desde el punto de vista de un diseñador , la herencia es una forma de especificar la pertenencia a un conjunto:
una clase define un conjunto de propiedades que pueden ser heredadas y personalizadas por conjuntos más específicos
(es decir, subclases).

Para ilustrar, pongamos a trabajar al robot que hace pizzas del que hablamos al comienzo de esta parte del libro.
Supongamos que hemos decidido explorar trayectorias profesionales alternativas y abrir una pizzería (no está mal, en lo
que respecta a las trayectorias profesionales). Una de las primeras cosas que tendremos que hacer es contratar empleados
para atender a los clientes, preparar la comida, etc. Siendo ingenieros de corazón, hemos decidido construir un robot para
hacer las pizzas; pero siendo política y cibernéticamente correctos, también hemos decidido hacer de nuestro robot un
empleado de pleno derecho con un salario.

Nuestro equipo de pizzería se puede definir mediante las cuatro clases en el siguiente archivo de ejemplo de Python 3.X y
2.X, employee.py. La clase más general, Employee, proporciona un comportamiento común, como aumentar los salarios
(giveRaise) e imprimir (__repr__). Hay dos tipos de empleados y, por lo tanto, dos subclases de Empleado: Chef y Mesero.
Ambos anulan el método de trabajo heredado para imprimir mensajes más específicos. Finalmente, nuestro robot de pizza
está modelado por una clase aún más específica: PizzaRobot es una especie de chef, que es una especie de empleado.
En términos de programación orientada a objetos, llamamos a estas relaciones enlaces "es-un": un robot es un chef, que
es un empleado. Aquí está el archivo employee.py :

# Archivo empleados.py (2.X +


3.X) de __future__ import print_function

class Empleado:
def __init__(self, nombre, salario=0):
self.name = nombre self.salary =
salario def giveRaise(self, percent):
self.salary = self.salary + (self.salary *
percent) def trabajo (self): print(self.name, "hace cosas") def
__repr__(self): return "<Empleado: nombre=%s, salario=%s>" %
(self.name, self.salary)

Chef de clase (empleado):

OOP y Herencia: Relaciones “Is-a” | 935


Machine Translated by Google

def __init__(self, nombre):


Empleado.__init__(self, nombre, 50000) def
trabajo(self): print(self.name, "hace comida")

class Servidor(Empleado): def


__init__(self, name):
Employee.__init__(self, name, 40000) def work(self):
print(self.name, "interfaces with customer")

clase PizzaRobot(Chef): def


__init__(self, nombre):
Chef.__init__(yo, nombre) def
trabajo(yo): print(yo.nombre, "hace pizza")

if __name__ == "__main__": bob =


PizzaRobot('bob') print(bob) # Hacer un robot llamado bob
bob.work() bob.giveRaise(0.20) # Ejecutar __repr__ heredado
print(bob); impresión() # Ejecutar acción específica del tipo
# Dale a bob un aumento del 20%

para klass en Empleado, Chef, Servidor, PizzaRobot: obj =


klass(klass.__name__) obj.work()

Cuando ejecutamos el código de autoevaluación incluido en este módulo, creamos un robot para
hacer pizza llamado bob, que hereda los nombres de tres clases: PizzaRobot, Chef y Employee.
Por ejemplo, imprimir bob ejecuta el método Employee.__repr__ , y darle a bob un aumento invoca a
Employee.giveRaise porque ahí es donde la búsqueda de herencia encuentra ese método:

c:\code> python empleados.py


<Empleado: nombre=bob, salario=50000> bob
hace pizza <Empleado: nombre=bob,
salario=60000.0>

empleado hace cosas


chef hace comida
El servidor interactúa con el cliente
PizzaRobot hace pizza

En una jerarquía de clases como esta, generalmente puede crear instancias de cualquiera de las clases,
no solo las que están en la parte inferior. Por ejemplo, el bucle for en el código de autoevaluación de este
módulo crea instancias de las cuatro clases; cada uno responde de manera diferente cuando se le pide
que trabaje porque el método de trabajo es diferente en cada uno. Bob el robot, por ejemplo, obtiene
trabajo de la clase PizzaRobot más específica (es decir, la más baja) .

Por supuesto, estas clases solo simulan objetos del mundo real; work imprime un mensaje por el momento,
pero podría expandirse para hacer un trabajo real más tarde (consulte las interfaces de Python para

936 | Capítulo 31: Diseño con clases


Machine Translated by Google

dispositivos como puertos seriales, placas Arduino y Raspberry Pi si está tomando esta sección
demasiado literalmente).

OOP y Composición: Relaciones “Has-a”


La noción de composición se introdujo en los capítulos 26 y 28. Desde el punto de vista de un
programador , la composición implica incrustar otros objetos en un objeto contenedor y activarlos para
implementar métodos contenedores. Para un diseñador, la composición es otra forma de representar
las relaciones en el dominio de un problema. Pero, en lugar de establecer la pertenencia, la
composición tiene que ver con los componentes, partes de un todo.

La composición también refleja las relaciones entre las partes, llamadas relaciones "tiene-un".
Algunos textos de diseño de programación orientada a objetos se refieren a la composición como
agregación, o distinguen entre los dos términos usando agregación para describir una dependencia
más débil entre contenedor y contenido. En este texto, una “composición” simplemente se refiere a
una colección de objetos incrustados. La clase compuesta generalmente proporciona una interfaz
propia y la implementa dirigiendo los objetos incrustados.

Ahora que hemos implementado a nuestros empleados, pongámoslos en la pizzería y dejemos que
se ocupen. Nuestra pizzería es un objeto compuesto: tiene un horno y tiene empleados como meseros
y chefs. Cuando un cliente ingresa y hace un pedido, los componentes de la tienda entran en acción:
el mesero toma el pedido, el chef prepara la pizza, etc. El siguiente ejemplo, el archivo pizzashop.py,
se ejecuta igual en Python 3.X y 2.X y simula todos los objetos y relaciones en este escenario:

# Archivo pizzashop.py (2.X + 3.X)


de __future__ import print_function from employee
import PizzaRobot, Server

clase Cliente:
def __init__(self, nombre):
self.name = nombre def
order(self, server): print(self.name,
"orders from", server) def pay(self, server):
print(self.name, "pays para artículo a", servidor)

class Horno:
def hornear(auto):
print("horno hornea")

class PizzaShop: def


__init__(self): self.server
= Server('Pat') self.chef = # Incrustar otros objetos
PizzaRobot('Bob') self.oven = Oven() # Un robot llamado bob

def pedido(auto, nombre):


cliente = Cliente(nombre) # Activar otros objetos
cliente.pedido(auto.servidor) # Pedidos de clientes desde el servidor

OOP y Composición: Relaciones “Has-a” | 937


Machine Translated by Google

self.chef.work()
self.oven.bake()
cliente.pago(self.server)

if __name__ == "__principal__":
escena = PizzaShop() # Hacer el compuesto
escena.orden('Homer') # Simular la orden de Homero
print('...')
escena.orden('Shaggy') # Simular la orden de Shaggy

La clase PizzaShop es un contenedor y controlador; su constructor crea e incrusta instancias de las


clases de empleados que escribimos en la sección anterior, así como una clase Oven definida aquí.
Cuando el código de autodiagnóstico de este módulo llama al método de pedido PizzaShop , se solicita
a los objetos incrustados que realicen sus acciones por turnos. Tenga en cuenta que creamos un nuevo
objeto Cliente para cada pedido y pasamos el objeto Servidor incorporado a los métodos Cliente ; los
clientes van y vienen, pero el servidor es parte del compuesto de la pizzería. Observe también que los
empleados todavía están involucrados en una relación de herencia; la composición y la herencia son
herramientas complementarias.

Cuando ejecutamos este módulo, nuestra pizzería maneja dos pedidos, uno de Homer y otro de Shaggy:

c:\code> python pizzashop.py


Homer ordena a <Empleado: nombre=Pat, salario=40000> Bob
hace pizza horneada en el horno Homer paga el artículo a
<Empleado: nombre=Pat, salario=40000>

...
Órdenes de Shaggy de <Empleado: nombre=Pat, salario=40000>
Bob hace hornos
de pizza
Shaggy paga el artículo a <Empleado: nombre=Pat, salario=40000>

Una vez más, esto es principalmente una simulación de juguete, pero los objetos y las interacciones son
representativos de los compuestos en el trabajo. Como regla general, las clases pueden representar casi
cualquier objeto y relación que puedas expresar en una oración; simplemente reemplace los sustantivos
con clases (p. ej., horno) y los verbos con métodos (p. ej., hornear), y tendrá un primer corte en un diseño.

Procesadores de flujo revisados


Para un ejemplo de composición que puede ser un poco más tangible que los robots para hacer pizza,
recuerde la función genérica del procesador de flujo de datos que codificamos parcialmente en la
introducción a OOP en el Capítulo 26:

def procesador (lector, convertidor, escritor):


while True:
datos = lector.leer() si
no son datos: romper
datos = convertidor(datos)
escritor.escribir(datos)

938 | Capítulo 31: Diseño con clases


Machine Translated by Google

En lugar de usar una función simple aquí, podríamos codificar esto como una clase que usa composición
para hacer su trabajo a fin de proporcionar más estructura y compatibilidad con la herencia. El siguiente
archivo 3.X/2.X, streams.py, muestra una forma de codificar la clase:
Procesador de clase:
def __init__(self, lector, escritor): self.lector =
lector self.escritor = escritor

def proceso(self): while


True: data =
self.reader.readline() if not data: break
data = self.converter(data)
self.writer.write(data)

def convertidor (auto, datos):


afirmar Falso, 'el convertidor debe definirse' # O generar una excepción

Esta clase define un método de conversión que espera que completen las subclases; es un ejemplo del
modelo abstracto de superclase que esbozamos en el Capítulo 29 (más información sobre la afirmación en
la Parte VII : simplemente genera una excepción si su prueba es falsa). Codificados de esta manera, los
objetos de lectura y escritura están incrustados dentro de la instancia de la clase (composición), y
proporcionamos la lógica de conversión en una subclase en lugar de pasar una función de conversión
(herencia). El archivo converters.py muestra cómo:

del procesador de importación de secuencias

clase Mayúsculas (Procesador):


def convertidor(self, data): return
data.upper()

if __name__ == '__main__': import


sys obj =
Uppercase(open('trispam.txt'), sys.stdout) obj.process()

Aquí, la clase Uppercase hereda la lógica de bucle de procesamiento de flujo (y cualquier otra cosa que
pueda estar codificada en sus superclases). Necesita definir solo lo que tiene de único: la lógica de
conversión de datos. Cuando se ejecuta este archivo, crea y ejecuta una instancia que lee del archivo
trispam.txt y escribe el equivalente en mayúsculas de ese archivo en el flujo de salida estándar :

c:\code> escribe trispam.txt


spam Spam SPAM!

c:\code> python converters.py SPAM


SPAM SPAM!

OOP y Composición: Relaciones “Has-a” | 939


Machine Translated by Google

Para procesar diferentes tipos de flujos, pase diferentes tipos de objetos a la llamada de construcción de clase. Aquí, usamos un
archivo de salida en lugar de un flujo: C:\code> python >>> import converters >>> prog = converters.Uppercase(open('trispam.txt'),

open('trispamup.txt', ' w')) >>> prog.proceso()

C:\código> escriba trispamup.txt


SPAM
SPAM
SPAM!

Pero, como se sugirió anteriormente, también podríamos pasar objetos arbitrarios codificados como clases
que definen las interfaces de métodos de entrada y salida requeridas. Aquí hay un ejemplo simple que
pasa en una clase de escritor que envuelve el texto dentro de las etiquetas HTML:
C:\code> python >>>
from converters import Mayúsculas
>>>
>>> clase HTMLize: def
escribe(self, linea):
print('<PRE>%s</PRE>' % linea.rstrip())

>>> Mayúsculas(abrir('trispam.txt'), HTMLize()).proceso()


<PRE>SPAM</PRE>
<PRE>SPAM</PRE>
<PRE>SPAM!</PRE>

Si rastrea el flujo de control de este ejemplo, verá que obtenemos conversión a mayúsculas (por herencia)
y formato HTML (por composición), aunque la lógica de procesamiento central en la superclase del
procesador original no sabe nada sobre ninguno de los pasos. El código de procesamiento solo se
preocupa de que los escritores tengan un método de escritura y que se defina un método llamado
convertir ; no le importa lo que esos métodos hacen cuando son llamados.
Tal polimorfismo y encapsulación de la lógica está detrás de gran parte del poder de las clases en Python.

Tal como está, la superclase Processor solo proporciona un bucle de exploración de archivos. En un
trabajo más realista, podríamos extenderlo para admitir herramientas de programación adicionales para
sus subclases y, en el proceso, convertirlo en un marco de aplicación completo . Codificar dicha
herramienta una vez en una superclase le permite reutilizarla en todos sus programas. Incluso en este
ejemplo simple, debido a que mucho está empaquetado y heredado con clases, todo lo que tuvimos que
codificar fue el paso de formato HTML; el resto era gratis.

Para ver otro ejemplo de composición en funcionamiento, consulte el ejercicio 9 al final del Capítulo 32 y
su solución en el Apéndice D; es similar al ejemplo de la pizzería. Nos hemos centrado en la herencia en
este libro porque esa es la herramienta principal que el propio lenguaje Python proporciona para la
programación orientada a objetos. Pero, en la práctica, la composición puede usarse tanto como la
herencia como una forma de estructurar clases, especialmente en sistemas más grandes. Como hemos
visto, la herencia y la composición suelen ser técnicas complementarias (ya veces alternativas).

940 | Capítulo 31: Diseño con clases


Machine Translated by Google

Debido a que la composición es un problema de diseño fuera del alcance del lenguaje Python y de
este libro, me remito a otros recursos para obtener más información sobre este tema.

Por qué le importará: clases y persistencia He mencionado

el soporte de persistencia de objetos pickle y shelve de Python varias veces en esta parte del libro porque
funciona especialmente bien con instancias de clase. De hecho, estas herramientas a menudo son lo
suficientemente convincentes como para motivar el uso de clases en general: al seleccionar o dejar de lado
una instancia de clase, obtenemos un almacenamiento de datos que contiene datos y lógica combinados.

Por ejemplo, además de permitirnos simular interacciones del mundo real, las clases de pizzerías desarrolladas
en este capítulo también podrían usarse como base de una base de datos persistente de restaurantes. Las
instancias de las clases se pueden almacenar en el disco en un solo paso utilizando los módulos pickle o
shelve de Python . Usamos estantes para almacenar instancias de clases en el tutorial de OOP en el Capítulo
28, pero la interfaz de decapado de objetos también es notablemente fácil de usar:

import pickle object


= SomeClass() file =
open(filename, 'wb') pickle.dump(object, # Crear archivo externo
file) # Guardar objeto en archivo

importar archivo
pickle = abrir (nombre de archivo, 'rb')
objeto = pickle.load (archivo) # Recuperarlo más tarde

Pickling convierte objetos en memoria en flujos de bytes serializados (en Python, cadenas), que pueden
almacenarse en archivos, enviarse a través de una red, etc.; el desensamblado vuelve a convertir flujos de
bytes en objetos idénticos en memoria. Los estantes son similares, pero seleccionan automáticamente los
objetos en una base de datos de acceso por clave, que exporta una interfaz similar a un diccionario:

importar archivar
objeto = SomeClass() dbase
= archivar.open(nombre de archivo)
dbase['clave'] = objeto # Guardar bajo clave

importar archivar
dbase = archivar. abrir (nombre de archivo)
objeto = dbase ['clave'] # Recuperarlo más tarde

En nuestro ejemplo de pizzería, el uso de clases para modelar a los empleados significa que podemos obtener
una base de datos simple de empleados y tiendas con poco trabajo adicional: seleccionar dichos objetos de
instancia en un archivo los hace persistentes en las ejecuciones del programa Python:

>>> from pizzashop import PizzaShop >>> shop


= PizzaShop() >>> shop.server, shop.chef
(<Empleado: nombre=Pat, salario=40000>,
<Empleado: nombre=Bob, salario=50000>) >>> import pickle >>> pickle.dump(comprar,
abrir('shopfile.pkl', 'wb'))

Esto almacena todo un objeto de tienda compuesto en un archivo a la vez. Para recuperarlo más tarde en otra
sesión o programa, también basta con un solo paso. De hecho, los objetos restaurados de esta manera
conservan tanto el estado como el comportamiento:

OOP y Composición: Relaciones “Has-a” | 941


Machine Translated by Google

>>> import pickle


>>> obj = pickle.load(open('shopfile.pkl', 'rb')) >>>
obj.server, obj.chef (<Empleado: nombre=Pat,
salario=40000>, <Empleado: nombre=Bob, salario=50000>)

>>> obj.orden('LSP')
Pedidos de LSP de <Empleado: nombre=Pat,
salario=40000> Bob hace hornos de pizza

LSP paga el artículo a <Empleado: nombre=Pat, salario=40000>

Esto simplemente ejecuta una simulación tal como está, pero podríamos ampliar el taller para realizar un
seguimiento del inventario, los ingresos, etc.; guardarlo en su archivo después de los cambios mantendría su estado
actualizado. Consulte el manual de la biblioteca estándar y la cobertura relacionada en el Capítulo 9, el Capítulo 28
y el Capítulo 37 para obtener más información sobre encurtidos y estantes.

OOP y Delegación: Objetos Proxy “Wrapper”


Además de la herencia y la composición, los programadores orientados a objetos a menudo hablan de
delegación, que generalmente implica objetos controladores que incrustan otros objetos a los que pasan
solicitudes de operación. Los controladores pueden encargarse de actividades administrativas, como
registrar o validar accesos, agregar pasos adicionales a los componentes de la interfaz o monitorear
instancias activas.

En cierto sentido, la delegación es una forma especial de composición, con un solo objeto incrustado
administrado por una clase contenedora (a veces denominada proxy) que retiene la mayor parte o la
totalidad de la interfaz del objeto incrustado. La noción de proxies a veces también se aplica a otros
mecanismos, como las llamadas a funciones; en la delegación, nos preocupamos por los proxies para
todo el comportamiento de un objeto, incluidas las llamadas a métodos y otras operaciones.

Este concepto se presentó con un ejemplo en el Capítulo 28, y en Python a menudo se implementa
con el gancho del método __getattr__ que estudiamos en el Capítulo 30. Debido a que este método de
sobrecarga de operadores intercepta accesos a atributos inexistentes, una clase contenedora puede
usar __getattr__ para enrutar accesos arbitrarios a un objeto envuelto. Dado que este método permite
que las solicitudes de atributos se enruten de forma genérica, la clase contenedora conserva la interfaz
del objeto envuelto y puede agregar operaciones adicionales propias.

A modo de revisión, considere el archivo trace.py (que se ejecuta igual en 2.X y 3.X):
class Wrapper:
def __init__(self, object):
self.wrapped = object def # Guardar objeto
__getattr__(self, attrname):
print('Trace: ' + attrname) # Rastrear
búsqueda return getattr(self.wrapped, attrname) # Delegate fetch

Recuerde del Capítulo 30 que __getattr__ obtiene el nombre del atributo como una cadena. Este código
utiliza la función incorporada getattr para obtener un atributo del objeto envuelto por cadena de nombre:
getattr(X,N) es como XN, excepto que N es una expresión que se evalúa como una cadena en tiempo
de ejecución, no como una variable . De hecho, getattr(X,N) es similar a X.__dict__[N],

942 | Capítulo 31: Diseño con clases


Machine Translated by Google

pero el primero también realiza una búsqueda de herencia, como XN, mientras que el segundo no lo hace
(consulte el Capítulo 22 y el Capítulo 29 para obtener más información sobre el atributo __dict__ ).

Puede usar el enfoque de la clase contenedora de este módulo para administrar el acceso a cualquier objeto con
atributos: listas, diccionarios e incluso clases e instancias. Aquí, la clase Wrapper simplemente imprime un
mensaje de seguimiento en cada acceso de atributo y delega la solicitud de atributo al objeto envuelto incrustado :

>>> from trace import Wrapper >>> x =


Wrapper([1, 2, 3]) >>> x.append(4) # Envolver una lista
# Método delegado a la lista
Seguimiento: añadir
>>> x.envuelto [1, # Imprimir mi miembro
2, 3, 4]

>>> x = Envoltorio({'a': 1, 'b': 2}) >>> # Envolver un diccionario


lista(x.teclas()) # Método delegado al diccionario
Seguimiento:
teclas ['a', 'b']

El efecto neto es aumentar toda la interfaz del objeto envuelto , con código adicional en la clase Wrapper .
Podemos usar esto para registrar nuestras llamadas a métodos, enrutar llamadas a métodos a lógica adicional o
personalizada, adaptar una clase a una nueva interfaz, etc.

Reviviremos las nociones de objetos envueltos y operaciones delegadas como una forma de extender los tipos
incorporados en el próximo capítulo. Si está interesado en el patrón de diseño de delegación, también esté atento
a las discusiones en el Capítulo 32 y el Capítulo 39 de los decoradores de funciones, un concepto fuertemente
relacionado diseñado para aumentar una llamada de método o función específica en lugar de toda la interfaz de
un objeto y una clase . decoradores, que sirven como una forma de agregar automáticamente dichos contenedores
basados en delegación a todas las instancias de una clase.

Nota sobre el sesgo de la versión: como vimos en el ejemplo del Capítulo 28, la delegación
de interfaces de objetos por parte de proxies generales ha cambiado sustancialmente en
3.X cuando los objetos envueltos implementan métodos de sobrecarga de operadores.
Técnicamente, esta es una diferencia de clase de nuevo estilo y también puede aparecer
en el código 2.X si habilita esta opción; según el siguiente capítulo, es obligatorio en 3.X
y, por lo tanto, a menudo se considera un cambio 3.X.

En las clases predeterminadas de Python 2.X, los métodos de sobrecarga de operadores


ejecutados por operaciones integradas se enrutan a través de métodos de interceptación
de atributos genéricos como __getattr__. Imprimir un objeto envuelto directamente, por
ejemplo, llama a este método para __repr__ o __str__, que luego pasa la llamada al
objeto envuelto. Este patrón es válido para __iter__, __add__ y los otros métodos de
operadores del capítulo anterior.

En Python 3.X, esto ya no sucede: la impresión no activa __get attr__ (o su primo


__getattribute__ que estudiaremos en el próximo capítulo) y en su lugar se usa una
pantalla predeterminada. En 3.X, las clases de nuevo estilo buscan métodos invocados
implícitamente por operaciones integradas en las clases y omiten por completo la
búsqueda normal de instancias. Las recuperaciones de atributos de nombre explícito se
enrutan a __getattr__ de la misma manera en 2.X y 3.X, pero incorporadas

OOP y Delegación: Objetos Proxy “Wrapper” | 943


Machine Translated by Google

la búsqueda del método de operación difiere en formas que pueden afectar algunas herramientas
basadas en delegación.

Volveremos a este problema en el próximo capítulo como un cambio de clase de nuevo estilo, y
lo veremos en vivo en el Capítulo 38 y el Capítulo 39, en el contexto de los atributos administrados
y los decoradores. Por ahora, tenga en cuenta que para los patrones de codificación de
delegación, es posible que deba redefinir los métodos de sobrecarga de operadores en las
clases contenedoras (ya sea a mano, por herramientas o por superclases) si son utilizados por
objetos incrustados y desea que sean interceptados. en clases de nuevo estilo.

Atributos de clase pseudoprivada


Además de los objetivos de estructuración más amplios, los diseños de clases a menudo también deben abordar el
uso de nombres. En el estudio de caso del Capítulo 28, por ejemplo, notamos que los métodos definidos dentro de
una clase de herramienta general pueden ser modificados por subclases si están expuestos, y señalamos las ventajas
y desventajas de esta política: si bien admite la personalización de métodos y llamadas directas, también está abierta
a cambios accidentales. reemplazos

En la Parte V, aprendimos que todos los nombres asignados en el nivel superior de un archivo de módulo se exportan.
De forma predeterminada, lo mismo se aplica a las clases: la ocultación de datos es una convención y los clientes
pueden buscar o cambiar atributos en cualquier clase o instancia a la que tengan una referencia. De hecho, los
atributos son todos "públicos" y "virtuales", en términos de C++; todos están accesibles en todas partes y se buscan
dinámicamente en el tiempo de ejecución.1 Dicho esto, hoy en día Python admite la noción de "mangling" de nombres

(es decir, expansión) para localizar algunos nombres en las clases. Los nombres alterados a veces se denominan
engañosamente "atributos privados", pero en realidad esta es solo una forma de localizar un nombre para la clase que
lo creó; la manipulación de nombres no impide el acceso por código fuera de la clase. Esta función está destinada
principalmente a evitar colisiones de espacios de nombres en instancias, no a restringir el acceso a los nombres en
general; Por lo tanto, es mejor llamar a los nombres mutilados "pseudoprivados" que "privados".

Los nombres pseudoprivados son una característica avanzada y completamente opcional, y probablemente no los
encontrará muy útiles hasta que comience a escribir herramientas generales o jerarquías de clases más grandes para
usar en proyectos de multiprogramadores. De hecho, no siempre se usan, incluso cuando probablemente deberían
hacerlo; más comúnmente, los programadores de Python codifican los nombres internos con un solo guión bajo (por
ejemplo, _X), que es solo una convención informal para hacerle saber que un nombre generalmente no debe usarse .
cambiarse (no significa nada para Python).

1. Esto tiende a asustar desproporcionadamente a las personas con experiencia en C++. En Python, incluso es posible cambiar
o eliminar por completo el método de una clase en tiempo de ejecución. Por otro lado, casi nadie hace esto en programas
prácticos. Como lenguaje de secuencias de comandos, Python se trata más de habilitar que de restringir. Además, recuerde
de nuestra discusión sobre la sobrecarga de operadores en el Capítulo 30 que __getattr__ y __setattr__ se pueden usar
para emular la privacidad, pero generalmente no se usan para este propósito en la práctica. Más sobre esto cuando
codifiquemos un decorador de privacidad más realista en el Capítulo 39.

944 | Capítulo 31: Diseño con clases


Machine Translated by Google

Sin embargo, debido a que puede ver esta función en el código de otras personas, debe estar al tanto de
ella, incluso si no la usa usted mismo. Y una vez que aprenda sus ventajas y contextos de uso, puede
encontrar que esta característica es más útil en su propio código de lo que creen algunos programadores.

Descripción general de la

manipulación de nombres Así es como funciona la manipulación de nombres: solo dentro de una declaración
de clase , los nombres que comienzan con dos guiones bajos pero no terminan con dos guiones bajos se
expanden automáticamente para incluir el nombre de la clase que los encierra al frente. Por ejemplo, un
nombre como __X dentro de una clase denominada Spam se cambia automáticamente a _Spam__X : el
nombre original tiene un prefijo con un solo guión bajo y el nombre de la clase que lo contiene. Debido a
que el nombre modificado contiene el nombre de la clase adjunta, generalmente es único; no chocará con
nombres similares creados por otras clases en una jerarquía.

La manipulación de nombres ocurre solo para los nombres que aparecen dentro del código de una
declaración de clase , y luego solo para los nombres que comienzan con dos guiones bajos al principio.
Sin embargo, funciona para todos los nombres precedidos por guiones bajos dobles: tanto los atributos de
clase (incluidos los nombres de métodos) como los nombres de atributos de instancia asignados a uno
mismo. Por ejemplo, en una clase llamada Spam, un método llamado __meth se transforma en
_Spam__meth, y una referencia de atributo de instancia self.__X se transforma en self._Spam__X.

A pesar de la manipulación, siempre que la clase use la versión de doble guión bajo cada vez que se
refiera al nombre, todas sus referencias seguirán funcionando. Sin embargo, debido a que más de una
clase puede agregar atributos a una instancia, esta manipulación ayuda a evitar conflictos, pero debemos
pasar a un ejemplo para ver cómo.

¿Por qué usar atributos pseudoprivados?


Uno de los principales problemas que la función de atributo pseudoprivado pretende aliviar tiene que ver
con la forma en que se almacenan los atributos de instancia. En Python, todos los atributos de instancia
terminan en el objeto de instancia única en la parte inferior del árbol de clases y son compartidos por todas
las funciones de método de nivel de clase a las que se pasa la instancia. Esto es diferente del modelo de
C++, donde cada clase tiene su propio espacio para los miembros de datos que define.

Dentro del método de una clase en Python, cada vez que un método se asigna a un atributo
propio (p. ej., self.attr = valor), cambia o crea un atributo en la instancia (recuerde que las
búsquedas de herencia ocurren solo por referencia, no por asignación). Debido a que esto es
cierto incluso si varias clases en una jerarquía se asignan al mismo atributo, las colisiones son posibles.

Por ejemplo, suponga que cuando un programador codifica una clase, se supone que la clase posee el
nombre de atributo X en la instancia. En los métodos de esta clase, el nombre se establece y luego se
obtiene:

clase C1:
def meth1(self): self.X = 88 def meth2(self): # Supongo que X es mío
print(self.X)

Atributos de clase pseudoprivada | 945


Machine Translated by Google

Supongamos además que otro programador, trabajando de forma aislada, hace la misma suposición en
otra clase:

clase C2:
def meta(auto): auto.X = 99 def # Yo también

meta(auto): imprimir(auto.X)

Ambas clases funcionan por sí mismas. El problema surge si las dos clases alguna vez se mezclan en el
mismo árbol de clases:

clase C3(C1, C2): ...


yo = C3() # ¡Solo 1 X en I!

Ahora, el valor que obtiene cada clase cuando dice self.X dependerá de la última clase que se le asignó.
Debido a que todas las asignaciones a self.X se refieren a la misma instancia única, solo hay un atributo
X , IX, sin importar cuántas clases usen ese nombre de atributo.

Esto no es un problema si se espera y, de hecho, así es como se comunican las clases: la instancia es
memoria compartida. Sin embargo, para garantizar que un atributo pertenece a la clase que lo usa, prefije
el nombre con guiones bajos dobles en todos los lugares donde se use en la clase, como en este archivo
2.X/3.X, pseudoprivate.py:

class C1:
def meth1(self): self.__X = 88 def # Ahora X es mío
meth2(self): print(self.__X) class C2: def # Se convierte en _C1__X en I
meta(self): self.__X = 99 def methb(self):
print(self. __X) # Yo también

# Se convierte en _C2__X en I

clase C3 (C1, C2): aprobado


yo = C3() # Dos nombres X en I

I.meth1(); I.meta()
imprimir(I.__dict__)
I.meth2(); I.methb()

Cuando se anteponen así, los atributos X se expandirán para incluir los nombres de sus clases antes de
agregarse a la instancia. Si ejecuta una llamada de directorio en I o inspecciona su diccionario de espacio
de nombres después de que se hayan asignado los atributos, verá los nombres expandidos, _C1__X y
_C2__X, pero no X. Debido a que la expansión hace que los nombres sean más exclusivos dentro de la
instancia, la clase los codificadores pueden estar bastante seguros al asumir que realmente poseen
cualquier nombre que antepongan con dos guiones bajos:

% python pseudoprivate.py
{'_C2__X': 99, '_C1__X': 88}
88
99

Este truco puede evitar posibles colisiones de nombres en la instancia, pero tenga en cuenta que no
equivale a una verdadera privacidad. Si conoce el nombre de la clase envolvente, aún puede acceder a
cualquiera de estos atributos en cualquier lugar donde tenga una referencia a la instancia utilizando el
nombre completamente expandido (por ejemplo, I._C1__X = 77). Además, los nombres aún podrían
colisionar si los programadores sin saberlo usan explícitamente el patrón de nomenclatura expandido (poco probable, pero no

946 | Capítulo 31: Diseño con clases


Machine Translated by Google

imposible). Por otro lado, esta función hace que sea menos probable que accidentalmente pises los nombres de
una clase.

Los atributos pseudoprivados también son útiles en marcos o herramientas más grandes, tanto para evitar la
introducción de nuevos nombres de métodos que accidentalmente podrían ocultar definiciones en otras partes del
árbol de clases como para reducir la posibilidad de que los métodos internos sean reemplazados por nombres
definidos más abajo en el árbol. Si un método está diseñado para usarse solo dentro de una clase que puede
mezclarse con otras clases, el prefijo de doble subrayado prácticamente garantiza que el método no interfiera con
otros nombres en el árbol, especialmente en escenarios de herencia múltiple:

clase Super:
método def (auto): ... # Un método de aplicación real

Herramienta de clase:

def __método(yo): ... def # Se convierte en _herramienta__método

otro(yo): yo.__método() # Usar mi método interno

class Sub1(Herramienta, Super): ...


def acciones(self): self.method() # Ejecuta Super.método como se esperaba

class Sub2(Tool): def


__init__(self): self.method = 99 # No rompe Tool.__method

Encontramos brevemente la herencia múltiple en el Capítulo 26 y la exploraremos con más detalle más adelante
en este capítulo. Recuerde que las superclases se buscan según su orden de izquierda a derecha en las líneas
de encabezado de clase . Aquí, esto significa que Sub1 prefiere los atributos de herramienta a los de Super.
Aunque en este ejemplo podríamos obligar a Python a elegir primero los métodos de la clase de la aplicación
cambiando el orden de las superclases enumeradas en el encabezado de la clase Sub1 , los atributos
pseudoprivados resuelven el problema por completo. Los nombres pseudoprivados también evitan que las
subclases redefinan accidentalmente los nombres de los métodos internos, como en Sub2.

Una vez más, debo señalar que esta característica tiende a ser útil principalmente para proyectos más grandes
de varios programas, y luego solo para nombres seleccionados. No caiga en la tentación de saturar su código
innecesariamente; solo use esta característica para nombres que realmente necesitan ser controlados por una
sola clase. Aunque es útil en algunas herramientas generales basadas en clases, para programas más simples,
probablemente sea una exageración.

Para obtener más ejemplos que hacen uso de la función de nomenclatura __X , consulte las clases mixtas de
lister.py que se presentan más adelante en este capítulo en la sección de herencia múltiple, así como la discusión
de los decoradores de clases privadas en el Capítulo 39.

Si le preocupa la privacidad en general, es posible que desee revisar la emulación de atributos de instancias
privadas esbozados en la sección "Acceso a atributos: __getattr__ y __se tattr__" en la página 909 en el Capítulo
30, y ver el decorador de clase privada más completo que tenemos. Construiré con delegación en el Capítulo 39.
Aunque es posible emular verdaderos controles de acceso en las clases de Python, esto rara vez se hace en la
práctica, incluso para sistemas grandes.

Atributos de clase pseudoprivada | 947


Machine Translated by Google

Los métodos son objetos: enlazados o no enlazados


Los métodos en general, y los métodos enlazados en particular, simplifican la implementación de muchos
objetivos de diseño en Python. Conocimos brevemente los métodos vinculados mientras estudiábamos
__call__ en el Capítulo 30. La historia completa, que desarrollaremos aquí, resulta ser más general y flexible
de lo que cabría esperar.

En el Capítulo 19, aprendimos cómo las funciones pueden procesarse como objetos normales. Los métodos
también son un tipo de objeto y pueden usarse genéricamente de la misma manera que otros objetos:
pueden asignarse nombres, pasarse a funciones, almacenarse en estructuras de datos, etc., y al igual que
las funciones simples, calificar como " objetos de primera clase”. Sin embargo, debido a que se puede
acceder a los métodos de una clase desde una instancia o una clase, en realidad vienen en dos sabores
en Python: Objetos de método (clase) no vinculados: no self

Acceder a un atributo de función de una clase calificando la clase devuelve un objeto de método no
vinculado. Para llamar al método, debe proporcionar un objeto de instancia explícitamente como
primer argumento. En Python 3.X, un método independiente es lo mismo que una función simple y se
puede llamar a través del nombre de la clase; en 2.X es un tipo distinto y no se puede llamar sin
proporcionar una instancia.

Objetos de método enlazados (instancia): self + pares de funciones


Acceder a un atributo de función de una clase calificando una instancia devuelve un objeto de método
enlazado. Python empaqueta automáticamente la instancia con la función en el objeto del método
enlazado, por lo que no necesita pasar una instancia para llamar al método.

Ambos tipos de métodos son objetos completos; pueden transferirse por un programa a voluntad, al igual
que las cadenas y los números. Ambos también requieren una instancia en su primer argumento cuando se
ejecutan (es decir, un valor para uno mismo). Esta es la razón por la que hemos tenido que pasar una
instancia explícitamente al llamar a métodos de superclase desde métodos de subclase en ejemplos
anteriores (incluido el archivo employee.py de este capítulo); técnicamente, dichas llamadas producen
objetos de método no vinculados en el camino.

Al llamar a un objeto de método vinculado , Python le proporciona una instancia automáticamente: la


instancia utilizada para crear el objeto de método vinculado. Esto significa que los objetos de método
enlazado suelen ser intercambiables con objetos de función simple, y los hace especialmente útiles para
interfaces escritas originalmente para funciones (consulte la barra lateral "Por qué le importará: devoluciones
de llamada de método enlazado" en la página 953 para un caso de uso realista en GUI) .

Para ilustrar en términos simples, supongamos que definimos la siguiente clase:

clase
Spam: def doit(self,
mensaje): print(mensaje)

Ahora, en funcionamiento normal, creamos una instancia y llamamos a su método en un solo paso para
imprimir el argumento pasado:

948 | Capítulo 31: Diseño con clases


Machine Translated by Google

objeto1 = correo no deseado ()

objeto1.doit('hola mundo')

En realidad, sin embargo, se genera un objeto de método enlazado en el camino, justo antes del
paréntesis de la llamada al método. De hecho, podemos obtener un método enlazado sin llamarlo. Una expresión
object.name se evalúa como un objeto como lo hacen todas las expresiones. En el
siguiente, devuelve un objeto de método enlazado que empaqueta la instancia (objeto1) con
la función de método (Spam.doit). Podemos asignar este par de métodos enlazados a otro
nombre y luego llámelo como si fuera una función simple:

objeto1 = correo no deseado ()

x = objeto1.hacer x('hola # Objeto de método enlazado: instancia+función


mundo') # Mismo efecto que object1.doit('...')

Por otro lado, si calificamos la clase para hacerlo , obtenemos un método no vinculado
objeto, que es simplemente una referencia al objeto de función. Para llamar a este tipo de método,
debemos pasar una instancia como el argumento más a la izquierda; no hay ninguno en la expresión
de lo contrario, y el método lo espera:

objeto1 = correo no deseado ()

t = Spam.doit # Objeto de método no vinculado (una función en 3.X: vea más adelante)
t(objeto1, 'hola') # Pase en instancia (si el método espera uno en 3.X)

Por extensión, se aplican las mismas reglas dentro del método de una clase si hacemos referencia a los atributos propios
que se refieren a funciones en la clase. Una expresión self.method es un objeto de método enlazado
porque self es un objeto de instancia:

Huevos de clase:
def m1(uno mismo, n):
imprimir
def m2(auto):
x = self.m1 x(42) # Otro objeto de método enlazado
# Parece una función simple

Huevos().m2() # Estampados 42

La mayoría de las veces, llama a los métodos inmediatamente después de obtenerlos con el atributo
calificación, por lo que no siempre nota los objetos de método generados en el camino.
Pero si comienza a escribir código que llama a objetos de forma genérica, debe tener cuidado de tratar
métodos no enlazados especialmente, normalmente requieren un objeto de instancia explícito para ser
aprobada en.

Para una excepción opcional a esta regla, vea la discusión de estática y


métodos de clase en el próximo capítulo, y la breve mención de uno en el
Siguiente sección. Al igual que los métodos vinculados, los métodos estáticos pueden enmascararse como
funciones básicas porque no esperan instancias cuando se les llama. En términos
generales, Python admite tres tipos de métodos de nivel de clase:
instancia, estático y clase, y 3.X permite funciones simples en clases,
también. Los métodos de metaclase del Capítulo 40 también son distintos, pero son
esencialmente métodos de clase con menos alcance.

los métodos son objetos: enlazados o no enlazados | 949


Machine Translated by Google

Los métodos independientes son funciones en 3.X

En Python 3.X, el lenguaje ha eliminado la noción de métodos independientes. Lo que aquí describimos
como un método no vinculado se trata como una función simple en 3.X. Para la mayoría de los
propósitos, esto no hace ninguna diferencia en su código; de cualquier manera, se pasará una instancia
al primer argumento de un método cuando se llame a través de una instancia.

Sin embargo, los programas que realizan pruebas de tipo explícitas pueden verse afectados: si imprime
el tipo de un método de nivel de clase sin instancia, muestra "método no vinculado" en 2.X y "función"
en 3.X.

Además, en 3.X está bien llamar a un método sin una instancia, siempre que el método no espere una
y lo llame solo a través de la clase y nunca a través de una instancia.
Es decir, Python 3.X pasará una instancia a métodos solo para llamadas a través de instancias. Al llamar a través de una clase, debe

pasar una instancia manualmente solo si el método espera una: C:\code> c:\python33\python >>> class Selfless: def __init__(self,

data): self.data = data def desinteresado(arg1, arg2): devuelve arg1 + arg2 def normal(self, arg1, arg2):

# Una función simple en 3.X

# Instancia esperada cuando se llama


devolver self.data + arg1 + arg2

>>> X = Desinteresado(2)
>>> X.normal(3, 4) # Instancia pasada a sí mismo automáticamente: 2+(3+4)
9
>>> Desinteresado.normal(X, 3, 4) 9 # auto esperado por método: pasar manualmente
>>> Desinteresado.desinteresado(3,
4) 7 # Sin instancia: funciona en 3.X, falla en 2.X.

La última prueba de esto falla en 2.X, porque los métodos independientes requieren que se pase una
instancia de forma predeterminada; funciona en 3.X porque dichos métodos se tratan como funciones
simples que no requieren una instancia. Aunque esto elimina algunas posibles trampas de errores en
3.X (¿qué pasa si un programador se olvida accidentalmente de pasar una instancia?), permite que
los métodos de una clase se usen como funciones simples siempre que no se pasen y no espere un
"autocontrol". ” argumento de instancia.

Sin embargo, las siguientes dos llamadas aún fallan tanto en 3.X como en 2.X: la primera (llamar a
través de una instancia) pasa automáticamente una instancia a un método que no la espera, mientras
que la segunda (llamar a través de una clase) no lo hace. pasar una instancia a un método que espera
uno (el texto del mensaje de error aquí es por 3.3):
>>> X.desinteresado(3, 4)
TypeError: selfless() toma 2 argumentos posicionales pero se dieron 3

>>> Desinteresado.normal(3, 4)
TypeError: normal() falta 1 argumento posicional requerido: 'arg2'

950 | Capítulo 31: Diseño con clases


Machine Translated by Google

Debido a este cambio, la función incorporada y el decorador de staticmethod descritos en


el próximo capítulo no es necesario en 3.X para métodos sin un argumento propio que son
llamado solo a través del nombre de la clase , y nunca a través de una instancia; tales métodos son
se ejecutan como funciones simples, sin recibir un argumento de instancia. En 2.X, tales llamadas son
errores a menos que una instancia se pase manualmente o el método esté marcado como estático
(más sobre métodos estáticos en el próximo capítulo).

Es importante ser consciente de las diferencias de comportamiento en 3.X, pero los métodos enlazados son
generalmente más importante desde una perspectiva práctica de todos modos. Debido a que se emparejan para reunir
la instancia y la función en un solo objeto, pueden tratarse como invocables.
genéricamente. La siguiente sección demuestra lo que esto significa en el código.

Para obtener una ilustración más visual del tratamiento del método independiente en Python
3.X y 2.X, vea también el ejemplo de lister.py en la herencia múltiple
sección más adelante en este capítulo. Sus clases imprimen el valor de los métodos obtenidos
de instancias y clases, en ambas versiones de Python, como métodos no vinculados en 2.X y funciones
simples en 3.X. También tenga en cuenta que este
el cambio es inherente a 3.X en sí mismo, no al modelo de clase de nuevo estilo al que data.

Métodos enlazados y otros objetos a los que se puede llamar

Como se mencionó anteriormente, los métodos vinculados se pueden procesar como objetos genéricos, al igual que
funciones simples: se pueden pasar alrededor de un programa arbitrariamente. Además, porque
Los métodos enlazados combinan una función y una instancia en un solo paquete, pueden
ser tratado como cualquier otro objeto invocable y no requiere una sintaxis especial cuando se invoca.
Lo siguiente, por ejemplo, almacena cuatro objetos de método enlazados en una lista y los llama
más tarde con expresiones de llamada normales:

>>> Número de clase:


def __init__(uno mismo, base):
self.base = base
def doble(auto):
volver self.base * 2
def triple(auto):
volver self.base * 3

>>> x = Número(2) >>> # Objetos de instancia de clase


y = Número(3) >>> z = # Estado + métodos
Número(4)
>>> x.doble() # Llamadas inmediatas normales
4

>>> actos = [x.doble, y.doble, y.triple, z.doble] >>> for acto en actos: # Lista de métodos enlazados
# Las llamadas son diferidas
imprimir (acto ()) # Funciones de llamada como si

4
6

los métodos son objetos: enlazados o no enlazados | 951


Machine Translated by Google

9
8

Al igual que las funciones simples, los objetos de método enlazado tienen información de introspección propia,
incluidos atributos que dan acceso al objeto de instancia y la función de método que emparejan. Llamar al
método enlazado simplemente envía el par:
>>> límite = x.doble >>>
límite.__self__, límite.__func__
(<__principal__.Número objeto en 0x...etc...>, <función Número.doble en 0x...etc...> ) >>> enlazado.__self__.base

2
>>> enlazado() # Llamadas abound.__func__(bound.__self__, ...)
4

Otros llamables

De hecho, los métodos enlazados son solo uno de los pocos tipos de objetos a los que se puede llamar en
Python. Como se demuestra a continuación, las funciones simples codificadas con def o lambda, las instancias
que heredan una __llamada__ y los métodos de instancia vinculados pueden tratarse y llamarse de la misma manera .
camino:

>>> def cuadrado(arg):


return arg ** 2 # Funciones simples (def o lambda)

>>> clase Suma:


def __init__(self, val): self.val = # Instancias llamables
val def __call__(self, arg):
return self.val + arg

>>> clase Producto: def


__init__(self, val): self.val = val # Métodos enlazados
def method(self, arg):
return self.val * arg

>>> sobject = Suma(2) >>>


pobject = Producto(3) >>>
acciones = [square, sobject, pobject.method] # Función, instancia, método

>>> para actuar en acciones: # Los tres llamaron de la misma manera


print(act(5)) # Llame a cualquier invocable de un argumento

25
7
15

>>> acciones[-1](5) # Índice, comprensiones, mapas


15
>>> [act(5) for act in actions] [25, 7, 15] >>>
list(mapa(lambda act: act(5), acciones))
[25, 7, 15]

952 | Capítulo 31: Diseño con clases


Machine Translated by Google

Técnicamente hablando, las clases también pertenecen a la categoría de objetos invocables, pero normalmente
los llamamos para generar instancias en lugar de hacer un trabajo real: una sola acción está mejor codificada
como una función simple que una clase con un constructor, pero la clase aquí sirve para ilustrar su naturaleza
exigible:

>>> class Negate: def


__init__(self, val): self.val = -val # Las clases también son invocables
def __repr__(self): return # Pero llamado por objeto, no por trabajo
str(self.val) # Formato de impresión de instancia

>>> acciones = [square, sobject, pobject.method, Negate] >>> for actuar en # Llamar a una clase también

acciones: imprimir(act(5))

25
7
15
-5
>>> [act(5) para actuar en acciones] [25, 7, # ¡Ejecuta __repr__ no __str__!
15, ÿ5]

>>> tabla = {act(5): acto por acto en acciones} >>> for (clave, # 3.X/ 2.7 comprensión de dictados
valor) en tabla.items(): print('{0:2} => {1}'.format( clave, valor)) #
2.6+/ 3.X str.format

25 => <cuadrado de función en 0x0000000002987400> 15 =>


<método vinculado Producto.método de <__main__.Objeto de producto en ...etc...>> -5 => <clase '__main__.Negate'>
7 => < __main__.Objeto de suma en 0x000000000298BE48>

Como puede ver, los métodos enlazados y el modelo de objetos invocables de Python en general son algunas
de las muchas formas en que el diseño de Python lo convierte en un lenguaje increíblemente flexible.

Ahora debería comprender el modelo de objeto de método. Para ver otros ejemplos de métodos enlazados en
funcionamiento, consulte la próxima barra lateral "Por qué le importará: devoluciones de llamada de método
enlazado" en la página 953 , así como la discusión del capítulo anterior sobre los controladores de devolución
de llamada en la sección sobre el método __llamada__.

Por qué le importará: devoluciones de llamadas de métodos


enlazados Debido a que los métodos enlazados emparejan automáticamente una instancia con la función de
método de una clase, puede usarlos en cualquier lugar donde se espere una función simple. Uno de los lugares
más comunes en los que verá que esta idea se pone en práctica es en el código que registra métodos como
controladores de devolución de llamada de eventos en la interfaz GUI de tkinter (llamada Tkinter en Python 2.X) que hemos conocido antes.
Como revisión, aquí está el caso simple:

def
handler(): ...usar alcances globales o de cierre para el estado...
...
widget = Botón (texto = 'correo no deseado', comando = controlador)

Para registrar un controlador para eventos de clic de botón, generalmente pasamos un objeto invocable que no
acepta argumentos al argumento de la palabra clave del comando . Los nombres de funciones (y lambdas) funcionan

los métodos son objetos: enlazados o no enlazados | 953


Machine Translated by Google

aquí, y también lo hacen los métodos de nivel de clase, aunque deben ser métodos vinculados si esperan una
instancia cuando se los llama:

class MyGui:
def
handler(self): ...use self.attr
para el estado... def
makewidgets(self): b = Button(text='spam', command=self.handler)

Aquí, el controlador de eventos es self.handler, un objeto de método enlazado que recuerda tanto a sí mismo
como a MyGui.handler. Debido a que self se referirá a la instancia original cuando el controlador se invoque
posteriormente en los eventos, el método tendrá acceso a los atributos de la instancia que pueden retener el
estado entre eventos, así como a los métodos de nivel de clase. Con funciones simples, el estado normalmente
debe conservarse en variables globales o en los ámbitos de función adjuntos.

Consulte también la discusión sobre la sobrecarga del operador __call__ en el Capítulo 30 para conocer otra
forma de hacer que las clases sean compatibles con las API basadas en funciones, y lambda en el Capítulo 19
para conocer otra herramienta que se usa a menudo en los roles de devolución de llamada. Como se señaló
en el primero de estos, generalmente no es necesario envolver un método enlazado en una lambda; el método
enlazado en el ejemplo anterior ya difiere la llamada (tenga en cuenta que no hay paréntesis para activar uno),
por lo que agregar una lambda aquí no tendría sentido.

Las clases son objetos: fábricas de objetos genéricos


A veces, los diseños basados en clases requieren que se creen objetos en respuesta a condiciones que no se
pueden predecir cuando se escribe un programa. El patrón de diseño de la fábrica permite este enfoque diferido.
Debido en gran parte a la flexibilidad de Python, las fábricas pueden adoptar múltiples formas, algunas de las
cuales no parecen especiales en absoluto.

Debido a que las clases también son objetos de "primera clase", es fácil pasarlas por un programa, almacenarlas
en estructuras de datos, etc. También puede pasar clases a funciones que generan tipos arbitrarios de objetos;
tales funciones a veces se denominan fábricas en los círculos de diseño de programación orientada a objetos.
Las fábricas pueden ser una tarea importante en un lenguaje fuertemente tipado como C++, pero son casi
triviales de implementar en Python.

Por ejemplo, la sintaxis de llamada que vimos en el Capítulo 18 puede llamar a cualquier clase con cualquier
número de argumentos constructores posicionales o de palabras clave en un solo paso para generar cualquier
tipo de instancia:2

def factory(aClass, *pargs, **kargs): return # Tupla de Varargs, dict


aClass(*pargs, **kargs) # Llamar a una clase (o aplicar solo en 2.X)

correo no deseado de clase:

2. En realidad, esta sintaxis puede invocar cualquier objeto invocable, incluidas funciones, clases y métodos. Por lo tanto, la
función de fábrica aquí también puede ejecutar cualquier objeto invocable, no solo una clase (a pesar del nombre del argumento).
Además, como aprendimos en el Capítulo 18, Python 2.X tiene una alternativa a aClass(*pargs, **kargs): la llamada
incorporada apply(aClass, pargs, kargs) , que se eliminó en Python 3.X por su redundancia y limitaciones.

954 | Capítulo 31: Diseño con clases


Machine Translated by Google

def doit(yo, mensaje):


print(mensaje)

Persona de clase:
def __init__(self, nombre, trabajo=Ninguno):
self.name = nombre self.job = trabajo

objeto1 = fábrica (correo no deseado) # Hacer un objeto de


Spam object2 = fábrica (Persona, "Arturo", "Rey") # Hacer un objeto de Persona objeto3 =
fábrica (Persona, nombre = 'Brian') # Ídem, con palabras clave y por defecto

En este código, definimos una función generadora de objetos llamada fábrica. Espera que se le pase un
objeto de clase (cualquier clase servirá) junto con uno o más argumentos para el constructor de la clase. La
función utiliza una sintaxis de llamada especial "varargs" para llamar a la función y devolver una instancia.

El resto del ejemplo simplemente define dos clases y genera instancias de ambas pasándolas a la función de fábrica . Y esa
es la única función de fábrica que necesitará escribir en Python; funciona para cualquier clase y cualquier argumento de
constructor. Si ejecuta esto en vivo (factory.py), sus objetos se verán así: >>> object1.doit(99) 99 >>> object2.name,
object2.job ('Arthur', 'King') >>> objeto3.nombre, objeto3.trabajo ('Brian', Ninguno)

A estas alturas, debería saber que todo es un objeto de "primera clase" en Python, incluidas las clases, que
generalmente son solo entradas del compilador en lenguajes como C++. Es natural pasarlos de esta manera.
Sin embargo, como se mencionó al comienzo de esta parte del libro, solo los objetos derivados de las clases
realizan programación orientada a objetos completa en Python.

¿Por qué Fábricas?

Entonces, ¿de qué sirve la función de fábrica (además de proporcionar una excusa para ilustrar objetos de
primera clase en este libro)? Desafortunadamente, es difícil mostrar aplicaciones de este patrón de diseño
sin enumerar mucho más código del que tenemos aquí. Sin embargo, en general, una fábrica de este tipo
podría permitir aislar el código de los detalles de la construcción de objetos configurados dinámicamente.

Por ejemplo, recuerde el ejemplo del procesador presentado en el resumen del Capítulo 26, y luego
nuevamente como ejemplo de composición anteriormente en este capítulo. Acepta objetos de lectura y
escritura para procesar flujos de datos arbitrarios. La versión original de este ejemplo se pasó manualmente
en instancias de clases especializadas como FileWriter y SocketReader para personalizar los flujos de datos
que se procesan; más tarde, pasamos objetos codificados de archivo, flujo y formateador. En un escenario
más dinámico, se pueden usar dispositivos externos como archivos de configuración o GUI para configurar
los flujos.

clases son objetos: fábricas de objetos genéricos | 955


Machine Translated by Google

En un mundo tan dinámico, es posible que no podamos codificar la creación de objetos de interfaz de
transmisión en nuestros scripts, sino que podríamos crearlos en tiempo de ejecución de acuerdo con el
contenido de un archivo de configuración.

Tal archivo podría simplemente proporcionar el nombre de cadena de una clase de flujo que se importará
desde un módulo, además de un argumento de llamada de constructor opcional. Las funciones o el código de
estilo de fábrica pueden ser útiles aquí porque nos permitirían obtener y pasar clases que no están codificadas
en nuestro programa con anticipación. De hecho, es posible que esas clases ni siquiera existieran cuando
escribimos nuestro código:

classname = ...analizar desde el archivo de configuración...


classarg = ...analizar desde el archivo de configuración...

import streamtypes aclass # Código personalizable


= getattr(streamtypes, classname) lector = factory(aclass, # Recuperar del módulo
classarg) procesador(lector, ...) # O unaclase(classarg)

Aquí, el getattr incorporado se usa nuevamente para obtener un atributo de módulo dado un nombre de
cadena (es como decir obj.attr, pero attr es una cadena). Debido a que este fragmento de código asume un
solo argumento de constructor, no necesita estrictamente fábrica; podríamos crear una instancia con solo una
clase (classarg). Sin embargo, la función de fábrica puede resultar más útil en presencia de listas de
argumentos desconocidos, y el patrón general de codificación de fábrica puede mejorar la flexibilidad del
código.

Herencia Múltiple: Clases “Mix-in”


Nuestro último patrón de diseño es uno de los más útiles y servirá como tema para un ejemplo más realista
para concluir este capítulo y apuntar hacia el siguiente. Como beneficio adicional, el código que escribiremos
aquí puede ser una herramienta útil.

Muchos diseños basados en clases requieren la combinación de conjuntos dispares de métodos. Como hemos
visto, en una declaración de clase , se puede enumerar más de una superclase entre paréntesis en la línea de
encabezado. Cuando hace esto, aprovecha la herencia múltiple: la clase y sus instancias heredan nombres de
todas las superclases enumeradas.

Al buscar un atributo, la búsqueda de herencia de Python atraviesa todas las superclases en el encabezado
de la clase de izquierda a derecha hasta que se encuentra una coincidencia. Técnicamente, debido a que
cualquiera de las superclases puede tener sus propias superclases, esta búsqueda puede ser un poco más
compleja para árboles de clases más grandes:

• En las clases clásicas (las predeterminadas hasta Python 3.0), la búsqueda de atributos en todos los casos
avanza en profundidad, primero hasta la parte superior del árbol de herencia y luego de izquierda a
derecha. Este orden generalmente se llama DFLR, por su ruta de profundidad primero, de izquierda a

derecha. • En las clases de estilo nuevo (opcional en 2.X y estándar en 3.X), la búsqueda de atributos suele
ser como antes, pero en los patrones de diamantes avanza por niveles de árbol antes de ascender, de
forma más amplia. Este orden generalmente se llama el nuevo

956 | Capítulo 31: Diseño con clases


Machine Translated by Google

estilo MRO, para el orden de resolución de métodos, aunque se usa para todos los atributos, no solo para
los métodos.

La segunda de estas reglas de búsqueda se explica completamente en la discusión de clase del nuevo estilo en
el próximo capítulo. Aunque es difícil de entender sin el código del próximo capítulo (y algo raro de crear usted
mismo), los patrones de diamantes aparecen cuando varias clases en un árbol comparten una superclase
común; el orden de búsqueda de nuevo estilo está diseñado para visitar dicha superclase compartida solo una
vez, y después de todas sus subclases. Sin embargo, en cualquiera de los modelos, cuando una clase tiene
varias superclases, se buscan de izquierda a derecha según el orden indicado en las líneas de encabezado de
la instrucción de clase .

En general, la herencia múltiple es buena para modelar objetos que pertenecen a más de un conjunto. Por
ejemplo, una persona puede ser ingeniero, escritor, músico, etc., y heredar propiedades de todos esos conjuntos.
Con la herencia múltiple, los objetos obtienen la unión del comportamiento en todas sus superclases. Como
veremos más adelante, la herencia múltiple también permite que las clases funcionen como paquetes generales
de atributos mezclables.

Aunque es un patrón útil, la principal desventaja de la herencia múltiple es que puede generar un conflicto
cuando el mismo nombre de método (u otro atributo) se define en más de una superclase. Cuando esto ocurre,
el conflicto se resuelve automáticamente mediante el orden de búsqueda heredado o manualmente en su código:

• Predeterminado: de forma predeterminada, la herencia elige la primera aparición de un atributo que encuentra
cuando se hace referencia a un atributo normalmente , por ejemplo , mediante self.method() . En este
modo, Python elige el más bajo y el más a la izquierda en las clases clásicas, y en los patrones que no son
diamantes en todas las clases; Las clases de nuevo estilo pueden elegir una opción a la derecha antes de
una arriba en diamantes.

• Explícito: en algunos modelos de clase, es posible que a veces necesite seleccionar un atributo
explícitamente haciendo referencia a él a través de su nombre de clase, con superclass.method(self), por
ejemplo. Su código rompe el conflicto y anula el valor predeterminado de la búsqueda: para seleccionar

una opción a la derecha o encima del valor predeterminado de la búsqueda heredada.

Este es un problema solo cuando el mismo nombre aparece en varias superclases y no desea usar la primera
heredada. Debido a que este no es un problema tan común en el código típico de Python como puede parecer,
aplazaremos los detalles sobre este tema hasta que estudiemos las clases de nuevo estilo y su MRO y súper
herramientas en el próximo capítulo, y revisaremos esto como un " gotcha” al final de ese capítulo. Primero, sin
embargo, la siguiente sección demuestra un caso de uso práctico para múltiples herramientas basadas en
herencia.

Codificación de clases de visualización

combinadas Quizás la forma más común de utilizar la herencia múltiple es “mezclar” métodos de propósito
general de las superclases. Tales superclases generalmente se denominan clases mixtas: proporcionan métodos
que se agregan a las clases de aplicación por herencia. En cierto sentido, las clases mixtas son similares a los
módulos: proporcionan paquetes de métodos para usar en sus subclases de clientes. Sin embargo, a diferencia
de las funciones simples en los módulos, los métodos en mix-in

Herencia Múltiple: Clases “Mix-in” | 957


Machine Translated by Google

las clases también pueden participar en jerarquías de herencia y tener acceso a la instancia propia para usar
información de estado y otros métodos en sus árboles.

Por ejemplo, como hemos visto, la forma predeterminada de Python para imprimir un objeto de instancia de clase no es increíblemente útil: >>>

class Spam: def __init__(self): self.data1 = "food"

# Sin __repr__ o __str__

>>> X = Correo
basura() >>> imprimir(X) # Predeterminado: nombre de clase + dirección
(id) <__main__. Objeto spam en 0x00000000029CA908> # Lo mismo en 2.X, pero dice "instancia"

Como vio en el estudio de caso del Capítulo 28 y en la cobertura de sobrecarga de operadores del Capítulo 30,
puede proporcionar un método __str__ o __repr__ para implementar una representación de cadena personalizada
propia. Pero, en lugar de codificar uno de estos en todas y cada una de las clases que desea imprimir, ¿por qué
no codificarlo una vez en una clase de herramienta de propósito general y heredarlo en todas sus clases?

Para eso están los complementos. Definir un método de visualización en una superclase mixta una vez nos
permite reutilizarlo en cualquier lugar donde queramos ver un formato de visualización personalizado, incluso en
clases que ya tengan otra superclase. Ya hemos visto herramientas que hacen un trabajo relacionado:

• La clase AttrDisplay del Capítulo 28 formateó los atributos de instancia en un método genérico __repr__ , pero
no trepó a los árboles de clase y se utilizó solo en el modo de herencia única. • El módulo classtree.py del
Capítulo 29 definía funciones para escalar y dibujar árboles de clases, pero no mostraba los atributos de los

objetos en el camino y no estaba diseñado como una clase heredable.

Aquí, revisaremos las técnicas de estos ejemplos y las expandiremos para codificar un conjunto de tres clases
combinadas que sirven como herramientas de visualización genéricas para enumerar atributos de instancia,
atributos heredados y atributos en todos los objetos en un árbol de clases. . También usaremos nuestras
herramientas en modo de herencia múltiple e implementaremos técnicas de codificación que hacen que las clases
sean más adecuadas para usar como herramientas genéricas.

A diferencia del Capítulo 28, también codificaremos esto con __str__ en lugar de __repr__. Esto es en parte un
problema de estilo y limita su función a imprimir y str, pero las pantallas que desarrollaremos serán lo
suficientemente ricas como para clasificarlas como más fáciles de usar que como código.
Esta política también deja a las clases de clientes la opción de codificar una visualización alternativa de nivel
inferior para ecos interactivos y apariencias anidadas con __repr__. El uso de __repr__ aquí aún permitiría una
__str__ alternativa, pero la naturaleza de las pantallas que implementaremos sugiere con más fuerza un rol de
__str__ . Véase el Capítulo 30 para una revisión de estas distinciones.

958 | Capítulo 31: Diseño con clases


Machine Translated by Google

Listado de atributos de instancia con __dict__

Comencemos con el caso simple: listar atributos adjuntos a una instancia. La siguiente clase, codificada en el
archivo listinstance.py, define un complemento llamado ListInstance que sobrecarga el método __str__ para todas
las clases que lo incluyen en sus líneas de encabezado.
Debido a que esto está codificado como una clase, ListInstance es una herramienta genérica cuya lógica de formato
se puede usar para instancias de cualquier cliente de subclase:

#!python
# Archivo listinstance.py (2.X + 3.X)

clase ListInstance:
"""

Clase mixta que proporciona una impresión () o str () formateada de instancias a través de
la herencia de __str__ codificado aquí; muestra atributos de instancia solamente; self es una
instancia de la clase más baja; __X nombres evitan conflictos con los atributos del cliente
"""

def __atributos(uno mismo):


''
resultado =
for attr in sorted(self.__dict__): result
+= '\t%s=%s\n' % (attr, self.__dict__[attr]) return result

def __str__(self):
return '<Instancia de %s, dirección %s:\n%s>' % (
self.__class__.__name__, # El nombre de mi clase
id(self), self.__attrnames()) # Mi dirección
# lista nombre=valor

if __name__ == '__main__':
import testmixin
testmixin.tester(ListInstance)

Todo el código de esta sección se ejecuta en Python 2.X y 3.X. Una nota de codificación: este código exhibe un
patrón de comprensión clásico, y podría ahorrar algo de espacio en el programa implementando el método
__attrnames aquí de manera más concisa con una expresión generadora que se activa mediante el método de
combinación de cadenas , pero podría decirse que es menos claro: expresiones que envolver líneas como esta
generalmente debería hacer que considere alternativas de codificación más simples:

def __attrnames(self):
return ''.join('\t%s=%s\n' % (attr, self.__dict__ [attr]) for attr in
sorted(self.__dict__))
ListInstance utiliza algunos trucos explorados anteriormente para extraer el nombre y los atributos de la clase de la
instancia:

• Cada instancia tiene un atributo __class__ integrado que hace referencia a la clase a partir de la cual se creó, y
cada clase tiene un atributo __name__ que hace referencia al nombre en el encabezado, por lo que la
expresión self.__class__.__name__ obtiene el nombre de una instancia. clase.

Herencia Múltiple: Clases “Mix-in” | 959


Machine Translated by Google

• Esta clase hace la mayor parte de su trabajo simplemente escaneando el diccionario de atributos de la
instancia (recuerde, se exporta en __dict__) para crear una cadena que muestre los nombres y valores
de todos los atributos de la instancia. Las claves del diccionario están ordenadas para afinar cualquier
diferencia de orden entre las versiones de Python.

En estos aspectos, ListInstance es similar a la visualización de atributos del Capítulo 28; de hecho, es en
gran medida solo una variación de un tema. Sin embargo, nuestra clase aquí usa dos técnicas adicionales:

• Muestra la dirección de memoria de la instancia llamando a la función integrada id , que devuelve la


dirección de cualquier objeto (por definición, un identificador de objeto único, que será útil en
mutaciones posteriores de este código).

• Utiliza el patrón de nomenclatura pseudoprivado para su método de trabajo: __attrnames. Como


aprendimos anteriormente en este capítulo, Python localiza automáticamente cualquier nombre de
este tipo en su clase adjunta al expandir el nombre del atributo para incluir el nombre de la clase (en
este caso, se convierte en _ListInstance__attrnames). Esto es válido tanto para los atributos de clase
(como los métodos) como para los atributos de instancia adjuntos a uno mismo. Como se señaló en
la primera versión del Capítulo 28, este comportamiento es útil en una herramienta general como esta,
ya que garantiza que sus nombres no entren en conflicto con los nombres utilizados en sus subclases de clientes.

Debido a que ListInstance define un método de sobrecarga del operador __str__ , las instancias derivadas
de esta clase muestran sus atributos automáticamente cuando se imprimen, brindando un poco más de
información que una simple dirección. Aquí está la clase en acción, en modo de herencia única, mezclada
con la clase de la sección anterior (este código funciona igual en Python 3.X y 2.X, aunque las pantallas de
repetición predeterminadas de 2.X usan la etiqueta "instancia" en lugar de “objeto”):

>>> from listinstance import ListInstance >>> class


Spam(ListInstance): def __init__(self): self.data1 = 'comida' # Heredar un método __str__

>>> x = correo
basura() >>> imprimir(x) # print() y str() ejecutan __str__
<Instancia de Spam, dirección 43034496: data1=food

>

También puede buscar y guardar la salida de la lista como una cadena sin imprimirla con str, y los ecos interactivos aún usan el formato

predeterminado porque nos queda __repr__ como una opción para los clientes: >>> display = str(x) >>> monitor

# Imprima esto para interpretar escapes

'<Instancia de Spam, dirección 43034496:\n\tdata1=comida\n>'

>>> x # El __repr__ sigue siendo un valor predeterminado

<__principal__.Objeto de spam en 0x000000000290A780>

960 | Capítulo 31: Diseño con clases


Machine Translated by Google

La clase ListInstance es útil para cualquier clase que escriba, incluso las clases que ya tienen una o
más superclases. Aquí es donde la herencia múltiple es útil: al agregar ListInstance a la lista de
superclases en un encabezado de clase (es decir, mezclándolo), obtiene su __str__ "gratis" mientras
aún hereda de la(s) superclase(s) existente(s). El archivo testmixin0.py demuestra con un script de
prueba de primer corte:

# Archivo testmixin0.py de
listinstance importar ListInstance # Obtener clase de herramienta de listado

class Super: def


__init__(self): self.data1 = # Superclase __init__
'spam' def ham(self): pasar # Crear atributos de instancia

class Sub(Super, ListInstance): def # Mezclar en jamón y un __str__


__init__(self): Super.__init__(self) # Los anunciantes tienen acceso a sí mismos

self.data2 = 'huevos' self.data3 =


42 def spam(self): pasar # Más atributos de instancia

# Definir otro método aquí

si __nombre__ == '__principal__':
X = Sub()
imprimir(X) # Ejecutar __str__ mezclado

Aquí, Sub hereda nombres tanto de Super como de ListInstance; es un compuesto de sus propios
nombres y nombres en sus dos superclases. Cuando crea una subinstancia y la imprime,
automáticamente obtiene la representación personalizada mezclada desde ListInstance (en este
caso, la salida de este script es la misma en Python 3.X y 2.X, excepto por las direcciones de objetos,
que naturalmente pueden varían según el proceso): c:\code> python testmixin0.py <Instancia de
Sub, dirección 44304144: data1=spam data2=eggs data3=42

>

Este script de prueba testmixin0 funciona, pero codifica el nombre de la clase probada en el código y
dificulta experimentar con alternativas, como lo haremos en un momento.
Para ser más flexibles, podemos tomar prestada una página de los recargadores de módulos del
Capítulo 25 y pasar el objeto a probar, como en el siguiente script de prueba mejorado, testmixin, el
que realmente usan todos los códigos de autoevaluación de los módulos de clase lister. . En este
contexto, el objeto que se pasa al probador es una clase mixta en lugar de una función, pero el
principio es similar: todo califica como un objeto aceptable de "primera clase" en Python:

#!python #
Archivo testmixin.py (2.X + 3.X)
"""

Probador de mixin lister genérico: similar al recargador transitivo en


Capítulo 25, pero pasa un objeto de clase al probador (no funciona),

Herencia Múltiple: Clases “Mix-in” | 961


Machine Translated by Google

y testByNames agrega la carga del módulo y la clase por nombre


cuerdas aquí, de acuerdo con el patrón de fábricas del Capítulo 31.
"""

importar importlib

def tester(claselista, sept=Falso):

clase súper:
def __init__(self): self.data1 # Superclase __init__
= 'spam' def ham(self): # Crear atributos de instancia

pasar

class Sub(Super, listerclass): def # Mezclar en jamón y un __str__


__init__(self): Super.__init__(self) # Los anunciantes tienen acceso a sí mismos

self.data2 = 'huevos' # Más atributos de instancia


self.data3 = 42
def spam(auto): pasar # Definir otro método aquí

instancia = Sub() # Devolver instancia con __str__ de lister


print(instancia) if sept: # Ejecutar __str__ mezclado (o vía str(x))
print('-' * 80)

def testByNames(modname, classname, sept=False):


modobject = importlib.import_module(modname) # Importar por cadena de nombre
listerclass = getattr(modobject, classname) tester(listerclass, # Obtener atributo por cadena de nombre

sept)

si __nombre__ == '__principal__':
testByNames('listainstancia', 'ListaInstancia', True) testByNames('listaheredada', # Prueba los tres aquí
'ListaInheredada', True)
testByNames('árbol de lista', 'Árbol de lista', Falso)

Mientras está en eso, este script también agrega la capacidad de especificar el módulo de prueba y la clase por nombre
cadena, y aprovecha esto en su código de autocomprobación, una aplicación del patrón de fábrica
mecánica descrita anteriormente. Aquí está el nuevo script en acción, siendo ejecutado por el lister
módulo que lo importa para probar su propia clase (con los mismos resultados en 2.X y 3.X nuevamente);
también podemos ejecutar el script de prueba, pero ese modo prueba las dos variantes de lister, que
todavía tenemos que ver (¡o codificar!):

c:\code> python listinstance.py


<Instancia de Sub, domicilio 43256968:
datos1=correo no deseado

datos2=huevos
datos3=42
>

c:\código> python testmixin.py


<Instancia de Sub, domicilio 43977584:
datos1=correo no deseado

datos2=huevos
datos3=42

962 | Capítulo 31: Diseño con clases


Machine Translated by Google

>
...y se acercan las pruebas de otras dos clases de listers...

La clase ListInstance que hemos codificado hasta ahora funciona en cualquier clase en la que esté mezclada porque self se
refiere a una instancia de la subclase que atrae esta clase, sea lo que sea.
Una vez más, en cierto sentido, las clases combinadas son el equivalente de clase de los módulos: paquetes de métodos
útiles en una variedad de clientes. Por ejemplo, aquí está ListInstance trabajando nuevamente en modo de herencia única en
instancias de una clase diferente, cargada con importación y mostrando atributos asignados fuera de la clase:

>>> import listinstance


>>> clase C(listinstance.ListInstance): pasar

>>> x = C()
>>> xa, xb, xc = 1, 2, 3 >>>
imprimir(x)
<Instancia de C, dirección 43230824:
a=1 b=2 c=3

>

Además de la utilidad que brindan, los complementos optimizan el mantenimiento del código, como lo hacen todas las clases.
Por ejemplo, si luego decide extender __str__ de ListInstance para imprimir también todos los atributos de clase que hereda
una instancia, está seguro; debido a que es un método heredado, cambiar __str__ automáticamente actualiza la visualización
de cada subclase que importa la clase y la mezcla. Y dado que ahora es oficialmente "más tarde", pasemos a la siguiente
sección para ver cómo se vería esa extensión.

Listado de atributos heredados con dir

Tal como está, nuestro complemento ListerInstance muestra solo atributos de instancia (es decir, nombres adjuntos al objeto
de instancia en sí). Sin embargo, es trivial extender la clase para mostrar todos los atributos accesibles desde una instancia,
tanto los propios como los que hereda de sus clases. El truco es usar la función incorporada dir en lugar de escanear el
diccionario __dict__ de la instancia ; el último solo contiene atributos de instancia, pero el primero también recopila todos los
atributos heredados en Python 2.2 y versiones posteriores.

La siguiente mutación codifica este esquema; He codificado esto en su propio módulo para facilitar las pruebas simples,
pero si los clientes existentes usaran esta versión en su lugar, elegirían la nueva pantalla automáticamente (y recuerden del
Capítulo 25 que una cláusula as de importación puede cambiar el nombre de una nueva versión a se utiliza un nombre
anterior):

#!python
# Archivo listinherited.py (2.X + 3.X)

Lista de clase Heredada:


"""

Utilice dir() para recopilar atributos de instancia y nombres heredados de sus


clases; Python 3.X muestra más nombres que 2.X debido a la superclase de
objeto implícita en el modelo de clase de nuevo estilo; getattr()

Herencia Múltiple: Clases “Mix-in” | 963


Machine Translated by Google

obtiene los nombres heredados que no están en self.__dict__; use __str__, no


__repr__, ¡o de lo contrario esto se repite al imprimir métodos enlazados!
"""

def __attrnames(self):
''
resultado = for attr in
dir(self): if attr[:2] == '__' # Instancia dir()
and attr[-2:] == '__': result += '\t%s\n ' % atributo más: # Saltar internos

resultado += '\t%s=%s\n' % (attr, getattr(self, attr))


resultado devuelto

def __str__(self):
return '<Instancia de %s, dirección %s:\n%s>' % (
self.__class__.__name__, # El nombre de mi clase
id(self), self.__attrnames()) # Mi dirección
# lista nombre=valor

if __name__ == '__main__':
import testmixin
testmixin.tester(ListInherited)

Tenga en cuenta que este código omite los valores de los nombres __X__ ; la mayoría de estos son
nombres internos que generalmente no nos interesan en una lista genérica como esta. Esta versión
también debe usar la función incorporada getattr para obtener atributos por cadena de nombre en lugar
de usar la indexación del diccionario de atributos de la instancia; getattr emplea el protocolo de búsqueda
de herencia, y algunos de los nombres que enumeramos aquí no se almacenan en la instancia misma.

Para probar la nueva versión, ejecute su archivo directamente: pasa la clase que define a la función de prueba
del archivo mixin.py de prueba para que se use como complemento en una subclase. Sin embargo, esta salida de
esta clase de prueba y listado varía según la versión, porque los resultados de dir difieren. En Python 2.X,
obtenemos lo siguiente; observe el cambio de nombre en el trabajo en el nombre del método del listado (trunqué
algunas de las pantallas de valor completo para que quepan en esta página): c:\code> c:\python27\python

listinherited.py <Instance of Sub, address 35161352: _ListInherited__attrnames =<método enlazado


Sub.__attrnames de <prueba... más...>> __doc__ __init__ __module__ __str__ data1=spam data2=eggs
data3=42 ham=<método enlazado Sub.ham de <testmixin.Sub instancia en 0x00000.. .más...>>
spam=<método enlazado Sub.spam de <testmixin.Sub instancia en 0x00000...más...>>

>

En Python 3.X, se muestran más atributos porque todas las clases son de "nuevo estilo" y heredan los
nombres de la superclase de objeto implícita; más sobre esto en el Capítulo 32. Debido a que se heredan
tantos nombres de la superclase predeterminada, he omitido muchos aquí: hay 32 en total en 3.3. Ejecute
esto por su cuenta para obtener la lista completa:

964 | Capítulo 31: Diseño con clases


Machine Translated by Google

c:\code> c:\python33\python listinherited.py <Instancia


de Sub, dirección 43253152:
_ListInherited__attrnames=<método enlazado Sub.__attrnames de <prueba...más...>> __class__
__delattr__ __dict__ __dir__ __doc__ __eq__ .. .más nombres omitidos 32 en total...

__repr__
__setattr__
__sizeof__
__str__
__subclasshook__
__weakref__
data1=spam
data2=eggs
data3=42
ham=<método enlazado Sub.ham de <testmixin.tester.<locales>.Sub ...más...>>
spam=<método enlazado Sub. spam de <testmixin.tester.<locals>.Sub ...more...>>
>

Como una posible mejora para abordar la proliferación de nombres incorporados heredados y
valores largos aquí, la siguiente alternativa para ___attrnames en el archivo listinheri ted2.py
del paquete del ejemplo del libro agrupa los nombres de doble guión bajo por separado y
minimiza el ajuste de línea para atributos grandes valores; observe cómo escapa un % con %
% para que solo quede uno para la operación de formateo final al final:
def __attrnames(self, indent=' '*4): result =
'Unders%s\n%s%%s\nOthers%s\n' % ('-'*77, indent, '-'*77) unders = [] for attr en
dir(self): if attr[:2] == '__' and attr[-2:] == '__':
# Instancia dir()
# Saltar internos
unders.append(attr)
más:
display = str(getattr(self, attr))[:82-(len(indent) + len(attr))] result += '%s%s=%s\n'
% (indent, attr, display) return resultado % ', '.join(unders)

Con este cambio, la salida de prueba de la clase es un poco más sofisticada, pero también
más concisa y utilizable:

c:\code> c:\python27\python listinherited2.py <Instancia


de Sub, dirección 36299208: Unders---------------------------
-------------------------------------------------- __doc__, __init__, __módulo__, __str__

Otros------------------------------------------------- ----------------------------
_ListInherited__attrnames=<método enlazado Sub.__attrnames de <testmixin.Sub insta data1=spam
data2=eggs data3=42 ham=<método enlazado Sub.ham de <testmixin.Sub instancia en
0x000000000229E1C8>> spam=<método enlazado Sub.spam de <testmixin.Sub instancia en
0x000000000229E1C8>>

>

Herencia Múltiple: Clases “Mix-in” | 965


Machine Translated by Google

c:\code> c:\python33\python listinherited2.py


<Instancia de Sub, dirección 43318912:
Unders--------------------------- -------------------------------------------------- __clase__, __delattr__,
__dict__, __dir__, __doc__, __eq__, __formato__, __ge__,
__getattribute__, __gt__, __hash__, __init__, __le__, __lt__, __module__, __ne__, __new__,
__qualname__, __reduce__, __reduce_ex__, __repr__, __setattr__, __sizeof__, __str__,
__subclasshook__, __weakref__ Otros-------------- -------------------------------------------------- -------------
_ListInherited__attrnames=<método vinculado Sub.__attrnames de <testmixin.tester.<l
data1=spam data2=eggs data3=42 ham=<método vinculado Sub.ham de <testmixin.
tester.<locals>.Sub object at 0x0000000 spam=<bound method Sub.spam of
<testmixin.tester.<locals>.Sub object at 0x00000

>

El formato de visualización es un problema abierto (p. ej., el módulo pprint “impresora bonita” estándar
de Python puede ofrecer opciones aquí también), por lo que dejaremos el pulido adicional como un
ejercicio sugerido. La lista de árboles de la siguiente sección puede ser más útil en cualquier caso.

Bucle en __repr__: una precaución aquí: ahora que también estamos


mostrando en métodos heredados, tenemos que usar __str__ en lugar de
__repr__ para sobrecargar la impresión. Con __repr__, este código caerá
en bucles recursivos : mostrar el valor de un método activa el __repr__ de
la clase del método para mostrar la clase. Es decir, si el __repr__ del lister
intenta mostrar un método, mostrar la clase del método activará el __repr__
del lister nuevamente. ¡Sutil, pero cierto! Cambie __str__ a __repr__ aquí
para ver esto por sí mismo. Si debe usar __repr__ en dicho contexto,
puede evitar los bucles usando isinstance para comparar el tipo de valores
de atributo con los tipos . MethodType en la biblioteca estándar, para saber
qué elementos omitir.

Listado de atributos por objeto en

árboles de clases Codifiquemos una última extensión. Tal como está, nuestra última lista incluye
nombres heredados, pero no proporciona ningún tipo de designación de las clases de las que se adquieren los nombres.
Sin embargo, como vimos en el ejemplo de classtree.py cerca del final del Capítulo 29, es sencillo
escalar árboles de herencia de clases en el código. La siguiente clase mixta, codificada en el archivo
listtree.py, hace uso de esta misma técnica para mostrar los atributos agrupados por las clases en las
que viven: esboza el árbol de clases físico completo, mostrando los atributos adjuntos a cada objeto a lo
largo del camino. El lector aún debe inferir la herencia de atributos, pero esto brinda sustancialmente
más detalles que una simple lista plana:

#!python
# Archivo listtree.py (2.X + 3.X)

clase Árbol de lista:


"""

Mezcla que devuelve un rastro __str__ de todo el árbol de clases y todos

966 | Capítulo 31: Diseño con clases


Machine Translated by Google

los atributos de sus objetos en y por encima de sí mismo; ejecutado por print(), str()
devuelve una cadena construida; usa __X attr nombres para evitar impactar a los
clientes; recurre a las superclases explícitamente, usa str.format() para mayor claridad;
"""

def __attrnames(self,
' obj, indent): ' *
= resultado'' =(indent
for attr +in1) espacios
sorted(obj.__dict__): if
attr.startswith('__') and
attr.endswith('__'): resultado += espacios +
'{0}\n'.format(attr) else:

resultado += espacios + '{0}={1}\n'.format(attr, getattr(obj, attr))


resultado devuelto

def __listclass(self, aClass, sangría): puntos


= '.' * sangría si aClass en self.__visited:
return '\n{0}<Class {1}:, address {2}:
(ver arriba)>\n'.format(dots, aClass.__name__, id(aClass))

else:
self.__visited[aClass] = True
here = self.__attrnames(aClass,
'' indent)
above = for super in aClass.__bases__:
above += self.__listclass(super, indent+4)
return '\n{0}< Clase {1}, dirección {2}:\n{3}{4}
{5}>\n'.format(puntos, unaClase.__nombre__, id(unaClase), aquí,
arriba, puntos)

def __str__(self):
self.__visited = {} here
= self.__attrnames(self, 0) above =
self.__listclass(self.__class__, 4) return '<Instancia
de {0}, dirección {1}:\n {2}{3}>'.format( self.__class__.__name__,
id(self), aquí, arriba)

if __name__ == '__main__':
import testmixin
testmixin.tester(ListTree)
Esta clase logra su objetivo atravesando el árbol de herencia: desde la __clase__ de una instancia a su
clase, y luego desde las __bases__ de la clase a todas las superclases recursivamente, escaneando el
atributo __dict__ de cada objeto a lo largo del camino. En última instancia, concatena la cadena de cada
porción del árbol a medida que se desarrolla la recursividad.

Puede tomar un tiempo entender los programas recursivos como este, pero dada la forma arbitraria y la
profundidad de los árboles de clases, realmente no tenemos otra opción aquí (aparte de la pila explícita).

Herencia Múltiple: Clases “Mix-in” | 967


Machine Translated by Google

equivalentes de los tipos que encontramos en el Capítulo 19 y el Capítulo 25, que tienden a no ser más
simples, y que omitiremos aquí por espacio y tiempo). Esta clase está codificada para mantener su negocio lo
más explícito posible, sin embargo, para maximizar la claridad.

Por ejemplo, podría reemplazar la declaración de bucle del método __listclass en el primero de los siguientes
con la expresión del generador de ejecución implícita en el segundo, pero el segundo parece innecesariamente
complicado en este contexto (llamadas recursivas incrustadas en una expresión del generador) y no tiene un
rendimiento obvio . ventaja, especialmente dado el alcance limitado de este programa (ninguna de las
alternativas hace una lista temporal, aunque la primera puede crear resultados más temporales dependiendo
de la implementación interna de cadenas, concatenación y unión, algo que necesitaría cronometrar con las
herramientas del Capítulo 21 para determinar ):

arriba ''
= para super en aClass.__bases__:
arriba += self.__listclass(super, sangría+4)
...o...
arriba =
''.join( self.__listclass(super, indent+4) for super in aClass.__bases__)
También puede codificar la cláusula else en __listclass como la siguiente, como en la edición anterior de este
libro, una alternativa que incrusta todo en la lista de argumentos de formato ; se basa en el hecho de que la
llamada de unión inicia la expresión del generador y sus llamadas recursivas antes de que la operación de
formato comience a construir el texto de resultado; y parece más difícil de entender, a pesar de que yo lo
escribí (¡nunca es una buena señal!):

self.__visited[aClass] = True
genabove = (self.__listclass(c, indent+4) for c in aClass.__bases__) return
'\n{0}<Clase {1}, dirección {2}:\n{3 }{4}{5}>\n'.format(puntos,
unaClase.__nombre__, id(unaClase),
self.__attrnames(unaClase, sangría), # ¡Se ejecuta antes
del formato! ''.join(genaarriba), puntos)

Como siempre, lo explícito es mejor que lo implícito, y su código puede ser un factor tan importante como las
herramientas que utiliza.

Observe también cómo esta versión utiliza el método de formato de cadena Python 3.X y 2.6/2.7 en lugar de
expresiones de formato % , en un esfuerzo por hacer que las sustituciones sean posiblemente más claras;
cuando se aplican muchas sustituciones de esta manera, los números de argumento explícitos pueden hacer
que el código sea más fácil de descifrar. En resumen, en esta versión cambiamos la primera de las siguientes
líneas por la segunda:

return '<Instancia de %s, dirección %s:\n%s%s>' % (...) # Expresión


devuelve '<Instancia de {0}, dirección {1}:\n{2}{3}>'.format(...) # Método
Esta política también tiene un inconveniente desafortunado en 3.2 y 3.3, pero tenemos que ejecutar el código
para ver por qué.

968 | Capítulo 31: Diseño con clases


Machine Translated by Google

Ejecutando la lista de

árboles Ahora, para probar, ejecute el archivo de módulo de esta clase como antes; pasa la clase ListTree
a testmixin.py para que se mezcle con una subclase en la función de prueba. La salida de Tree-Sketcher
del archivo en Python 2.X es la siguiente:

c:\code> c:\python27\python listtree.py


<Instancia de Sub, dirección 36690632:
_ListTree__visited={} data1=spam data2=eggs
data3=42

....<Subclase, dirección 36652616:


__doc__ __init__ __module__
spam=<método no enlazado
Sub.spam>

........<Clase Super, dirección 36652712:


__doc__ __init__ __module__
ham=<método no enlazado
Super.ham>

........>

........<Árbol de lista de clases, dirección 30795816:


_ListTree__attrnames=<método no enlazado ListTree.__attrnames>
_ListTree__listclass=<método no enlazado ListTree.__listclass>
__doc__ __module__ __str__

........>
....>
>

Observe en esta salida cómo los métodos ahora están desvinculados en 2.X, porque los obtenemos
directamente de las clases . En la versión de la sección anterior, se mostraban como métodos enlazados ,
porque ListInherited los obtuvo de instancias con getattr en su lugar (la primera versión indexaba la instancia
__dict__ y no mostraba métodos heredados en las clases). También observe cómo la tabla __visited del
lister tiene su nombre alterado en el diccionario de atributos de la instancia; a menos que tengamos mucha
mala suerte, esto no chocará con otros datos allí.
Algunos de los métodos de la clase lister también están manipulados por pseudoprivacidad.

Bajo Python 3.X a continuación, nuevamente obtenemos atributos adicionales que pueden variar dentro de
la línea 3.X y superclases adicionales; como veremos en el próximo capítulo, todas las clases de nivel
superior heredan del objeto integrado. clase automáticamente en 3.X; Las clases de Python 2.X lo hacen
manualmente si desean un comportamiento de clase de nuevo estilo. También observe que los atributos
que eran métodos independientes en 2.X son funciones simples en 3.X, como se describió anteriormente
en este capítulo (y eso nuevamente, eliminé la mayoría de los atributos incorporados en el objeto para
ahorrar espacio aquí; ejecute esto por su cuenta para la lista completa):

Herencia Múltiple: Clases “Mix-in” | 969


Machine Translated by Google

c:\code> c:\python33\python listtree.py


<Instancia de Sub, dirección 44277488:
_ListTree__visited={} data1=spam data2=eggs
data3=42

....<Subclase, dirección 36990264:


__doc__ __init__ __module__
__qualname__ spam=<probador
de funciones.<locales>.Sub.spam
at 0x0000000002A3C840>

........<Class Super, dirección 36989352:


__dict__ __doc__ __init__
__module__ __qualname__
__weakref__ ham=<probador de
funciones.<locales>.Super.ham at
0x0000000002A3C730>

............<Objeto de clase, dirección 506770624:


__class__ __delattr__ __dir__ __doc__
__eq__ ...más omitidos: 22 en total...

__repr__
__setattr__
__tamaño
de__ __str__
__subclasshook__
............>
........>

........<Árbol de lista de clases, dirección 36988440:


_ListTree__attrnames=<función ListTree.__attrnames en 0x0000000002A3C158>
_ListTree__listclass=<función ListTree.__listclass en 0x0000000002A3C1E0> __dict__
__doc__ __module__ __qualname__ __str__ __weakref__

............<Objeto de clase:, dirección 506770624: (ver arriba)>


........>
....>
>

Esta versión evita enumerar el mismo objeto de clase dos veces al mantener una tabla de clases visitadas hasta
el momento (es por eso que se incluye la identificación de un objeto, para que sirva como clave para una anterior).

970 | Capítulo 31: Diseño con clases


Machine Translated by Google

elemento mostrado en el informe). Al igual que el recargador de módulos transitivos del Capítulo 25, un
diccionario funciona para evitar repeticiones en la salida porque los objetos de clase se pueden modificar y,
por lo tanto, pueden ser claves de diccionario; un conjunto proporcionaría una funcionalidad similar.

Técnicamente, los ciclos generalmente no son posibles en los árboles de herencia de clases: una clase ya
debe haber sido definida para ser nombrada como una superclase, y Python genera una excepción como
debería si intenta crear un ciclo más tarde mediante cambios de __bases__ , pero el mecanismo visitado aquí
evita volver a listar una clase dos veces:

>>> clase C: pasa


>>> clase B(C): pasa
>>> C.__bases__ = (B,) # ¡Magia oscura y profunda!
TypeError: un elemento __bases__ provoca un ciclo de herencia

Variación de uso: Mostrar valores de nombres de

guiones bajos Esta versión también tiene cuidado de evitar mostrar objetos internos grandes omitiendo los
nombres __X__ nuevamente. Si comenta el código que trata estos nombres de forma especial: for attr in
sorted(obj.__dict__): if attr.startswith('__') and attr.endswith('__'): result += espacios + '{0}
# \n'.format(attr) else: resultado += espacios + '{0}={1}\n'.format(attr, getattr(obj, attr))
#
#

entonces sus valores se mostrarán normalmente. Aquí está el resultado en 2.X con este cambio temporal
realizado, dando los valores de cada atributo en el árbol de clases:

c:\code> c:\python27\python listtree.py <Instancia


de Sub, dirección 35750408: _ListTree__visited={}
data1=spam data2=eggs data3=42

....<Subclase, dirección 36353608:


__doc__=Ninguno __init__=<Método
no enlazado Sub.__init__> __module__=testmixin
spam=<Método no enlazado Sub.spam>

........<Clase Super, dirección 36353704:


__doc__=Ninguno __init__=<método
no vinculado Super.__init__> __module__=testmixin
ham=<método no vinculado Super.ham>

........>

........<Árbol de lista de clases, dirección 31254568:


_ListTree__attrnames=<método no vinculado ListTree.__attrnames>
_ListTree__listclass=<método no vinculado ListTree.__listclass> __doc__=

Mezcla que devuelve un rastro __str__ de todo el árbol de clases y todos los atributos de
sus objetos en y por encima de sí mismo; ejecutado por print(), str() devuelve

Herencia Múltiple: Clases “Mix-in” | 971


Machine Translated by Google

cadena construida; usa __X attr nombres para evitar impactar a los clientes;
recurre a las superclases explícitamente, usa str.format() para mayor claridad;

__module__=__main__
__str__=<método no enlazado ListTree.__str__>
........>
....>
>

El resultado de esta prueba es mucho mayor en 3.X y puede justificar el aislamiento de los nombres de guiones bajos en
general, como hicimos anteriormente. De hecho, es posible que esta prueba ni siquiera funcione en algunas versiones
recientes de 3.X como está:

c:\código> c:\python33\python listtree.py


...etc...
Archivo "listtree.py", línea 18, en __attrnames
result += espacios + '{0}={1}\n'.format(attr, getattr(obj, attr))
TypeError: Type method_descriptor no define __format__
Debatí la recodificación para solucionar este problema, pero sirve como un buen ejemplo de requisitos y técnicas de
depuración en un proyecto dinámico de código abierto como Python. Según la siguiente nota, la llamada a str.format ya
no es compatible con ciertos tipos de objetos que son los valores de los nombres de atributos incorporados; otra razón
más por la que probablemente sea mejor omitir estos nombres.

Depuración de un problema de str.format: en 3.X, ejecutar la versión comentada


funciona en 3.0 y 3.1, pero parece haber un error, o al menos una regresión, aquí en
3.2 y 3.3: estos pitones fallan con un excepción porque cinco métodos incorporados
en object no definen un __format__ esperado por str.format, y el valor predeterminado
en object aparentemente ya no se aplica correctamente en tales casos con objetivos
de formato vacíos y genéricos. Para ver esto en vivo, basta con ejecutar un código
simplificado que aísle el problema:

c:\code> py ÿ3.1
>>> '{0}'.format(object.__reduce__)
"<método '__reduce__' de objetos 'objeto'>"
c:\code> py ÿ3.3 >>>
'{0 }'.formato(objeto.__reducir__)
TypeError: Type method_descriptor no define __format__

Según el comportamiento anterior y la documentación actual de Python, se supone


que los objetivos vacíos como este convierten el objeto en su cadena de impresión str
(consulte el PEP 3101 original y el manual de referencia del lenguaje 3.3).
Curiosamente, los objetivos de cadena {0} y {0:s} ahora fallan, pero el objetivo de
conversión de cadena forzada {0!s} funciona, al igual que la conversión previa de
cadena manual, lo que aparentemente refleja un cambio para un caso específico de
tipo que quizás descuidó Modos de uso genéricos más comunes:

c:\code> py ÿ3.3
>>> '{0:s}'.format(objeto.__reducir__)
TypeError: Type method_descriptor no define __format__ >>>
'{0!s}'.format(object.__reduce__) "<método '__reduce__' de
objetos 'objeto'>"

972 | Capítulo 31: Diseño con clases


Machine Translated by Google

>>> '{0}'.format(str(objeto.__reduce__))
"<método '__reduce__' de objetos 'objeto'>"

Para solucionarlo, envuelva la llamada de formato en una declaración de prueba para


capturar la excepción; use expresiones de formato % en lugar del método str.format ;
use uno de los modos de uso de str.format que aún funcionan antes mencionados y
espere que no cambie también; o espere una reparación de esto en una versión posterior de 3.X.
Aquí está la solución alternativa recomendada usando el % probado y verdadero
(también es notablemente más corto, pero no repetiré las comparaciones del Capítulo 7
aquí):

c:\code> py ÿ3.3
>>> '%s' % objeto.__reduce__
"<método '__reduce__' de objetos 'objeto'>"

Para aplicar esto en el código de la lista de árboles, cambie el primero de estos a su


seguidor:

resultado += espacios + '{0}={1}\n'.format(attr, getattr(obj, attr))


resultado += espacios + '%s=%s\n' % (attr, getattr(obj, atributo))

Python 2.X tiene la misma regresión en 2.7 pero no en 2.6 (heredada del cambio 3.2,
aparentemente) pero no muestra métodos de objetos en el ejemplo de este capítulo.
Dado que este ejemplo genera demasiada salida en 3.X de todos modos, es un punto
discutible aquí, pero es un ejemplo decente de codificación del mundo real.
Desafortunadamente, el uso de funciones más nuevas como str.format a veces coloca
su código en la posición incómoda de beta tester en la línea 3.X actual.

Variación de uso: ejecución en módulos

más grandes Para más diversión, elimine los comentarios de las líneas del controlador de subrayado para
habilitarlas nuevamente e intente mezclar esta clase en algo más sustancial, como la clase Button del
módulo del kit de herramientas GUI tkinter de Python. En general, querrá nombrar ListTree primero (más a
la izquierda) en un encabezado de clase , por lo que se recoge su __str__ ; El botón también tiene uno, y
la superclase más a la izquierda siempre se busca primero en la herencia múltiple.

El resultado de lo siguiente es bastante masivo (20 000 caracteres y 330 líneas en 3.X, ¡y 38 000 si olvida
quitar el comentario de la detección del guión bajo!), así que ejecute este código por su cuenta para ver la
lista completa. Observe cómo el atributo de diccionario __visited de nuestro lister se mezcla inofensivamente
con los creados por el propio tkinter . Si está usando Python 2.X, también recuerde que debe usar Tkinter
para el nombre del módulo en lugar de tkinter:
>>> from listtree import ListTree >>> from
tkinter import Button >>> class MyButton(ListTree, # Ambas clases tienen un __str__
Button): pasar # ListTree primero: use su __str__

>>> B = MyButton(text='spam') >>>


open('savetree.txt', 'w').write(str(B)) 20513 >>> # Guardar en un archivo para verlo más tarde
len(open('savetree.txt') .readlines()) 330
# Líneas en el archivo

>>> imprimir(B) # Imprima la pantalla aquí


<Instancia de MyButton, dirección 43363688:

Herencia Múltiple: Clases “Mix-in” | 973


Machine Translated by Google

_ListTree__visited={}
_name=43363688
_tclCommands=[] _w=.43363688
children={}

maestro=. ...mucho más omitido...


>
>>> S = cadena(B) # O imprima solo la primera parte
>>> imprimir(S[:1000])

Experimente arbitrariamente por su cuenta. El punto principal aquí es que OOP tiene que ver con la reutilización de código, y las
clases mixtas son un ejemplo poderoso. Como casi todo lo demás en programación, la herencia múltiple puede ser un dispositivo
útil cuando se aplica bien. En la práctica, sin embargo, es una característica avanzada y puede complicarse si se usa sin cuidado
o en exceso. Volveremos a tratar este tema como un problema al final del próximo capítulo.

Módulo colector
Por último, para facilitar aún más la importación de nuestras herramientas, podemos proporcionar un módulo recopilador que las
combina en un solo espacio de nombres: importar solo lo siguiente da acceso a los tres complementos de lister a la vez:

# File lister.py # Recopila


los tres listers en un módulo para mayor comodidad

from listinstance import ListInstance from listinherited


import ListInherited from listtree import ListTree

Lister = ListTree # Elija un listado predeterminado

Los importadores pueden usar los nombres de clases individuales tal cual, o crearles un alias con un nombre común usado en subclases

que se pueden modificar en la declaración de importación: >>> import lister >>> lister.ListInstance <class 'listinstance.ListInstance'> >>

> lister.Lister <clase 'listtree.ListTree'>


# Usar una lista específica

# Usar el valor predeterminado de Lister

>>> from lister import Lister >>> Lister # Usar el valor predeterminado de Lister

<clase 'listtree.ListTree'>

>>> from lister import ListInstance as Lister # Use Lister alias >>> Lister <class
'listinstance.ListInstance'>

Python a menudo hace que las API de herramientas flexibles sean casi automáticas.

974 | Capítulo 31: Diseño con clases


Machine Translated by Google

Espacio para mejorar: MRO,

tragamonedas, GUI Como la mayoría del software, hay mucho más que podríamos hacer aquí. A
continuación, se brindan algunos consejos sobre las extensiones que quizás desee explorar. Algunos son
proyectos interesantes, y dos sirven como transición al próximo capítulo, pero por espacio tendrá que
permanecer en la categoría de ejercicios sugeridos aquí.

Ideas generales: GUI, integrados


Agrupar los nombres de doble guión bajo como hicimos anteriormente puede ayudar a reducir el tamaño de
la visualización del árbol, aunque algunos como __init__ son definidos por el usuario y pueden merecer un
tratamiento especial. Dibujar el árbol en una GUI también podría ser un próximo paso natural: el kit de
herramientas tkinter que utilizamos en los ejemplos de la lista de la sección anterior se envía con Python y
proporciona soporte básico pero fácil, y otros ofrecen alternativas más ricas pero más complejas. Consulte
las notas al final del estudio de caso del Capítulo 28 para obtener más indicaciones en este departamento.

Árboles físicos versus herencia: usando el MRO (versión preliminar)


En el próximo capítulo, también conoceremos el modelo de clase de nuevo estilo, que modifica el orden de
búsqueda para un caso especial de herencia múltiple (rombos). Allí, también estudiaremos el atributo de
objeto de clase de nuevo estilo class.__mro__ : una tupla que proporciona el orden de búsqueda del árbol
de clases utilizado por herencia, conocido como MRO de nuevo estilo.

Tal como está, nuestro listado de árboles ListTree esboza la forma física del árbol de herencia y espera
que el espectador deduzca de esto de dónde se hereda un atributo. Este era su objetivo, pero un visor de
objetos general también podría usar la tupla MRO para asociar automáticamente un atributo con la clase
de la que se hereda, escaneando el MRO de nuevo estilo (o el orden DFLR de las clases clásicas) para
cada uno. atributo heredado en un resultado de directorio , podemos simular la búsqueda de herencia de
Python y asignar atributos a sus objetos de origen en el árbol de clase físico que se muestra.

De hecho, escribiremos un código que se acerque mucho a esta idea en el módulo mapattrs del próximo
capítulo , y reutilizaremos las clases de prueba de este ejemplo para demostrar la idea, así que permanezca
atento al epílogo de esta historia. Esto podría usarse en lugar de o además de mostrar las ubicaciones
físicas de los atributos en __attrnames aquí; ambas formas pueden ser datos útiles para que los
programadores los vean. Este enfoque también es una forma de tratar con las máquinas tragamonedas, el
tema de la siguiente nota.

Datos virtuales: ranuras, propiedades y más (versión preliminar)


Debido a que analizan los diccionarios de espacio de nombres __dict__ de instancia, las clases ListInstance
y ListTree presentadas aquí plantean algunos problemas de diseño sutiles. En las clases de Python, es
posible que algunos nombres asociados con los datos de la instancia no se almacenen en la instancia misma.
Esto incluye temas presentados en el siguiente capítulo, como propiedades, espacios y descriptores de
nuevo estilo, pero también atributos calculados dinámicamente en todas las clases con herramientas como
__getattr__. Ninguno de estos nombres de atributos "virtuales" se almacena en el diccionario de espacio de
nombres de una instancia, por lo que ninguno se mostrará como parte de los propios datos de una instancia.

Herencia Múltiple: Clases “Mix-in” | 975


Machine Translated by Google

De estos, las tragamonedas parecen las más fuertemente asociadas con una instancia; almacenan datos
en instancias, aunque sus nombres no aparecen en los diccionarios de espacio de nombres de instancia.
Las propiedades y los descriptores también están asociados con las instancias, pero no reservan espacio
en la instancia, su naturaleza computada es mucho más explícita y pueden parecer más cercanos a los
métodos de nivel de clase que a los datos de la instancia.

Como veremos en el próximo capítulo, los espacios funcionan como atributos de instancia, pero son
creados y administrados por elementos creados automáticamente en las clases. Son una opción de
clase de nuevo estilo que se usa con relativa poca frecuencia, donde los atributos de instancia se
declaran en un atributo de clase __slots__ y no se almacenan físicamente en el __dict__ de una
instancia; de hecho, las tragamonedas pueden suprimir un __dict__ por completo. Debido a esto, las
herramientas que muestran las instancias mediante el escaneo de sus espacios de nombres no asociarán
directamente la instancia con los atributos almacenados en las ranuras. Tal como está, ListTree muestra
las ranuras como atributos de clase dondequiera que aparezcan (aunque no en la instancia), y
ListInstance no las muestra en absoluto.

Aunque esto tendrá más sentido después de que estudiemos esta característica en el próximo capítulo,
afecta el código aquí y herramientas similares. Por ejemplo, si en textmixin.py asignamos
__slots__=['data1'] en Super y __slots__=['data3'] en Sub, estas dos clases de listado solo muestran el
atributo data2 en la instancia. ListTree también muestra data1 y data3 , pero como atributos de los
objetos de clase Super y Sub y con un formato especial para sus valores (técnicamente, son descriptores
de nivel de clase, otra herramienta de estilo nuevo que se presenta en el próximo capítulo).

Como se explicará en el próximo capítulo, para mostrar los atributos de ranura como nombres de
instancia, las herramientas generalmente necesitan usar dir para obtener una lista de todos los atributos,
tanto presentes físicamente como heredados, y luego usar getattr para obtener sus valores de la
instancia, o buscar valores de su fuente de herencia a través de __dict__ en escaneos de árbol y aceptar
la visualización de las implementaciones de algunas clases at. Debido a que dir incluye los nombres de
los atributos "virtuales" heredados, incluidos los espacios y las propiedades, se incluirían en el conjunto
de instancias. Como también encontraremos, el MRO podría ayudar aquí a asignar el atributo dir a sus
fuentes, o restringir las visualizaciones de instancias a nombres codificados en clases definidas por el
usuario al filtrar los nombres heredados del objeto integrado.

ListInherited es inmune a la mayor parte de esto, porque ya muestra el conjunto completo de resultados
de directorios , que incluye los nombres de __dict__ y los nombres de __ranuras__ de todas las clases ,
aunque su visualización es de uso marginal tal cual. Una variante de ListTree que usa la técnica dir junto
con la secuencia MRO para asignar atributos a las clases también se aplicaría a las ranuras, porque los
nombres basados en ranuras aparecen en los resultados __dict__ de la clase individualmente como
herramientas de administración de ranuras, aunque no en la instancia __dict__.

Alternativamente, como política, podríamos simplemente dejar que nuestro código maneje los atributos
basados en ranuras como lo hace actualmente, en lugar de complicarlo para una característica avanzada
que rara vez se usa y que es incluso una práctica cuestionable en la actualidad. Las ranuras y los
atributos de instancia normal son diferentes tipos de nombres. De hecho, mostrar los nombres de las
ranuras como atributos de clases en lugar de instancias es técnicamente más preciso; como veremos en
el próximo capítulo, su implementación es en las clases, aunque su espacio es en las instancias.

976 | Capítulo 31: Diseño con clases


Machine Translated by Google

En última instancia, intentar recopilar todos los atributos "virtuales" asociados con una clase puede ser un sueño
imposible de todos modos. Las técnicas como las descritas aquí pueden abordar las ranuras y las propiedades,
pero algunos atributos son completamente dinámicos, sin ninguna base física: los que se calculan al obtenerlos
mediante un método genérico como __get attr__ no son datos en el sentido clásico. Las herramientas que intentan
mostrar datos en un lenguaje sumamente dinámico, Python, deben venir con la advertencia de que, en el mejor de
los casos, algunos datos son etéreos.

También haremos una extensión menor al código de esta sección en los ejercicios al final de esta parte del libro, para

enumerar los nombres de las superclases entre paréntesis al comienzo de las pantallas de instancias, así que manténgalo
archivado para referencia futura por ahora. Para comprender mejor el último de los dos puntos anteriores, debemos
concluir este capítulo y pasar al siguiente y último en esta parte del libro.

Otros temas relacionados con el diseño

En este capítulo, hemos estudiado la herencia, la composición, la delegación, la herencia múltiple, los métodos
enlazados y las fábricas: todos los patrones comunes que se usan para combinar clases en los programas de Python.
Sin embargo, en realidad solo hemos arañado la superficie aquí en el dominio de los patrones de diseño. En otras partes
de este libro encontrará cobertura de otros temas relacionados con el diseño, tales como:

• Superclases abstractas (Capítulo 29) • Decoradores

(Capítulo 32 y Capítulo 39) • Subclases de tipo (Capítulo 32)

• Métodos estáticos y de clase (Capítulo 32) • Atributos

administrados (Capítulo 32 y Capítulo 38) • Metaclases

(Capítulo 32 y Capítulo 40 ) )

Sin embargo, para obtener más detalles sobre los patrones de diseño, delegaremos a otros recursos sobre programación
orientada a objetos en general. Aunque los patrones son importantes en el trabajo de programación orientada a objetos
y, a menudo, son más naturales en Python que en otros lenguajes, no son específicos de Python en sí y son un tema que
a menudo se adquiere mejor con la experiencia.

Resumen del capítulo

En este capítulo, probamos formas comunes de usar y combinar clases para optimizar su reutilización y los beneficios de
la factorización, lo que generalmente se considera problemas de diseño que a menudo son independientes de cualquier
lenguaje de programación en particular (aunque Python puede facilitar su implementación). Estudiamos delegación
(envolver objetos en clases proxy), composición (controlar objetos incrustados) y herencia (adquirir comportamiento de
otras clases), así como algunos conceptos más esotéricos como atributos pseudoprivados, herencia múltiple, métodos
enlazados y fábricas.

Resumen del capítulo | 977


Machine Translated by Google

El siguiente capítulo finaliza nuestra mirada a las clases y la programación orientada a objetos examinando temas
más avanzados relacionados con las clases. Parte de su material puede ser de más interés para los creadores de
herramientas que para los programadores de aplicaciones, pero aun así merece una revisión por parte de la mayoría
de las personas que harán OOP en Python; si no es por su código, es posible que deba entender el código de otros .
Primero, sin embargo, aquí hay otra prueba rápida de capítulo para revisar.

Pon a prueba tus conocimientos: Cuestionario

1. ¿Qué es la herencia múltiple?

2. ¿Qué es la delegación?

3. ¿Qué es la composición?
4. ¿Qué son los métodos enlazados?

5. ¿Para qué se utilizan los atributos pseudoprivados?

Pon a prueba tus conocimientos: respuestas

1. La herencia múltiple ocurre cuando una clase hereda de más de una superclase; es útil para mezclar varios
paquetes de código basado en clases. El orden de izquierda a derecha en los encabezados de declaraciones
de clase determina el orden general de las búsquedas de atributos.

2. La delegación implica envolver un objeto en una clase de proxy, lo que agrega un comportamiento adicional y
pasa otras operaciones al objeto envuelto. El proxy conserva la interfaz del objeto envuelto.

3. La composición es una técnica mediante la cual una clase de controlador incrusta y dirige una serie de objetos
y proporciona una interfaz propia; es una forma de construir estructuras más grandes con clases.

4. Los métodos vinculados combinan una instancia y una función de método; puede llamarlos sin pasar un objeto
de instancia explícitamente porque la instancia original todavía está disponible.

5. Los atributos pseudoprivados (cuyos nombres comienzan pero no terminan con dos guiones bajos iniciales:
__X) se utilizan para localizar nombres en la clase envolvente. Esto incluye tanto atributos de clase como
métodos definidos dentro de la clase y atributos de autoinstancia asignados dentro de los métodos de la clase.
Dichos nombres se expanden para incluir el nombre de la clase, lo que los hace generalmente únicos.

978 | Capítulo 31: Diseño con clases


Machine Translated by Google

CAPÍTULO 32

Temas de clase avanzada

Este capítulo concluye nuestra mirada a la programación orientada a objetos en Python presentando algunos
temas más avanzados relacionados con las clases: estudiaremos la creación de subclases de tipos
incorporados, cambios y extensiones de clase de "nuevo estilo", métodos estáticos y de clase, ranuras y
propiedades, funciones y clases. dec orators, el MRO y el super call, y más.

Como hemos visto, el modelo OOP de Python es, en esencia, relativamente simple, y algunos de los temas
presentados en este capítulo son tan avanzados y opcionales que es posible que no los encuentre muy a
menudo en su carrera de programación de aplicaciones de Python. Sin embargo, en aras de la exhaustividad,
y porque nunca se sabe cuándo puede surgir un tema "avanzado" en el código que usa, completaremos
nuestra discusión de las clases con una breve mirada a estas herramientas avanzadas para el trabajo de POO.

Como de costumbre, debido a que este es el último capítulo de esta parte del libro, termina con una sección
sobre "errores" relacionados con la clase y el conjunto de ejercicios de laboratorio para esta parte. Lo animo a
trabajar con los ejercicios para ayudar a consolidar las ideas que hemos estudiado aquí. También sugiero
trabajar o estudiar proyectos más grandes de OOP Python como complemento de este libro. Como ocurre con
gran parte de la informática, los beneficios de la programación orientada a objetos tienden a ser más evidentes
con la práctica.

Notas de contenido: este capítulo recopila temas de clase avanzados, pero algunos son
demasiado extensos para que este capítulo los cubra bien. Temas tales como
propiedades, descriptores, decoradores y metaclases se mencionan brevemente aquí y
se les da un tratamiento más completo en la parte final de este libro, después de algunas
excepciones. Asegúrese de buscar ejemplos más completos y una cobertura ampliada
de algunos de los temas que entran en la categoría de este capítulo.

También notará que este es el capítulo más grande de este libro; supongo que los
lectores lo suficientemente valientes como para abordar los temas de este capítulo están
listos para arremangarse y explorar su cobertura en profundidad. Si no está buscando
temas avanzados de programación orientada a objetos, es posible que desee saltar a
los materiales del final del capítulo y volver aquí cuando confronte estas herramientas
en el código de su futuro de programación.

979
Machine Translated by Google

Ampliación de tipos integrados


Además de implementar nuevos tipos de objetos, las clases a veces se usan para extender el
funcionalidad de los tipos integrados de Python para admitir estructuras de datos más exóticas. Para
Por ejemplo, para agregar métodos de inserción y eliminación de colas a las listas, puede codificar clases que envuelven
(incrustar) un objeto de lista y exportar métodos de inserción y eliminación que procesan la lista especialmente,
como la técnica de delegación que estudiamos en el Capítulo 31. A partir de Python 2.2, también puede
use la herencia para especializar tipos incorporados. Las siguientes dos secciones muestran ambas técnicas.
en acción.

Ampliación de tipos mediante incrustación

¿Recuerda esas funciones establecidas que escribimos en el Capítulo 16 y el Capítulo 18? Aquí está
cómo se ven devueltos a la vida como una clase de Python. El siguiente ejemplo (el
setwrapper.py ) implementa un nuevo tipo de objeto de conjunto moviendo algunas de las funciones de conjunto a
métodos y agregando una sobrecarga básica de operadores. En su mayor parte, esto
La clase simplemente envuelve una lista de Python con operaciones de conjunto adicionales. Pero debido a que es una clase, también

admite múltiples instancias y personalización por herencia en subclases. A diferencia de nuestro


funciones anteriores, el uso de clases aquí nos permite crear múltiples objetos de conjunto autónomos con datos y
comportamiento preestablecidos, en lugar de pasar listas a funciones manualmente:
conjunto de clases:

def __init__(self, valor = []): # Constructor


self.datos = [] # Administra una lista
self.concat(valor)

def intersect(self, other): res = [] for x in # otro es cualquier secuencia


self.data: # uno mismo es el sujeto

si x en otro: # Elija elementos comunes


res.append(x)
conjunto de retorno (res) # Devolver un nuevo Conjunto

def union(self, other): res = # otro es cualquier secuencia


self.data[:] for x in other: if not # Copia de mi lista
x in res: # Agregar elementos en otros

res.append(x)
conjunto de retorno (res)

def concat(self, value): for x in value: # valor: lista, Conjunto...


if not x in self.data: # Elimina duplicados

self.data.append(x)

def __len__(self): return len(self.data) def __getitem__(self, key): return # len(uno mismo), si uno mismo

self.data[key] def __and__(self, other): return self.intersect(other) def # uno mismo[i], uno mismo[i:j]

__or__(self, other) : return self.union(otro) # uno mismo y otros


# auto | otro

980 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

def __repr__(uno mismo): return 'Establecer:' + repr(self.data) # print(self),...


def __iter__(uno mismo): return iter(self.data) # for x in self,...

Para usar esta clase, creamos instancias, llamamos a métodos y ejecutamos operadores definidos como de costumbre:

desde setwrapper import Set


x = Conjunto ([1, 3, 5, 7])
print(x.unión(Conjunto([1, 4, 7]))) print(x | # impresiones Conjunto:[1, 3, 5, 7, 4]
Conjunto([1, 4, 6])) # impresiones Conjunto:[1, 3, 5, 7, 4, 6]

La sobrecarga de operaciones como la indexación y la iteración también permite instancias de nuestro


Configure la clase para que a menudo se haga pasar por listas reales. Porque interactuarás y extenderás
esta clase en un ejercicio al final de este capítulo, no diré mucho más sobre esto
código hasta el Apéndice D.

Ampliación de tipos mediante subclases

A partir de Python 2.2, todos los tipos incorporados en el lenguaje ahora se pueden clasificar en subclases.
directamente. Las funciones de conversión de tipo como list, str, dict y tuple se han vuelto
nombres de tipo incorporados, aunque transparentes para su secuencia de comandos, una llamada de conversión de tipo (por ejemplo,

list('spam')) ahora es realmente una invocación del constructor de objetos de un tipo.

Este cambio le permite personalizar o ampliar el comportamiento de los tipos incorporados con declaraciones de clase
definidas por el usuario : simplemente subclasifique los nuevos nombres de tipo para personalizarlos. Las instancias
de sus subclases de tipo generalmente se pueden usar en cualquier lugar donde pueda aparecer el tipo integrado
original. Por ejemplo, suponga que tiene problemas para acostumbrarse al hecho de que
Las compensaciones de la lista de Python comienzan en 0 en lugar de 1. No se preocupe, siempre puede codificar su
propia subclase que personaliza este comportamiento central de las listas. El archivo typesubclass.py muestra
cómo:

# Tipo/ clase de lista incorporada de subclase


# Asignar 1..N a 0..N-1; volver a llamar a la versión integrada.

clase MiLista(lista):
def __getitem__(auto, compensación):
print('(indexando %s en %s)' % (self, offset))
devolver lista.__getitem__(self, offset - 1)

si __nombre__ == '__principal__':
imprimir (lista ('abc'))
x = MiLista('abc') print(x) # __init__ heredado de la lista
# __repr__ heredado de la lista

imprimir(x[1]) # MiLista.__getitem__
imprimir(x[3]) # Personaliza el método de superclase de lista

x.append('correo no deseado'); imprimir # Atributos de la superclase de lista


(x) x.reverse (); imprimir (x)

En este archivo, la subclase MyList amplía el método de indexación __getitem__ de la lista integrada.
solo, para mapear los índices 1 a N de regreso al requerido 0 a Nÿ1. Todo lo que realmente hace es disminuir

Ampliación de tipos integrados | 981


Machine Translated by Google

el índice enviado y volver a llamar a la versión de indexación de la superclase, pero es


suficiente para hacer el truco:

% python typesubclass.py
['a B C']
['a B C']
(indexando ['a', 'b', 'c'] en 1)
a
(indexando ['a', 'b', 'c'] en 3)
C

['a', 'b', 'c', 'correo no deseado']


['correo no deseado', 'c', 'b', 'a']

Esta salida también incluye el texto de seguimiento que la clase imprime en la indexación. Por supuesto, ya sea
cambiar la indexación de esta manera es una buena idea en general es otro problema: los usuarios de su
La clase MyList puede muy bien confundirse por una salida tan central de la secuencia de Python
¡comportamiento! La capacidad de personalizar los tipos integrados de esta manera puede ser un activo poderoso,
aunque.

Por ejemplo, este patrón de codificación da lugar a una forma alternativa de codificar un conjunto, como un
subclase del tipo de lista incorporado, en lugar de una clase independiente que administra un objeto de lista incrustado como
se muestra en la sección anterior. Como aprendimos en el Capítulo 5, Python
hoy viene con un poderoso objeto de conjunto incorporado, junto con literal y comprensión
sintaxis para hacer nuevos conjuntos. Sin embargo, codificar uno usted mismo sigue siendo una excelente manera de aprender
sobre la subclasificación de tipos en general.

La siguiente clase, codificada en el archivo setsubclass.py, personaliza listas para agregar solo métodos
y operadores relacionados con el procesamiento de conjuntos. Debido a que todos los demás comportamientos se heredan del
superclase de lista incorporada , esto lo convierte en una alternativa más corta y simple: todo
no definido aquí se enruta a la lista directamente:

from __future__ import print_function # 2.X compatibilidad

conjunto de clases (lista):


def __init__(self, valor = []): lista.__init__([]) # Constructor
self.concat(valor) # Personaliza la lista
# Copia valores predeterminados mutables

def intersect(self, other): res = [] for # otro es cualquier secuencia


x in self: # uno mismo es el sujeto

si x en otro: # Elija elementos comunes


res.append(x)
conjunto de retorno (res) # Devolver un nuevo Conjunto

def union(uno mismo, otro): # otro es cualquier secuencia


res = Set(uno mismo) # Cópiame a mí y a mi lista
res.concat(otro)
volver res

def concat(self, value): for x in # valor: lista, Conjunto, etc.


value: if not x in self: # Elimina duplicados

982 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

self.append(x)

def __y__(uno mismo, otro): return self.intersect(otro) def __or__(self,


otro): return self.union(otro) def __repr__(self): return 'Set:' +
list.__repr__(self)

if __name__ == '__principal__':
x = Conjunto([1,3,5,7]) y =
Conjunto([2,1,4,5,6]) print(x,
y, len(x)) print (x.intersect(y),
y.union(x)) print(x & y, x | y) x.reverse();
imprimir (x)

Aquí está el resultado del código de autodiagnóstico al final de este archivo. Debido a que la creación de
subclases de tipos principales es una función algo avanzada con un público objetivo limitado, omitiré más
detalles aquí, pero lo invito a rastrear estos resultados en el código para estudiar su comportamiento (que es
el mismo en Python 3.X y Python 3.X). 2.X):

% python setsubclass.py
Conjunto:[1, 3, 5, 7] Conjunto:[2, 1, 4, 5, 6] 4
Conjunto:[1, 5] Conjunto:[2, 1, 4, 5, 6, 3, 7]
Establecer:[1, 5] Establecer:[1, 3, 5, 7, 2, 4, 6]
Conjunto:[7, 5, 3, 1]

Hay formas más eficientes de implementar conjuntos con diccionarios en Python, que reemplazan los escaneos
de búsqueda lineal anidados en las implementaciones de conjuntos que se muestran aquí con operaciones de
índice de diccionario más directas (hashing) y, por lo tanto, se ejecutan mucho más rápido. Para obtener más
detalles, consulte la continuación de este hilo en el libro de seguimiento Programación de Python. Nuevamente,
si está interesado en conjuntos, también eche otro vistazo al tipo de objeto conjunto que exploramos en el
Capítulo 5; este tipo proporciona amplias operaciones de conjuntos como herramientas integradas. Es divertido
experimentar con las implementaciones de conjuntos, pero hoy en día ya no son estrictamente necesarias en
Python.

Para ver otro ejemplo de subclases de tipos, explore la implementación del tipo bool en Python 2.3 y versiones
posteriores. Como se mencionó anteriormente en el libro, bool es una subclase de int con dos instancias
(Verdadero y Falso) que se comportan como los números enteros 1 y 0 , pero heredan métodos personalizados
de representación de cadenas que muestran sus nombres.

El modelo de clase del “nuevo estilo”


En la versión 2.2, Python introdujo un nuevo tipo de clases, conocidas como clases de nuevo estilo ; las clases
que seguían el modelo original y tradicional se conocieron como clases clásicas en comparación con el nuevo
tipo. En 3.X, la historia de la clase se fusionó, pero permanece dividida para los usuarios y el código de Python
2.X:

• En Python 3.X, todas las clases son automáticamente lo que antes se denominaba "nuevo estilo", ya sea
que hereden explícitamente del objeto o no. La codificación de la superclase de objetos es opcional e
implícita.

El modelo de clase del “nuevo estilo” | 983


Machine Translated by Google

• En Python 2.X, las clases deben heredar explícitamente del objeto (u otro tipo integrado) para ser consideradas
"nuevo estilo" y habilitar y obtener todo el comportamiento del nuevo estilo. Las clases sin esto son "clásicas".

Debido a que todas las clases son automáticamente de estilo nuevo en 3.X, las características de las clases de
estilo nuevo son simplemente características de clase normales en esa línea. Sin embargo, he optado por
mantener sus descripciones en esta sección por separado, en deferencia a los usuarios del código Python 2.X:
las clases en dicho código adquieren características y comportamiento de nuevo estilo solo cuando se derivan de un objeto.

En otras palabras, cuando los usuarios de Python 3.X vean descripciones de temas de "nuevo estilo" en este
libro, deben tomarlos como descripciones de propiedades existentes de sus clases.
Para los lectores 2.X, estos son un conjunto de cambios y extensiones opcionales que puede optar por habilitar o
no, a menos que el código que debe usar ya los emplee.

En Python 2.X, la diferencia sintáctica que identifica las clases de estilo nuevo es que se derivan de un tipo
integrado, como una lista, o de una clase integrada especial conocida como objeto. El objeto de nombre
incorporado se proporciona para servir como una superclase para las clases de nuevo estilo si no es apropiado
usar otro tipo incorporado: clase nuevo estilo (objeto):

# 2.X derivación explícita de nuevo estilo


...código de clase normal... # No se requiere en 3.X: automático

Cualquier clase derivada de objeto, o cualquier otro tipo incorporado, se trata automáticamente como una clase
de nuevo estilo. Es decir, siempre que un tipo incorporado esté en algún lugar de su árbol de superclase, una
clase 2.X adquiere un comportamiento de clase y extensiones de nuevo estilo. Las clases que no se derivan de
funciones integradas, como object , se consideran clásicas.

¿Qué tan nuevo es New-Style?


Como veremos, las clases de nuevo estilo vienen con profundas diferencias que afectan ampliamente a los
programas, especialmente cuando el código aprovecha sus características avanzadas adicionales. De hecho, al
menos en términos de su soporte OOP, estos cambios en algunos niveles transforman a Python en un lenguaje
completamente diferente, uno que es obligatorio en la línea 3.X, uno que es opcional en 2.X solo si es ignorado
por todos los programadores, y uno que toma prestado mucho más de (y a menudo es tan complejo como) otros
idiomas en este dominio.

Las clases de nuevo estilo surgen en parte de un intento de fusionar la noción de clase con la de tipo en la época
de Python 2.2, aunque pasaron desapercibidas para muchos hasta que se escalaron a conocimiento requerido
en 3.X. Tendrá que juzgar el éxito de esa fusión por sí mismo, pero como veremos, todavía hay distinciones en el
modelo, ahora entre clase y metaclase, y uno de sus efectos secundarios es hacer que las clases normales sean
más poderosas pero también sustancialmente más complejo. El algoritmo de herencia de nuevo estilo formalizado

en el Capítulo 40, por ejemplo, crece en complejidad por lo menos en un factor de 2.

Aún así, algunos programadores que usan código de aplicación sencillo pueden notar solo una ligera divergencia
de las clases "clásicas" tradicionales. Después de todo, hemos logrado llegar a este punto en este libro escribiendo
ejemplos de clase sustanciales, en su mayoría solo menciones pasajeras.

984 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

de este cambio. Además, el modelo de clase clásica todavía disponible en 2.X funciona exactamente como lo
ha hecho durante unas dos décadas.1

Sin embargo, debido a que modifican los comportamientos de las clases principales, las clases de estilo
nuevo tuvieron que introducirse en Python 2.X como una herramienta distinta para evitar afectar cualquier
código existente que dependa del modelo anterior. Por ejemplo, algunas diferencias sutiles, como la búsqueda
de herencia de patrones de diamantes y la interacción de las operaciones integradas y los métodos de
atributos administrados como __getattr__ , pueden hacer que algún código existente falle si no se cambia. El
uso de extensiones opcionales en el nuevo modelo, como las ranuras, puede tener el mismo efecto.

La división del modelo de clase se elimina en Python 3.X, que exige clases de nuevo estilo, pero aún existe
para los lectores que usan 2.X o reutilizan la gran cantidad de código 2.X existente en uso de producción.
Debido a que esta ha sido una extensión opcional en 2.X, el código escrito para esa línea puede usar cualquier
modelo de clase.

Las siguientes dos secciones de nivel superior brindan información general sobre las formas en que difieren
las clases de nuevo estilo y las nuevas herramientas que brindan. Estos temas representan cambios
potenciales para algunos lectores de Python 2.X, pero simplemente temas de clases avanzadas adicionales
para muchos lectores de Python 3.X. Si está en el último grupo, aquí encontrará una cobertura completa,
aunque parte de ella se presenta en el contexto de los cambios, que puede aceptar como características, pero
solo si nunca debe lidiar con cualquiera de los millones de líneas. del código 2.X existente.

Cambios de clase de nuevo estilo


Las clases de nuevo estilo difieren de las clases clásicas en varios aspectos, algunos de los cuales son sutiles
pero pueden afectar tanto al código 2.X existente como a los estilos de codificación comunes. Como vista
previa y resumen, estas son algunas de las formas más destacadas en que se diferencian:

Obtención de atributos para incorporados: instancia


omitida Los métodos de interceptación de atributos genéricos __getattr__ y __getattribute__ aún se
ejecutan para los atributos a los que se accede mediante un nombre explícito, pero ya no para los
atributos obtenidos implícitamente mediante operaciones integradas. No se llaman para el operador
__X__ sobre la carga de nombres de métodos solo en contextos integrados: la búsqueda de dichos
nombres comienza en las clases, no en las instancias. Esto rompe o complica los objetos que sirven
como proxy para la interfaz de otro objeto, si los objetos envueltos implementan la sobrecarga de operadores.

1. Como punto de datos, el libro Programación Python, una continuación de programación de aplicaciones de 1600 páginas de este libro
que usa 3.X exclusivamente, no usa ni necesita acomodar ninguna de las herramientas de clase de nuevo estilo de este capítulo, y
todavía se las arregla para construir programas significativos para GUI, sitios web, programación de sistemas, bases de datos y
texto. En su mayoría, es un código sencillo que aprovecha los tipos y bibliotecas integrados para hacer su trabajo, no extensiones
OOP oscuras y esotéricas. Cuando utiliza clases, son relativamente simples y proporcionan estructura y factorización de código. El
código de ese libro también es probablemente más representativo de la programación del mundo real que algunos de los textos de
este tutorial de lenguaje, lo que sugiere que muchas de las herramientas avanzadas de programación orientada a objetos de Python
pueden ser artificiales y tener más que ver con el diseño del lenguaje que con los objetivos prácticos del programa. Por otra parte,
ese libro tiene el lujo de restringir su conjunto de herramientas a dicho código; tan pronto como su compañero de trabajo encuentre
una manera de usar una función de lenguaje arcano, ¡todas las apuestas están canceladas!

Cambios de clase de nuevo estilo | 985


Machine Translated by Google

Dichos métodos deben redefinirse en aras de la distribución integrada diferente en las nuevas clases de estilo.

Clases y tipos combinados: pruebas de tipos


Las clases ahora son tipos y los tipos ahora son clases. De hecho, los dos son esencialmente sinónimos, aunque
las metaclases que ahora incluyen tipos todavía son algo distintas de las clases normales. El tipo (I) integrado
devuelve la clase de la que está hecha una instancia, en lugar de un tipo de instancia genérico, y normalmente
es lo mismo que I.__class__.
Además, las clases son instancias de la clase de tipo , y el tipo puede dividirse en subclases para personalizar
la creación de clases con metaclases codificadas con declaraciones de clase . Esto puede afectar el código que

prueba los tipos o que se basa en el modelo de tipo anterior.

Clase raíz de objeto automática : valores predeterminados

Todas las clases de nuevo estilo (y, por lo tanto, los tipos) heredan de object, que viene con un pequeño conjunto
de métodos de sobrecarga de operadores predeterminados (por ejemplo, __repr__). En 3.X, esta clase se
agrega automáticamente encima de las clases raíz definidas por el usuario (es decir, las más altas ) en un árbol,
y no es necesario que se incluya explícitamente como una superclase. Esto puede afectar el código que asume
la ausencia de métodos predeterminados y clases raíz.

Orden de búsqueda de herencia: MRO y diamantes Los

patrones de diamantes de herencia múltiple tienen un orden de búsqueda levemente diferente: aproximadamente,
en los diamantes se buscan primero en ancho que en profundidad. Este orden de búsqueda de atributos,
conocido como MRO, se puede rastrear con un nuevo atributo __mro__ disponible en clases de nuevo estilo. El
nuevo orden de búsqueda se aplica en gran medida solo a los árboles de clases de diamantes, aunque la raíz
del objeto implícito del nuevo modelo forma un diamante en todos los árboles de herencia múltiple. El código que
se basa en el pedido anterior no funcionará igual.

Algoritmo de herencia: Capítulo 40 El algoritmo


utilizado para la herencia en las clases de nuevo estilo es sustancialmente más complejo que el modelo de
profundidad primero de las clases clásicas, incorporando casos especiales para descriptores, metaclases e
incorporados. No podremos formalizar esto hasta el Capítulo 40 después de que hayamos estudiado las
metaclases y los descriptores con mayor profundidad, pero puede afectar el código que no anticipa sus
circunvoluciones adicionales.

Nuevas herramientas avanzadas: impactos


en el código Las clases de estilo nuevo tienen un conjunto de herramientas de clase nuevas, que incluyen
ranuras, propiedades, descriptores, super y el método __getattribute__ . La mayoría de estos tienen propósitos
de construcción de herramientas muy específicos. Sin embargo, su uso también puede afectar o romper el
código existente; las ranuras, por ejemplo, a veces impiden por completo la creación de un diccionario de espacio
de nombres de instancia, y los controladores de atributos genéricos pueden requerir una codificación diferente.

Exploraremos las extensiones anotadas en el último de estos elementos en una sección propia posterior de nivel
superior, y aplazaremos la cobertura del algoritmo de herencia formal hasta el Capítulo 40 , como se indica. Sin
embargo, debido a que los otros elementos en esta lista tienen el potencial de romper el código Python tradicional,
echemos un vistazo más de cerca a cada uno aquí.

986 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

Nota de contenido: tenga en cuenta que los cambios de clase de nuevo estilo se aplican tanto a 3.X como
a 2.X, aunque son una opción en este último. Este capítulo y este libro a veces etiquetan características
como cambios 3.X para contrastar con el código 2.X tradicional, pero algunas son técnicamente
introducidas por clases de nuevo estilo, que son obligatorias en 3.X, pero pueden aparecer en el código
2.X. también.
Para el espacio, esta distinción se menciona a menudo pero no dogmáticamente aquí.
Para complicar esta distinción, algunos cambios relacionados con la clase 3.X se deben a las clases de
nuevo estilo (p. ej., omitir __getattr__ para los métodos de operador), pero otros no (p. ej., reemplazar los
métodos independientes con funciones). Además, muchos programadores de 2.X se apegan a las clases
clásicas, ignorando lo que ven como una característica de 3.X. Sin embargo, las clases de nuevo estilo
no son nuevas y se aplican a ambos Python; si aparecen en el código 2.X, también son de lectura
obligatoria para los usuarios de 2.X.

Obtención de atributos para instancias de saltos integradas

Presentamos este cambio de clase de nuevo estilo en las barras laterales tanto en el Capítulo 28 como en
el Capítulo 31 debido a su impacto en ejemplos y temas anteriores. En las clases de estilo nuevo (y, por lo
tanto, en todas las clases de 3.X), los métodos de interceptación de atributos de instancia genérica __get
attr__ y __getattribute__ ya no son llamados por operaciones integradas para los nombres de métodos de
sobrecarga del operador __X__; la búsqueda de dichos nombres comienza en clases, no instancias. Sin
embargo, los atributos a los que se accede mediante un nombre explícito se enrutan a través de estos
métodos, incluso si son nombres __X__ . Por lo tanto, esto es principalmente un cambio en el comportamiento
de las operaciones integradas.

Más formalmente, si una clase define un método de sobrecarga de índice __getitem__ y X es una instancia
de esta clase, entonces una expresión de índice como X[I] es más o menos equivalente a X.__geti tem__(I)
para las clases clásicas, pero type(X ).__getitem__(X, I) para las clases de nuevo estilo; este último comienza
su búsqueda en la clase y, por lo tanto, salta un paso __getattr__ de la instancia para un nombre indefinido.

Técnicamente, este método de búsqueda de operaciones integradas como X[I] utiliza la herencia normal a
partir del nivel de clase e inspecciona solo los diccionarios de espacio de nombres de todas las clases de
las que deriva X , una distinción que puede ser importante en el modelo de metaclase que utilizamos. Nos
encontraremos más adelante en este capítulo y nos centraremos en el Capítulo 40, donde las clases pueden
adquirir un comportamiento diferente. Sin embargo, la instancia se omite en la búsqueda integrada.

¿Por qué el cambio de búsqueda?

Puede encontrar razones formales para este cambio en otros lugares; este libro no se inclina a repetir como
loros las justificaciones de un cambio que rompe muchos programas de trabajo. Pero esto se imagina tanto
como una ruta de optimización como una solución a un problema de patrón de llamada aparentemente
oscuro . La primera razón está respaldada por la frecuencia de las operaciones integradas. Si cada +, por
ejemplo, requiere pasos adicionales en la instancia, puede degradar la velocidad del programa, especialmente
dadas las muchas extensiones de nivel de atributo del modelo de nuevo estilo.

Cambios de clase de nuevo estilo | 987


Machine Translated by Google

La última razón es más oscura y se describe en los manuales de Python; en resumen, refleja un enigma introducido
por el modelo de metaclase . Debido a que las clases ahora están en instancias de metaclases, y debido a que las
metaclases pueden definir métodos de operador incorporados para procesar las clases que generan, la ejecución de
una llamada de método para una clase debe omitir la clase en sí y buscar un nivel superior para seleccionar un método
que procesa la clase, en lugar de seleccionar la propia versión de la clase. Su propia versión daría como resultado una
llamada de método independiente, porque el propio método de la clase procesa instancias inferiores. Este es solo el
modelo de método no vinculado habitual que discutimos en el capítulo anterior, pero es potencialmente agravado por
el hecho de que las clases también pueden adquirir comportamiento de tipo de metaclases.

Como resultado, debido a que las clases son tanto tipos como instancias por derecho propio, todas las instancias se
omiten para la búsqueda del método de operación integrado. Supuestamente, esto se aplica a las instancias normales
por uniformidad y consistencia, pero tanto los nombres no integrados como las llamadas directas y explícitas a los
nombres integrados aún verifican la instancia de todos modos. Aunque tal vez sea una consecuencia del modelo de
clase de nuevo estilo, para algunos esto puede parecer una solución a la que se llegó en aras de un patrón de uso que
era más artificial y oscuro que el ampliamente utilizado que rompió. Su papel como ruta de optimización parece más
defendible, pero también tiene repercusiones.

En particular, esto tiene implicaciones potencialmente amplias para las clases basadas en delegación , a menudo
conocidas como clases proxy , cuando los objetos incrustados implementan la sobrecarga de operadores. En las
clases de nuevo estilo, la clase de un objeto proxy generalmente debe redefinir dichos nombres para capturar y
delegar, ya sea manualmente o con herramientas. El efecto neto es complicar significativamente o obviar por completo
toda una categoría de programas. Exploramos la delegación en el Capítulo 28 y el Capítulo 31; es un patrón común
que se usa para aumentar o adaptar la interfaz de otra clase, para agregar validación, seguimiento, sincronización y
muchos otros tipos de lógica. Aunque los proxies pueden ser más la excepción que la regla en el código típico de
Python, muchos programas de Python dependen de ellos.

Implicaciones para la intercepción de

atributos En términos simples, y se ejecuta en Python 2.X para mostrar cómo difieren las clases de nuevo estilo, la
indexación y las impresiones se enrutan a __getattr__ en las clases tradicionales, pero no para las clases de nuevo
estilo, donde la impresión usa un valor predeterminado:2

>>> class C:
data = 'spam' def
__getattr__(self, nombre): print(name) # Clásico en 2.X: captura incorporados
return getattr(self.data, name)

>>> X = C()
>>> X[0]
__getitem__

2. A partir de las listas de interacciones de este capítulo, comencé a omitir algunas líneas en blanco y acortar algunas
direcciones hexadecimales a 32 bits en las pantallas de objetos, para reducir el tamaño y el desorden. Voy a suponer que,
a esta altura del libro, encontrarás esos pequeños detalles irrelevantes.

988 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

's'
>>> imprimir(X) # Classic no hereda por defecto
__str__ spam

>>> clase C(objeto): ...resto # Nuevo estilo en 2.X y 3.X


de la clase sin cambios...

>>> X = C() # Incorporaciones no enrutadas a getattr


>>> X[0]
TypeError: el objeto 'C' no admite la indexación >>> print(X)
<__main__.C object at 0x02205780>

Aunque aparentemente racionalizado en el nombre de los métodos de metaclase de clase y la optimización


de las operaciones integradas, esta divergencia no es abordada por instancias normales de mayúsculas y
minúsculas que tienen un __getattr__, y se aplica solo a operaciones integradas, no a métodos con nombres
normales o llamadas explícitas. a los métodos integrados por nombre:

>>> clase C: pasar >>> # 2.X clase clásica


X = C()
>>> X.normal = lambda: 99 >>>
X.normal() 99 >>> X.__add__ =
lambda(y): 88 + y >>> X.__add__(1)
89 >>> X + 1 89

>>> clase C(objeto): pase >>> X = # 2.X/ 3.X clase de estilo nuevo
C()
>>> X.normal = lambda: 99 >>>
X.normal() 99 # Normales todavía de la instancia

>>> X.__sumar__ = lambda(y): 88 + y >>>


X.__sumar__(1) 89 >>> X + 1 # Lo mismo para nombres incorporados explícitos

TypeError: tipos de operandos no admitidos para +: 'C' e 'int'

Este comportamiento termina siendo heredado por el método de interceptación de atributos __getattr__ :

>>> clase C(objeto):


def __getattr__(yo, nombre): print(nombre)

>>> X = C()
>>> X.normal # Los nombres normales todavía se enrutan a getattr
normal >>>
X.__add__ # Las llamadas directas por nombre también lo son, ¡pero las expresiones no!
__add__ >>> X + 1
TypeError: tipos de
operandos no admitidos para +: 'C' e 'int'

Cambios de clase de nuevo estilo | 989


Machine Translated by Google

Requisitos de codificación de proxy

En un escenario de delegación más realista, esto significa que las operaciones integradas, como las expresiones, ya no funcionan

igual que su equivalente tradicional de llamada directa. De forma asimétrica, las llamadas directas a nombres de métodos integrados
siguen funcionando, pero las expresiones equivalentes sí.
no porque las llamadas de tipo directo fallan para los nombres que no están en el nivel de clase y superior. En otra
En otras palabras, esta distinción surge solo en operaciones incorporadas; las recuperaciones explícitas se ejecutan correctamente:

>>> clase C(objeto):


datos = 'correo no deseado'

def __getattr__(yo, nombre):


imprimir('getattr: ' + nombre)
devuelve getattr(self.data, nombre)

>>> X = C()
>>> X.__getitem__(1) # El mapeo tradicional funciona, pero el nuevo estilo no.
getattr: __getitem__
'pags'

>>> X[1]
TypeError: el objeto 'C' no admite la indexación
>>> tipo(X).__getitem__(X, 1)
AttributeError: el tipo de objeto 'C' no tiene el atributo '__getitem__'

>>> X.__add__('huevos') # Lo mismo para +: instancia omitida solo para expresión


getattr: __add__
'spameggs'

>>> X + 'huevos'
TypeError: tipos de operandos no admitidos para +: 'C' y 'str'
>>> tipo(X).__add__(X, 'huevos')
AttributeError: el tipo de objeto 'C' no tiene el atributo '__add__'

El efecto neto: codificar un proxy de un objeto cuya interfaz puede ser invocada en parte por

operaciones incorporadas, las clases de nuevo estilo requieren tanto __getattr__ para nombres normales, como
así como redefiniciones de métodos para todos los nombres a los que se accede mediante operaciones integradas, ya sea
codificado manualmente, obtenido de superclases o generado por herramientas. Cuando las redefiniciones

están así incorporados, las llamadas a través de ambas instancias y tipos son equivalentes a las integradas
operaciones, aunque los nombres redefinidos ya no se enrutan al __getattr__ genérico
controlador de nombres indefinido, incluso para llamadas de nombres explícitos:

>>> clase C(objeto): datos # Nuevo estilo: 3.X y 2.X


= 'correo no deseado'
def __getattr__(self, nombre): # Captura nombres normales
print('getattr: ' + nombre)
devuelve getattr(self.data, nombre)
def __getitem__(self, i): # Redefinir incorporados
print('getitem: ' + str(i))
return self.data[i] def # Ejecutar expr o getattr
__add__(self, otro):
imprimir('agregar: ' + otro)
devuelve getattr(self.data, '__add__')(otro)

>>> X = C()

990 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

>>> X.upper
getattr: superior
<método integrado superior del objeto str en 0x0233D670> >>> X.upper() getattr:
superior 'SPAM'

>>> X[1] # Operación incorporada (implícita)


obtener
elemento: 1 'p'
>>> X.__getitem__(1) getitem: # Equivalencia tradicional (explícita)
1 'p' >>> tipo(X).__getitem__(X,
1) getitem: 1 'p'
# Equivalencia de nuevo estilo

>>> X + 'huevos' # Lo mismo para + y otros


agregar: huevos
'spameggs'
>>> X.__add__('huevos')
agregar: huevos 'spameggs' >>>
tipo(X).__add__(X, 'huevos')
agregar: huevos 'spameggs'

Para más detalles

Revisaremos este cambio en el Capítulo 40 sobre metaclases, y por ejemplo en los contextos de administración
de atributos en el Capítulo 38 y decoradores de privacidad en el Capítulo 39. En el último de estos, también
exploraremos estructuras de codificación para proporcionar proxies con el requerido métodos de operador de
forma genérica: no es una tarea imposible y es posible que deba codificarse solo una vez si se hace bien. Para
obtener más información sobre el tipo de código influenciado por este problema, consulte los capítulos
posteriores, así como los ejemplos anteriores en el Capítulo 28 y el Capítulo 31.

Debido a que ampliaremos este tema más adelante en el libro, acortaremos la cobertura aquí.
Sin embargo, para obtener enlaces externos y sugerencias sobre este tema, consulte lo siguiente (junto con su
motor de búsqueda local):

• Problema de Python 643841: este problema se ha discutido ampliamente, pero su historia más oficial
parece estar documentada en http:// bugs.python.org/ issue643841. Allí, se planteó como una preocupación
para los programas reales y se intensificó para que se abordara, pero se anuló un remedio de biblioteca
propuesto o un cambio más amplio en Python a favor de un simple cambio de documentación para
describir el nuevo comportamiento obligatorio. • Recetas de herramientas: consulte también http://

code.activestate.com/ recipes/ 252151, una receta de Active State Python que describe una herramienta que
completa automáticamente nombres de métodos especiales como despachadores de llamadas genéricos
en una clase de proxy creada con técnicas de metaclase introducidas más adelante en este capítulo. Esta
herramienta todavía debe pedirle que pase el operador

Cambios de clase de nuevo estilo | 991


Machine Translated by Google

Sin embargo, los nombres de métodos que un objeto envuelto puede implementar (debe hacerlo, ya
que los componentes de la interfaz de un objeto envuelto pueden heredarse de fuentes arbitrarias). •

Otros enfoques: una búsqueda en la web hoy descubrirá numerosas herramientas adicionales que, de
manera similar, llenan las clases de proxy con métodos de sobrecarga; ¡Es una preocupación
generalizada! Nuevamente, en el Capítulo 39, también veremos cómo codificar superclases sencillas
y generales una vez que proporcionen los métodos o atributos requeridos como complementos, sin
metaclases, generación de código redundante o técnicas complejas similares.

Esta historia puede evolucionar con el tiempo, por supuesto, pero ha sido un problema durante muchos
años. Tal como está hoy, los proxies de clase clásicos para objetos que sobrecargan a cualquier operador
se rompen efectivamente como clases de nuevo estilo. Tales clases en 2.X y 3.X requieren codificar o
generar envoltorios para todos los métodos de operador invocados implícitamente que un objeto envuelto
puede soportar. Esto no es ideal para este tipo de programas (algunos proxies pueden requerir docenas de
métodos de envoltura (¡potencialmente más de 50!)—pero refleja, o al menos es un artefacto de, los
objetivos de diseño de los desarrolladores de clases de nuevo estilo.

Asegúrese de ver la cobertura de la metaclase del Capítulo 40 para obtener una ilustración
adicional de este problema y su justificación. También veremos allí que este comportamiento
de los incorporados califica como un caso especial en la herencia de nuevo estilo.
Entender esto bien requiere más antecedentes sobre las metaclases de los que puede
proporcionar el capítulo actual, un subproducto lamentable de las metaclases en general:
se han convertido en un requisito previo para un uso mayor del que sus creadores pueden
haber previsto.

Cambios en el modelo de

tipo Pasemos a nuestro próximo cambio de estilo nuevo: dependiendo de su evaluación, en las clases de
estilo nuevo, la distinción entre tipo y clase se ha silenciado en gran medida o se ha desvanecido por
completo. Específicamente: Las clases son tipos El objeto de tipo genera clases como sus instancias, y las

clases generan instancias de sí mismas. Ambos se consideran tipos, porque generan instancias. De hecho,
no existe una diferencia real entre los tipos integrados, como listas y cadenas, y los tipos definidos
por el usuario codificados como clases. Esta es la razón por la que podemos crear subclases de tipos
integrados, como se mostró anteriormente en este capítulo: una subclase de un tipo integrado, como
una lista , califica como una nueva clase de estilo y se convierte en un nuevo tipo definido por el
usuario.

Los tipos son clases


Los nuevos tipos generadores de clases se pueden codificar en Python como las metaclases que
veremos más adelante en este capítulo: subclases de tipo definidas por el usuario que se codifican
con declaraciones de clase normales y controlan la creación de las clases que son sus instancias.
Como veremos, las metaclases son tanto clase como tipo, aunque son lo suficientemente distintas
como para sustentar un argumento razonable de que la anterior dicotomía tipo/clase se ha convertido
en metaclase/clase, quizás a costa de una mayor complejidad en las clases normales.

992 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

Además de permitirnos crear subclases de tipos incorporados y codificar metaclases, una de las más
contextos prácticos donde esta fusión de tipo/clase se vuelve más obvia es cuando hacemos
pruebas de tipo explícito. Con las clases clásicas de Python 2.X, el tipo de una instancia de clase es un
“instancia” genérica, pero los tipos de objetos integrados son más específicos:

C:\código> c:\python27\python
>>> clase C: pasar # Clases clásicas en 2.X

>>> I = C() # Las instancias están hechas de clases


>>> tipo(I), I.__clase__
(<tipo 'instancia'>, <clase __main__.C en 0x02399768>)

>>> tipo(C) # Pero las clases no son lo mismo que los tipos
<tipo 'objclase'>
>>> C.__clase__
AttributeError: la clase C no tiene el atributo '__class__'

>>> tipo([1, 2, 3]), [1, 2, 3].__clase__


(<escriba 'lista'>, <escriba 'lista'>)

>>> tipo(lista), lista.__clase__


(<tipo 'tipo'>, <tipo 'tipo'>)

Pero con las clases de estilo nuevo en 2.X, el tipo de una instancia de clase es la clase que se crea
from, ya que las clases son simplemente tipos definidos por el usuario: el tipo de una instancia es su clase,
y el tipo de una clase definida por el usuario es el mismo que el tipo de un tipo de objeto integrado.
Las clases también tienen un atributo __class__ ahora, porque son instancias de tipo:

C:\código> c:\python27\python
>>> clase C(objeto): pasar # Clases de nuevo estilo en 2.X

>>> I = C() # El tipo de instancia es la clase de la que está hecho


>>> tipo(I), I.__clase__
(<clase '__principal__.C'>, <clase '__principal__.C'>)

>>> tipo(C), C.__clase__ # Las clases son tipos definidos por el usuario
(<tipo 'tipo'>, <tipo 'tipo'>)

Lo mismo es cierto para todas las clases en Python 3.X, ya que todas las clases tienen automáticamente
un estilo nuevo, incluso si no tienen superclases explícitas. De hecho, la distinción entre tipos incorporados
y tipos de clase definidos por el usuario parece desaparecer por completo en 3.X:

C:\código> c:\python33\python
>>> clase C: pasar

>>> I = C() # Todas las clases son de nuevo estilo en 3.X


>>> tipo(I), I.__clase__ (<clase # El tipo de instancia es la clase de la que está hecho
'__principal__.C'>, <clase '__principal__.C'>)

>>> tipo(C), C.__clase__ # La clase es un tipo y el tipo es una clase


(<clase 'tipo'>, <clase 'tipo'>)

>>> tipo([1, 2, 3]), [1, 2, 3].__clase__


(<clase 'lista'>, <clase 'lista'>)

Cambios de clase de nuevo estilo | 993


Machine Translated by Google

>>> tipo(lista), lista.__clase__ (<clase # Las clases y los tipos integrados funcionan igual
'tipo'>, <clase 'tipo'>)

Como puede ver, en 3.X las clases son tipos, pero los tipos también son clases. Técnicamente, cada clase es
generada por una metaclase, una clase que normalmente es de tipo o una subclase personalizada para
aumentar o administrar las clases generadas. Además de afectar el código que realiza pruebas de tipo, esto
resulta ser un gancho importante para los desarrolladores de herramientas. Hablaremos más sobre las
metaclases más adelante en este capítulo, y nuevamente con más detalle en el Capítulo 40.

Implicaciones para las

pruebas de tipos Además de permitir la personalización de tipos integrada y los enlaces de metaclases, la
combinación de clases y tipos en el modelo de clases de nuevo estilo puede afectar el código que realiza pruebas de tipos.
En Python 3.X, por ejemplo, los tipos de instancias de clase se comparan directa y significativamente, y de la misma manera que
los objetos de tipo incorporados. Esto se deriva del hecho de que las clases ahora son tipos, y el tipo de una instancia es la clase
de la instancia: C:\code> c:\python33\python >>> class C: pass >>> class D: pass

>>> c, d = C(), D() >>>


tipo(c) == tipo(d) # 3.X: compara las clases de las instancias
Falso

>>> tipo(c), tipo(d) (<clase


'__principal__.C'>, <clase '__principal__.D'>) >>> c.__clase__,
d.__clase__ (<clase '__principal__.C'> , <clase '__principal__.D'>)

>>> c1, c2 = C(), C() >>>


tipo(c1) == tipo(c2)
Verdadero

Sin embargo, con las clases clásicas en 2.X, comparar tipos de instancias es casi inútil, porque todas las instancias tienen el
mismo tipo de "instancia". Para comparar realmente los tipos, se deben comparar los atributos de la instancia __class__ (si le
importa la portabilidad, esto también funciona en 3.X, pero no es obligatorio allí): C:\code> c:\python27\python >>> class C: pasa
>>> clase D: pasa

>>> c, d = C(), D() >>>


tipo(c) == tipo(d) # 2.X: ¡todas las instancias son del mismo tipo!

Verdadero >>> c.__clase__ == d.__clase__ # Comparar clases explícitamente si es necesario


Falso

>>> tipo(c), tipo(d) (<tipo


'instancia'>, <tipo 'instancia'>) >>> c.__clase__,
d.__clase__ (<clase __principal__.C en 0x024585A0>,
<clase __principal__ .D en 0x024588D0>)

994 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

Y como debería esperar ahora, las clases de estilo nuevo en 2.X funcionan igual que todas las clases en 3.X en este sentido: la
comparación de tipos de instancias compara las clases de instancias automáticamente: C:\code> c:\python27 \python >>>
clase C(objeto): pase >>> clase D(objeto): pase

>>> c, d = C(), D() >>>


tipo(c) == tipo(d) # 2.X nuevo estilo: igual que todo en 3.X
Falso

>>> tipo(c), tipo(d) (<clase


'__principal__.C'>, <clase '__principal__.D'>) >>> c.__clase__,
d.__clase__ (<clase '__principal__.C'> , <clase
'__principal__.D'>)

Por supuesto, como he señalado varias veces en este libro, la verificación de tipos suele ser algo
incorrecto en los programas de Python (codificamos para interfaces de objetos, no para tipos de
objetos), y es más probable que la instancia integrada más general sea lo que querrá usar en los casos
excepcionales en los que se deben consultar los tipos de clase de instancia. Sin embargo, el
conocimiento del modelo de tipo de Python puede ayudar a aclarar el modelo de clase en general.

Todas las clases se derivan de "objeto"

Otra ramificación del cambio de tipo en el modelo de clase de nuevo estilo es que debido a que todas las clases se derivan
(heredan) del objeto de clase, ya sea implícita o explícitamente, y debido a que todos los tipos ahora son clases, cada objeto se
deriva de la clase incorporada del objeto , ya sea directamente o a través de una superclase. Considere la siguiente interacción
en Python 3.X: >>> class C: pass # For new-style classes >>> X = C() >>> type(X), type(C)

# El tipo es una instancia de clase creada a


partir de (<clase '__main__.C'>, <clase 'tipo'>)

Como antes, el tipo de una instancia de clase es la clase de la que se creó, y el tipo de una clase es la
clase de tipo porque las clases y los tipos se han fusionado. Sin embargo, también es cierto que la
instancia y la clase se derivan de la clase y el tipo de objeto incorporados , una superclase implícita o
explícita de cada clase: >>> isinstance(X, object)

Verdadero

>>> esinstancia(C, objeto) # Las clases siempre heredan del objeto


Verdadero

Lo anterior devuelve los mismos resultados para las clases de estilo nuevo y clásico en 2.X hoy, aunque
los resultados de tipo 2.X difieren. Más importante aún, como veremos más adelante, el objeto no se
agrega ni está presente en la tupla __bases__ de una clase clásica 2.X , por lo que no es una verdadera
superclase.

Cambios de clase de nuevo estilo | 995


Machine Translated by Google

La misma relación se aplica a los tipos integrados como listas y cadenas, porque los tipos son clases en
el modelo de nuevo estilo: los tipos integrados ahora son clases y sus instancias también se derivan de
objetos :
>>> tipo('correo no deseado'),
tipo(cadena) (<clase 'cadena'>, <clase 'tipo'>)

>>> isinstance('spam', objeto) # Lo mismo para tipos integrados (clases)


Verdadero

>>> esinstancia(cadena, objeto)


Verdadero

De hecho, el tipo en sí se deriva del objeto y el objeto se deriva del tipo, aunque los dos son objetos
diferentes: una relación circular que limita el modelo de objetos y se deriva del hecho de que los tipos
son clases que generan clases:
>>> tipo(tipo) <clase # Todas las clases son tipos y viceversa
'tipo'> >>>
tipo(objeto) <clase
'tipo'>

>>> isinstance(tipo, objeto) # Todas las clases se derivan del objeto, incluso del tipo
Verdadero

>>> isinstance(objeto, tipo) # Los tipos forman clases, y el tipo es una clase

Verdadero >>> tipo es objeto


Falso

Implicaciones para los

incumplimientos Lo anterior puede parecer oscuro, pero este modelo tiene una serie de implicaciones
prácticas. Por un lado, significa que a veces debemos ser conscientes de los valores predeterminados
del método que vienen con la clase raíz de objeto explícito o implícito solo en las clases de nuevo estilo:
c:\code> py ÿ2 >>>
dir(objeto) ['__class__',
'__delattr__', '__doc__', '__format__', '__getattribute__', '__hash__' , '__init__', '__new__', '__reduce__ ', '__reduce_ex__',
'__repr__', '__setattr__', ' __sizeof__', '__str__', '__subclasshook__']

>>> clase C: pasar >>>


C.__bases__ () # Las clases clásicas no heredan del objeto

>>> X = C()
>>> X.__repr__
AttributeError: la instancia C no tiene el atributo '__repr__'

>>> clase C(objeto): pasar >>> # Las clases de nuevo estilo heredan los valores predeterminados de los objetos

C.__bases__ (<tipo 'objeto'>,)

>>> X = C()
>>> X.__repr__
<envoltura de método '__repr__' del objeto C en 0x00000000020B5978>

c:\código> py ÿ3

996 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

>>> clase C: pase # Esto significa que todas las clases obtienen valores predeterminados en 3.X

>>> C.__bases__
(<clase 'objeto'>,)
>>> C().__repr__
<método-envoltura '__repr__' del objeto C en 0x0000000002955630>

Este modelo también genera menos casos especiales que la distinción tipo/clase anterior de las clases
clásicas, y nos permite escribir código que puede asumir y usar de manera segura una superclase de objeto
(por ejemplo, asumiéndola como un "ancla" en algunas superclases ) . -en los roles descritos más adelante
y pasándole llamadas a métodos para invocar el comportamiento predeterminado). Veremos ejemplos de
esto último más adelante en el libro; por ahora, pasemos a explorar el último gran cambio de estilo nuevo.

Cambio de herencia de diamantes

Nuestro último cambio en el modelo de clase de nuevo estilo es también uno de los más visibles: su orden
de búsqueda de herencia ligeramente diferente para los llamados árboles de herencia múltiple con patrón de
diamantes , un patrón de árbol en el que más de una superclase conduce a la misma superclase superior
más arriba (y cuyo nombre proviene de la forma de diamante que tiene el árbol si lo esbozas, un cuadrado
apoyado en una de sus esquinas).

El patrón de diamante es un concepto de diseño bastante avanzado, solo ocurre en múltiples árboles de
herencia y tiende a codificarse rara vez en la práctica de Python, por lo que no cubriremos este tema en
profundidad. Sin embargo, en resumen, los diferentes órdenes de búsqueda se introdujeron brevemente en
la cobertura de herencia múltiple del capítulo anterior: Para clases clásicas (la predeterminada en 2.X): DFLR

La ruta de búsqueda de herencia es estrictamente primero en profundidad y luego de izquierda a derecha:


Python sube todo el camino hasta la cima, abrazando el lado izquierdo del árbol, antes de que retroceda
y comience a mirar más hacia la derecha. Este orden de búsqueda se conoce como DFLR por las
primeras letras en las direcciones de su ruta.

Para las clases de estilo nuevo (opcional en 2.X y automática en 3.X): MRO La
ruta de búsqueda de herencia es más amplia en los casos de diamantes: Python primero busca en
cualquier superclase a la derecha de la que acaba de buscar antes de ascender a la superclase común
en la parte superior. En otras palabras, esta búsqueda avanza por niveles antes de ascender. Este
orden de búsqueda se denomina MRO de nuevo estilo para "orden de resolución de métodos" (y, a
menudo, solo MRO para abreviar cuando se usa en contraste con el orden DFLR). A pesar del nombre,
esto se usa para todos los atributos en Python, no solo para los métodos.

El algoritmo MRO de nuevo estilo es un poco más complejo de lo que se acaba de describir, y lo ampliaremos
un poco más formalmente más adelante, pero esto es todo lo que muchos programadores necesitan saber.
Aún así, tiene beneficios importantes para el código de clase de nuevo estilo, así como un potencial de
ruptura de programas para el código de clase clásico existente.

Por ejemplo, el MRO de nuevo estilo permite que las superclases inferiores sobrecarguen los atributos de las
superclases superiores, independientemente del tipo de árboles de herencia múltiple en los que se mezclen.

Cambios de clase de nuevo estilo | 997


Machine Translated by Google

dentro. Además, la regla de búsqueda de nuevo estilo evita visitar la misma superclase más de
una vez cuando es accesible desde varias subclases. Podría decirse que es mejor que DFLR, pero
se aplica a un pequeño subconjunto del código de usuario de Python; como veremos, sin embargo, la clase de nuevo estilo
El modelo en sí mismo hace que los diamantes sean mucho más comunes y que el MRO sea más importante.

Al mismo tiempo, el nuevo MRO ubicará los atributos de manera diferente, creando un potencial
incompatibilidad para las clases clásicas 2.X. Pasemos a un poco de código para ver cómo se desarrollan sus diferencias
en la práctica.

Implicaciones para los árboles de herencia de diamantes

Para ilustrar cómo difiere la búsqueda MRO de nuevo estilo, considere esta encarnación simplista
del patrón de herencia múltiple de diamantes para clases clásicas. Aquí, las superclases de D
B y C conducen al mismo ancestro común, A:

>>> clase A: atributo = 1 # Clásico (Python 2.X)


>>> clase B(A): pasar >>> # B y C conducen a A
clase C(A): atributo = 2
>>> clase D(B, C): pasar # Intenta A antes que C

>>> x = D()
>>> x.atributo # Busca x, D, B, A
1

El atributo x.attr aquí se encuentra en la superclase A, porque con las clases clásicas, el
la búsqueda de herencia sube lo más alto que puede antes de retroceder y moverse a la derecha. los
el orden de búsqueda DFLR completo visitaría x, D, B, A, C y luego A. Para este atributo, la búsqueda
se detiene tan pronto como attr se encuentra en A, arriba de B.

Sin embargo, con las clases de nuevo estilo derivadas de un objeto similar incorporado (y todas las clases en
3.X), el orden de búsqueda es diferente: Python busca en C a la derecha de B, antes de probar A
arriba B. El orden de búsqueda MRO completo visitaría x, D, B, C y luego A. Para este atributo,
la búsqueda se detiene tan pronto como se encuentra attr en C:

>>> clase A(objeto): atributo = 1 >>> # Nuevo estilo ("objeto" no requerido en 3.X)
clase B(A): pasar
>>> clase C(A): atributo = 2
>>> clase D(B, C): pasar # Intenta C antes que A

>>> x = D()
>>> x.atributo # Busca x, D, B, C
2

Este cambio en el procedimiento de búsqueda de herencia se basa en la suposición de que si


mezclas en C más abajo en el árbol, probablemente tengas la intención de tomar sus atributos de preferencia
a las A. También asume que C siempre tiene la intención de anular los atributos de A en todos los contextos,
lo que probablemente sea cierto cuando se usa solo, pero puede no serlo cuando se mezcla con
un diamante con clases clásicas, es posible que ni siquiera sepa que C puede mezclarse como
esto cuando lo codificas.

998 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

Dado que lo más probable es que el programador haya querido decir que C debería anular A en este caso,
sin embargo, las clases de nuevo estilo visitan C primero. De lo contrario, C podría ser esencialmente inútil en un
contexto de diamante para cualquier nombre en A también: no podría personalizar A y se usaría
solo para nombres exclusivos de C.

Resolución explícita de conflictos

Por supuesto, el problema con las suposiciones es que asumen cosas. Si este orden de búsqueda
desviación parece demasiado sutil para recordar, o si desea tener más control sobre la búsqueda
proceso, siempre puede forzar la selección de un atributo desde cualquier parte del árbol
asignando o nombrando en su defecto el que quieras en el lugar donde se imparten las clases
mezclados. Lo siguiente, por ejemplo, elige un orden de estilo nuevo en una clase clásica
resolviendo la elección explícitamente:
>>> clase A: atributo = 1 # Clásico
>>> clase B(A): >>> pasar
clase C(A): >>> atributo = 2
clase D(B, C): attr = C.attr # <== Elija C, a la derecha

>>> x = D()
>>> x.atributo # Funciona como nuevo estilo (todas las 3.X)
2

Aquí, un árbol de clases clásicas está emulando el orden de búsqueda de clases de nuevo estilo para un
atributo específico: la asignación al atributo en D elige la versión en C, por lo tanto
subvirtiendo la ruta de búsqueda de herencia normal (D.attr será el más bajo en el árbol). Las nuevas clases de
estilo pueden emular de manera similar las clases clásicas eligiendo la versión superior del
atributo de destino en el lugar donde se mezclan las clases:

>>> clase A(objeto): atributo = 1 >>> # Nuevo estilo


clase B(A): pasar
>>> clase C(A): atributo = 2
>>> clase D(B, C): attr = B.attr # <== Elija A.attr, arriba

>>> x = D()
>>> x.atributo # Funciona como el clásico (predeterminado 2.X)
1

Si está dispuesto a resolver siempre conflictos como este, es posible que pueda ignorar en gran medida
la diferencia en el orden de búsqueda y no confiar en suposiciones sobre lo que quiso decir cuando
codificaste tus clases.

Naturalmente, los atributos elegidos de esta manera también pueden ser funciones de método: los métodos son
atributos asignables normales que hacen referencia a objetos de función a los que se puede llamar:
>>> clase A:
def met(s): print('A.meth')

>>> clase C(A):


def meth(s): print('C.meth')

>>> clase B(A):

Cambios de clase de nuevo estilo | 999


Machine Translated by Google

pasar

>>> clase D(B, C): pasar >>> x = # Usar el orden de búsqueda predeterminado

D() >>> x.meth() # Variará según el tipo de clase


# Predeterminado al orden clásico en 2.X
A.meth

>>> clase D(B, C): meth = C.meth >>> x = D() # <== Elija el método de C: nuevo estilo (y 3.X)

>>> x.meth()
metanfetamina C

>>> clase D(B, C): meth = B.meth >>> x = D() # <== Método de Pick B: clásico

>>> x.meth()
A.meth

Aquí, seleccionamos métodos asignándolos explícitamente a nombres más bajos en el árbol. Podríamos
también simplemente llame a la clase deseada explícitamente; en la práctica, este patrón podría ser más
común, especialmente para cosas como constructores:

clase D (B, C):


def metanfetamina (uno mismo): # Redefinir inferior
...
C. metanfetamina (uno mismo) # <== Elija el método de C llamando

Dichas selecciones por asignación o llamada en puntos de combinación pueden aislar efectivamente su código
de esta diferencia en los sabores de clase. Esto se aplica solo a los atributos que maneja este
por supuesto, pero la resolución explícita de los conflictos asegura que su código no varíe
según la versión de Python, al menos en términos de selección de conflicto de atributos. En otras palabras, esto
puede servir como una técnica de portabilidad para las clases que pueden necesitar ejecutarse tanto en el
Modelos de clase clásica y de estilo nuevo.

Explícito es mejor que implícito, también para la resolución de métodos: Incluso sin
la divergencia de clases de estilo clásico/nuevo, la resolución de método explícito
La técnica que se muestra aquí puede ser útil en escenarios de herencia múltiple en general. Por
ejemplo, si desea formar parte de una superclase en el
izquierda y parte de una superclase a la derecha, es posible que deba decirle a Python
qué atributos del mismo nombre elegir mediante asignaciones explícitas
o llamadas en subclases. Revisaremos esta noción en un "te pillé" al final.
de este capitulo

También tenga en cuenta que los patrones de herencia de diamantes pueden ser más problemáticos
en algunos casos de lo que he implicado aquí (por ejemplo, ¿qué pasa si B y C tienen
constructores requeridos que llaman al constructor en A?). Dado que tales contextos son raros en
Python del mundo real, postergaremos este tema hasta que exploremos
la función súper incorporada cerca del final de este capítulo; además de proporcionar acceso
genérico a superclases en árboles de herencia simple, super admite un modo cooperativo para
resolver conflictos en herencia múltiple
árboles ordenando las llamadas a métodos según el MRO, suponiendo que este orden
tiene sentido en este contexto también!

1000 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

Ámbito del cambio en el

orden de búsqueda En suma, de forma predeterminada, el patrón de rombos se busca de manera diferente
para las clases clásicas y las de estilo nuevo, y este es un cambio no compatible con versiones anteriores.
Tenga en cuenta, sin embargo, que este cambio afecta principalmente a los casos de patrón de diamantes de
herencia múltiple; La herencia de clases de nuevo estilo funciona igual para la mayoría de las demás
estructuras de árboles de herencia. Además, no es imposible que todo este problema tenga una importancia
más teórica que práctica, ya que la búsqueda de nuevo estilo no fue lo suficientemente significativa como para
abordarla hasta Python 2.2 y no se convirtió en estándar hasta 3.0, parece poco probable que afecte a la mayoría de Python. código.

Habiendo dicho eso, también debo tener en cuenta que aunque no codifique patrones de diamantes en las
clases que escriba usted mismo, debido a que la superclase de objeto implícita está por encima de cada
clase raíz en 3.X, como vimos anteriormente, cada caso de herencia múltiple exhibe el diamante patrón hoy.
Es decir, en las clases de nuevo estilo, el objeto desempeña automáticamente el papel que desempeña la
clase A en el ejemplo que acabamos de considerar. Por lo tanto, la regla de búsqueda MRO de nuevo estilo
no solo modifica la semántica lógica, sino que también es una importante optimización del rendimiento: evita
visitar y buscar la misma clase más de una vez, incluso el objeto automático.

Igual de importante, también hemos visto que la superclase de objeto implícito en el modelo de nuevo estilo
proporciona métodos predeterminados para una variedad de operaciones integradas, incluidos los métodos
de formato de visualización __str__ y __repr__ . Ejecute un dir (objeto) para ver qué métodos se proporcionan.
Sin el orden de búsqueda MRO de nuevo estilo, en los casos de herencia múltiple, los valores predeterminados
en el objeto siempre anularían las redefiniciones en las clases codificadas por el usuario, a menos que
siempre se hicieran en la superclase más a la izquierda. En otras palabras, ¡el modelo de clase del nuevo
estilo en sí mismo hace que el uso del orden de búsqueda del nuevo estilo sea más crítico!

Para obtener un ejemplo más visual de la superclase de objetos implícita en 3.X y otros ejemplos de patrones
de diamantes creados por ella, consulte la salida de la clase ListTree en el ejemplo lister.py del capítulo
anterior, así como el caminante de árboles classtree.py ejemplo en el capítulo 29 y la siguiente sección.

Más sobre el MRO: Orden de resolución de métodos

Para rastrear cómo funciona la herencia de estilo nuevo por defecto, también podemos usar el atributo new
class.__mro__ mencionado en los ejemplos de lista de clases del capítulo anterior, técnicamente una
extensión de estilo nuevo, pero útil aquí para explorar un cambio. Este atributo devuelve el MRO de una
clase: el orden en el que la herencia busca clases en un árbol de clases de estilo nuevo. Este MRO se basa
en el algoritmo de linealización de la superclase C3 desarrollado inicialmente en el lenguaje de programación
Dylan, pero luego adoptado por otros lenguajes, incluidos Python 2.3 y Perl 6.

El algoritmo MRO

Este libro evita deliberadamente una descripción completa del algoritmo MRO, porque muchos
Los programadores de Python no necesitan preocuparse (esto solo afecta a los diamantes, que son rela

Cambios de clase de nuevo estilo | 1001


Machine Translated by Google

bastante raro en el código del mundo real); porque difiere entre 2.X y 3.X; y porque los detalles del MRO son
demasiado arcanos y académicos para este texto. Como regla general, este libro evita los algoritmos formales
y prefiere enseñar informalmente con ejemplos.

Por otro lado, es posible que algunos lectores todavía estén interesados en la teoría formal detrás de MRO de
nuevo estilo. Si este conjunto lo incluye a usted, se describe con todo detalle en línea; busque en los manuales
de Python y en la web los enlaces MRO actuales. Sin embargo, en resumen, el MRO esencialmente funciona
así:

1. Enumere todas las clases que una instancia hereda del uso de la regla de búsqueda DFLR de la clase
clásica e incluya una clase varias veces si se visita más de una vez.

2. Escanee la lista resultante en busca de clases duplicadas, eliminando todas menos la última ocurrencia de
duplicados en la lista.

La lista MRO resultante para una clase determinada incluye la clase, sus superclases y todas las superclases
superiores hasta la clase raíz del objeto en la parte superior del árbol. Está ordenado de tal manera que cada
clase aparece antes que sus padres, y varios padres conservan el orden en que aparecen en la tupla de la
superclase __bases__ .

Sin embargo, es crucial que, debido a que los padres comunes en diamantes aparecen solo en la posición de
su última visita, las clases más bajas se buscan primero cuando la lista MRO se usa más tarde por herencia
de atributos. Además, cada clase se incluye y, por lo tanto, se visita solo una vez, sin importar cuántas clases
conduzcan a ella.

Veremos las aplicaciones de este algoritmo más adelante en este capítulo, incluida la de super : una función
integrada que eleva el MRO a lectura obligatoria si desea comprender completamente cómo se envían los
métodos mediante esta llamada, en caso de que elija usarlo. Como veremos, a pesar de su nombre, esta
llamada invoca la siguiente clase en el MRO, que podría no ser una superclase en absoluto.

Rastreando el

MRO Si solo quiere ver cómo la herencia de nuevo estilo de Python ordena las superclases en general, las
clases de nuevo estilo (y por lo tanto todas las clases en 3.X) tienen un tributo class.__mro__ , que es una
tupla que da el orden de búsqueda lineal que usa Python para buscar atributos en las superclases. En realidad,
este atributo es el orden de herencia en las clases de nuevo estilo y, a menudo, es tanto detalle de MRO como
necesitan muchos usuarios de Python.

Aquí hay algunos ejemplos ilustrativos, ejecutados en 3.X; solo para patrones de herencia de diamantes , la
búsqueda es el nuevo orden que hemos estado estudiando, a través de antes de arriba, según el MRO para
las clases de estilo nuevo que siempre se usa en 3.X y está disponible como una opción en 2.X:

>>> clase A: pasa >>>


clase B(A): pasa >>> clase # Diamantes: el orden difiere para el nuevo estilo
C(A): pasa >>> clase D(B, C): # Amplitud primero en los niveles inferiores
pasa >>> D.__mro__ (<clase
'__principal__ .D'>, <clase
'__principal__.B'>, <clase '__principal__.C'>, <clase '__principal__.A'>, <clase 'objeto'>)

1002 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

Sin embargo, para los que no son diamantes, la búsqueda sigue siendo como siempre (aunque con una raíz de objeto
adicional ): hacia la parte superior y luego hacia la derecha (también conocido como DFLR, profundidad primero y de
izquierda a derecha, el modelo utilizado para todas las clases clásicas ). en 2.X):

>>> clase A: pasa >>>


clase B(A): pasa >>> clase # Sin diamantes: orden igual que el clásico
C: pasa >>> clase D(B, C): # Profundidad primero, luego de izquierda a derecha

pasa >>> D.__mro__ (<clase


'__main__.D' >, <clase
'__principal__.B'>, <clase '__principal__.A'>, <clase '__principal__.C'>, <clase 'objeto'>)

El MRO del siguiente árbol, por ejemplo, es el mismo que el diamante anterior, por
DFLR:

>>> clase A: pasa >>>


clase B: pasa >>> clase # Otro no diamante: DFLR
C(A): pasa >>> clase D(B,
C): pasa >>> D.__mro__ (<clase
'__main__.D' >, <clase
'__principal__.B'>, <clase '__principal__.C'>, <clase '__principal__.A'>, <clase 'objeto'>)

Observe cómo la superclase de objeto implícito siempre aparece al final del MRO; como hemos visto,
se agrega automáticamente encima de las clases raíz (superiores) en los árboles de clases de estilo
nuevo en 3.X (y opcionalmente en 2.X):
>>> A.__bases__ # Enlaces de superclase: objeto en dos raíces
(<clase 'objeto'>,)
>>> B.__bases__
(<clase 'objeto'>,)
>>> C.__bases__
(<clase '__principal__.A'>,)
>>> D.__bases__
(<clase '__principal__.B'>, <clase '__principal__.C'>)

Técnicamente, la superclase de objeto implícito siempre crea un diamante en herencia múltiple, incluso
si sus clases no lo hacen; sus clases se buscan como antes, pero el nuevo estilo MRO garantiza que
el objeto se visite en último lugar, por lo que sus clases pueden anular sus valores predeterminados:
>>> clase X: pasa >>>
clase Y: pasa >>> clase
A(X): pasa >>> clase B(Y): # Nondiamond: primero la profundidad y luego de izquierda a derecha

pasa >>> clase D(A, B): # Aunque el "objeto" implícito siempre forma un diamante
pasa >>> D .mro() [<clase
'__principal__.D'>, <clase
'__principal__.A'>, <clase '__principal__.X'>, <clase '__principal__.B'>, <clase '__principal__.Y'>,
<clase 'objeto'>]

>>> X.__bases__, Y.__bases__ ((<clase


'objeto'>,), (<clase 'objeto'>,))
>>> A.__bases__, B.__bases__ ((<clase
'__principal__.X'>,), (<clase '__principal__.Y'>,))

Cambios de clase de nuevo estilo | 1003


Machine Translated by Google

El atributo class.__mro__ solo está disponible en clases de nuevo estilo; no está presente en 2.X a menos que
las clases se deriven del objeto. Estrictamente hablando, las clases de nuevo estilo también tienen un método
class.mro() utilizado en el ejemplo anterior para variar; se llama en el momento de la instanciación de la clase
y su valor de retorno es una lista que se usa para inicializar el atributo __mro__ cuando se crea la clase (el
método está disponible para su personalización en las metaclases, que se describe más adelante). También
puede seleccionar nombres de MRO si las visualizaciones de objetos de las clases son demasiado detalladas,
aunque este libro generalmente muestra los objetos para recordarle su verdadera forma:

>>> D.mro() == lista(D.__mro__)

Verdadero >>> [cls.__name__ para cls en D.__mro__]


['D', 'A', 'X', 'B', 'Y', 'objeto']

Independientemente de cómo acceda a ellas o las muestre, las rutas de clase MRO pueden ser útiles para
resolver confusiones y en herramientas que deben imitar el orden de búsqueda de herencia de Python. La
siguiente sección muestra este último rol en acción.

Ejemplo: asignación de atributos a fuentes de herencia Como un caso de

uso principal de MRO, notamos al final del capítulo anterior que los trepadores de árboles de clases, como la
combinación de listas de árboles de clases que escribimos allí, podrían beneficiarse del MRO. Tal como estaba
codificado, el listado de árboles proporcionaba las ubicaciones físicas de los atributos en un árbol de clases.
Sin embargo, al asignar la lista de atributos heredados en un resultado de directorio a la secuencia lineal MRO
(o el orden DFLR para las clases clásicas), estas herramientas pueden asociar más directamente los atributos
con las clases de las que se heredan, lo que también es una relación útil para los programadores.

No volveremos a codificar nuestra lista de árboles aquí, pero como primer paso importante, el siguiente archivo,
mapattrs.py, implementa herramientas que se pueden usar para asociar atributos con su fuente de herencia;
como bono adicional, su función mapattrs demuestra cómo la herencia realmente busca atributos en los objetos
del árbol de clase, aunque el MRO de nuevo estilo está en gran parte automatizado para nosotros:

"""

Archivo mapattrs.py (3.X + 2.X)

Herramienta principal: mapattrs() asigna todos los atributos heredados por


una instancia a la instancia o clase de la que se heredan.

Asume que dir() proporciona todos los atributos de una instancia. Para simular
la herencia, utiliza la tupla MRO de la clase, que proporciona el orden de
búsqueda de las clases de estilo nuevo (y todo en 3.X), o un recorrido recursivo
para inferir el orden DFLR de las clases clásicas en 2.X.

También aquí: heritage() da un orden de clase neutral a la versión; una


variedad
""" de herramientas de diccionario usando comprensiones 3.X/2.7.

import pprint
def trace(X, label='', end='\n'):

1004 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

print(etiqueta + pprint.pformat(X) + end) # Imprime bien

def filterdictvals(D, V):


"""
dict D con entradas para el valor V eliminadas.
filterdictvals(dict(a=1, b=2, c=1), 1) => {'b': 2}
"""

devuelve {K: V2 para (K, V2) en D.items() si V2 != V}

def invertir (D):


"""

dict D con valores cambiados a claves (agrupados por valores).


Todos los valores deben ser hashables para funcionar como
claves dict/set. invertdict(dict(a=1, b=2, c=1)) => {1: ['a', 'c'], 2: ['b']}
"""

def keysof(V):
devuelve ordenado(K por K en D.keys() si D[K] == V)
devuelve {V: claves de (V) para V en conjunto (D.valores ())}

def dflr(cls):
"""

Orden clásico de izquierda a derecha en profundidad del árbol de clases en cls.


Los
""" ciclos no son posibles: Python no permite cambios en __bases__.
aquí = [cls]
para sup en cls.__bases__:
aquí += dflr(sup) volver
aquí

herencia def (instancia):


"""

Secuencia
""" de orden de herencia: nuevo estilo (MRO) o clásico (DFLR)

si hasattr(instancia.__clase__, '__mro__'):
return (instancia,) + instancia.__clase__.__mro__ más:

return [instancia] + dflr(instancia.__clase__)

def mapattrs(instancia,
""" conobjeto=Falso, porfuente=Falso):

dict con claves que dan todos los atributos heredados de instancia,
con valores que dan el objeto del que se hereda cada uno. withobject:
False=eliminar los atributos de clase integrados del objeto. bysource:
True=resultado del grupo por objetos en lugar de atributos.
Admite
""" clases con ranuras que excluyen __dict__ en instancias.

attr2obj = {}
hereda = herencia(instancia) for attr
in dir(instancia): for obj in hereda: if
hasattr(obj, '__dict__') and attr in
obj.__dict__: attr2obj[attr] = obj break # Ver tragamonedas

si no con objeto:

Cambios de clase de nuevo estilo | 1005


Machine Translated by Google

attr2obj = filterdictvals(attr2obj, objeto)


devolver attr2obj si no es bysource else invertdict(attr2obj)

si __nombre__ == '__principal__':
print('Clases clásicas en 2.X, estilo nuevo en 3.X')
clase A: atributo1 = 1
clase B(A): atributo2 = 2
clase C(A): attr1 = 3
clase D (B, C): aprobado
yo = D()
print('Py=>%s' % I.attr1) # ¿La búsqueda de Python == la nuestra?

trace(herencia(I), 'INH\n') trace(mapattrs(I), 'ATTRS\n') # [Orden de herencia]


trace(mapattrs(I, bysource=True) , 'OBJS\n') # Atributos => Fuente
# Fuente => [Atributos]

print('Clases de nuevo estilo en 2.X y 3.X')


clase A(objeto): attr1 = 1 clase B(A): # "(objeto)" opcional en 3.X
clase C(A): attr1 = 3 atributo2 = 2

clase D (B, C): aprobado


yo = D()
imprimir('Py=>%s' %I.attr1)
traza(herencia(I), 'INH\n')
trace(mapattrs(I), 'ATTR\n')
trace(mapattrs(I, bysource=True), 'OBJS\n')

Este archivo asume que dir proporciona todos los atributos de una instancia. Mapea cada atributo en un directorio
resultado a su fuente escaneando el pedido MRO para clases de nuevo estilo, o el DFLR
order para las clases clásicas, buscando el espacio de nombres __dict__ de cada objeto en el camino.
Para las clases clásicas, el orden de DFLR se calcula con un escaneo recursivo simple. La red
El efecto es simular la búsqueda de herencia de Python en ambos modelos de clase.

El código de autoevaluación de este archivo aplica sus herramientas a los árboles de herencia múltiple de diamantes que
vio antes. Utiliza el módulo de biblioteca pprint de Python para mostrar listas y diccionarios muy bien
—pprint.pprint es su llamada básica y su formato p devuelve una cadena de impresión. ejecutar esto en
Python 2.7 para ver órdenes de búsqueda de DFLR clásico y MRO de nuevo estilo; en Python 3.3,
la derivación del objeto es innecesaria, y ambas pruebas dan los mismos resultados de nuevo estilo.
Es importante destacar que attr1, cuyo valor está etiquetado con "Py=>" y cuyo nombre aparece en
las listas de resultados, se hereda de la clase A en la búsqueda clásica, pero de la clase C en el nuevo estilo
búsqueda:

c:\code> py ÿ2 mapattrs.py
Clases clásicas en 2.X, nuevo estilo en 3.X
Py=>1
INH
[<__main__.D instancia en 0x000000000225A688>,
<clase __principal__.D en 0x0000000002248828>,
<clase __principal__.B en 0x0000000002248768>,
<clase __principal__.A en 0x0000000002248708>,
<clase __principal__.C en 0x00000000022487C8>,
<clase __principal__.A en 0x0000000002248708>]

ATTRS

1006 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

{'__doc__': <clase __main__.D en 0x0000000002248828>, '__module__':


<clase __main__.D en 0x0000000002248828>, 'attr1': <clase __main__.A en
0x0000000002248708>, 'attr2': <clase en __main__.B 0x0000000002248768>}

OBJS
{<clase __main__.A en 0x0000000002248708>: ['attr1'], <clase __main__.B
en 0x0000000002248768>: ['attr2'], <clase __main__.D en
0x0000000002248828>: ['__doc__', '__module__']}

Clases de nuevo estilo en 2.X y 3.X Py=>3

INH
(<__principal__.D objeto en 0x0000000002257B38>, <clase
'__principal__.D'>, <clase '__principal__.B'>, <clase
'__principal__.C'>, <clase '__principal__.A'>, <tipo 'objeto '>)

ATTRS
{'__dict__': <clase '__main__.A'>, '__doc__':
<clase '__main__.D'>, '__module__': <clase
'__main__.D'>, '__weakref__': <clase '__main__.A
'>, 'attr1': <clase '__main__.C'>, 'attr2': <clase
'__main__.B'>}

OBJS
{<clase '__main__.A'>: ['__dict__', '__weakref__'], <clase '__main__.B'>:
['attr2'], <clase '__main__.C'>: ['attr1'], <clase '__principal__.D'>:
['__doc__', '__módulo__']}

Como una aplicación más grande de estas herramientas, lo siguiente es nuestro simulador de herencia
en funcionamiento en 3.3 en las clases de prueba del archivo testmixin0.py del capítulo anterior (he
eliminado algunos nombres incorporados aquí por espacio; como de costumbre, ejecute en vivo para
toda la lista ). Observe cómo se asignan __X nombres pseudoprivados a sus clases de definición, y
cómo aparece ListInstance en el MRO antes del objeto, que tiene una __str__ que, de lo contrario, se
elegiría primero; como recordará, mezclar este método fue el punto central de la clases de lister!
c:\code> py ÿ3 >>>
from mapattrs import trace, dflr, heritage, mapattrs >>> from testmixin0 import
Sub >>> I = Sub() >>> trace(dflr(I.__class__)) [< clase 'testmixin0.Sub'>, <clase
'testmixin0.Super'>, <clase 'objeto'>, <clase 'listinstance.ListInstance'>,
# Sub hereda de las<clase
raíces 'objeto'>]
Super y ListInstance # 2.X
orden de búsqueda: ¡objeto implícito antes de lister!

>>> rastro(herencia(I)) # 3.X (+ 2.X nuevo estilo) orden de búsqueda: listado


primero (<objeto testmixin0.Sub en 0x0000000002974630>, <clase 'testmixin0.Sub'>,

Cambios de clase de nuevo estilo | 1007


Machine Translated by Google

<clase 'testmixin0.Super'>,
<clase 'listinstance.ListInstance'>, <clase
'objeto'>)

>>> rastrear(mapattrs(I))
{'_ListInstance__attrnames': <clase 'listinstance.ListInstance'>, '__init__':
<clase 'testmixin0.Sub'>, '__str__': <clase 'listinstance.ListInstance'>,

...etc...
'data1': <objeto testmixin0.Sub en 0x0000000002974630>,
'data2': <objeto testmixin0.Sub en 0x0000000002974630>,
'data3': <objeto testmixin0.Sub en 0x0000000002974630>,
'ham': <clase 'testmixin0.Super' >, 'correo no deseado': <clase
'testmixin0.Sub'>}

>>> trace(mapattrs(I, bysource=True))


{<testmixin0.Sub object at 0x0000000002974630>: ['data1', 'data2', 'data3'], <class
'listinstance.ListInstance'>: ['_ListInstance__attrnames ', '__str__'], <clase
'testmixin0.Super'>: ['__dict__', '__weakref__', 'ham'], <clase 'testmixin0.Sub'>: ['__doc__',
'__init__', '__module__ ', '__qualname__', 'correo no deseado']}

>>> trace(mapattrs(I, conobjeto=Verdadero))


{'_ListInstance__attrnames': <clase 'listinstance.ListInstance'>, '__class__':
<clase 'objeto'>, '__delattr__': <clase 'objeto'>, ...etc...

Este es el bit que puede ejecutar si desea etiquetar objetos de clase con nombres heredados por una
instancia, ¡aunque es posible que desee filtrar algunos nombres incorporados de doble guión bajo por
el bien de la vista de los usuarios!

>>> amap = mapattrs(I, withobject=True, bysource=True)


>>> trace(amap) {<testmixin0.Sub object at
0x0000000002974630>: ['data1', 'data2', 'data3'], <class 'listinstance.ListInstance'>:
['_ListInstance__attrnames', '__str__'], <class 'testmixin0.Super'>: ['__dict__',
'__weakref__', 'ham'], <class 'testmixin0.Sub'>: [ '__doc__', '__init__', '__module__',
'__qualname__', 'spam'], <class 'object'>: ['__class__', '__delattr__', ...etc... '__sizeof__',
'__subclasshook__ ']}

Finalmente, y como continuación de las cavilaciones del capítulo anterior y como paso a la siguiente
sección, a continuación se muestra cómo funciona este esquema también para los atributos de
tragamonedas basados en clases. Debido a que el __dict__ de una clase incluye atributos de clase
normales y entradas individuales para los atributos de instancia definidos por su lista de __ranuras__ , las ranuras en

1008 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

los tributos heredados por una instancia se asociarán correctamente con la clase de implementación
de la que se adquieren, aunque no estén almacenados físicamente en el propio __dict__ de la
instancia :
# mapattrs-slots.py: probar la herencia de atributos de
__slots__ de mapattrs importar mapattrs, rastrear

clase A(objeto): __slots__ = ['a', 'b']; x = 1; y = 2 clase B(A):


__ranuras__ = ['b', 'c'] clase C(A): clase D(B, C): z = 3
x=2

def __init__(self): self.nombre = 'Bob';

I = D()
traza(mapattrs(I, bysource=True)) # También: trace(mapattrs(I))

Para clases de estilo explícitamente nuevo como las de este archivo, los resultados son los mismos tanto en 2.7
como en 3.3, aunque 3.3 agrega un nombre incorporado adicional al conjunto. Los nombres de los atributos aquí
reflejan todos los heredados por la instancia de clases definidas por el usuario, incluso aquellos implementados por
ranuras definidas en las clases y almacenadas en el espacio asignado en la instancia: c:\code> py ÿ3 mapattrs-

slots.py {< Objeto __main__.D en 0x00000000028988E0>: ['nombre'], <clase '__main__.C'>: ['x'], <clase
'__main__.D'>: ['__dict__', '__doc__', '__init__' , '__módulo__', '__qualname__', '__weakref__', 'z'], <clase
'__principal__.A'>: ['a', 'y'], <clase '__principal__.B'>: ['__ranuras__' , 'antes de Cristo']}

Pero debemos avanzar para comprender mejor el papel de las tragamonedas y comprender por qué
los mapattrs deben tener cuidado de verificar si hay un __dict__ antes de buscarlo.

Estudie este código para obtener más información. Para la lista de árboles del capítulo anterior, su
siguiente paso podría ser indexar el resultado del diccionario bysource =True de la función mapattrs
para obtener los atributos de un objeto durante el recorrido del bosquejo del árbol, en lugar de (¿o
quizás además de?) su exploración física actual de __dict__ . Probablemente necesitará usar getattr
en la instancia para obtener los valores de los atributos, porque algunos pueden implementarse
como ranuras u otros atributos "virtuales" en sus clases de origen, y obtenerlos directamente en la
clase no devolverá el valor de la instancia. Sin embargo, si codigo más aquí, privaré a los lectores
de la diversión restante y de la siguiente sección de su tema.

Cambios de clase de nuevo estilo | 1009


Machine Translated by Google

El módulo pprint de Python utilizado en este ejemplo funciona como se muestra en Pythons
3.3 y 2.7, pero parece tener un problema en Pythons 3.2 y 3.1 donde genera una excepción
de argumentos numéricos incorrectos internamente para los objetos que se muestran aquí.
Dado que ya he dedicado demasiado espacio a cubrir los defectos transitorios de Python,
y dado que esto se ha reparado en las versiones de Python utilizadas en esta edición,
dejaremos de trabajar en torno a esto en la columna de ejercicios sugeridos para los
lectores que ejecutan esto en el pitones infectados; cambie el trazo a impresiones simples
según sea necesario, ¡y tenga en cuenta la nota sobre la dependencia de la batería en el
Capítulo 1!

Extensiones de clase de nuevo estilo


Más allá de los cambios descritos en la sección anterior (algunos de los cuales, francamente, pueden
parecer demasiado académicos y oscuros para muchos lectores de este libro), las clases de nuevo estilo
brindan un puñado de herramientas de clase más avanzadas que tienen una aplicación más directa y
práctica. —ranuras, propiedades, descriptores y más. Las siguientes secciones brindan una descripción
general de cada una de estas características adicionales, disponibles para la clase de estilo nuevo en
Python 2.X y todas las clases en Python 3.X. También en esta categoría de extensiones se encuentran el
atributo __mro__ y la súper llamada, ambos cubiertos en otra parte: el primero en la sección anterior para
explorar un cambio, y el último pospuesto hasta el final del capítulo para servir como un estudio de caso
más amplio.

Slots: declaraciones de atributos


Al asignar una secuencia de nombres de atributo de cadena a un atributo de clase __slots__ especial ,
podemos habilitar una clase de estilo nuevo para limitar el conjunto de atributos legales que tendrán las
instancias de la clase y optimizar el uso de la memoria y posiblemente la velocidad del programa. Sin
embargo, como veremos, las tragamonedas deben usarse solo en aplicaciones que claramente justifiquen
la complejidad adicional. Complicarán su código, pueden complicar o romper el código que puede usar y
requieren una implementación universal para que sean efectivos.

Conceptos básicos de las tragamonedas

Para usar las ranuras, asigne una secuencia de nombres de cadena a la variable especial __ranuras__ y
al atributo en el nivel superior de una declaración de clase : solo los nombres en la lista de __ranuras__ se
pueden asignar como atributos de instancia. Sin embargo, como todos los nombres en Python, los nombres
de los atributos de las instancias aún deben asignarse antes de poder hacer referencia a ellos, incluso si
aparecen en __ranuras__:

>>> limitador de clase (objeto):


__ranuras__ = ['edad', 'nombre', 'trabajo']

>>> x = limitador() >>>


x.edad AttributeError: # Debe asignar antes de usar
edad

1010 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

>>> x.age = 40 # Parecen datos de instancia


>>> x.age 40 >>>
x.ape = 1000
AttributeError: el # Ilegal: no en __slots__
objeto 'limiter' no tiene atributo 'ape'

Esta función se concibe como una forma de detectar errores tipográficos como este (se detectan las asignaciones a
nombres de atributos ilegales que no están en __ranuras__ ) y como un mecanismo de optimización.

La asignación de un diccionario de espacio de nombres para cada objeto de instancia puede resultar costosa en
términos de memoria si se crean muchas instancias y solo se requieren unos pocos atributos. Para ahorrar espacio, en
lugar de asignar un diccionario para cada instancia, Python reserva suficiente espacio en cada instancia para contener
un valor para cada atributo de ranura, junto con atributos heredados en la clase común para administrar el acceso a la
ranura. Esto también podría acelerar la ejecución, aunque este beneficio es menos claro y puede variar según el
programa, la plataforma y Python.

Las tragamonedas también son una especie de ruptura importante con la naturaleza dinámica central de Python, que
dicta que cualquier nombre puede crearse por asignación. De hecho, imitan a C++ por su eficiencia a expensas de la
flexibilidad, e incluso tienen el potencial de romper algunos programas. Como veremos, las tragamonedas también
vienen con una plétora de reglas de uso para casos especiales. Según el propio manual de Python, no deben usarse
excepto en casos claramente garantizados; son difíciles de usar correctamente y, para citar el manual, lo son:

es mejor reservarlo para casos excepcionales en los que hay un gran número de instancias en una aplicación crítica para la
memoria.

En otras palabras, esta es otra característica más que debe usarse solo si está claramente justificada.
Desafortunadamente, las tragamonedas parecen estar apareciendo en el código de Python con mucha más frecuencia
de lo que deberían; su oscuridad parece ser un atractivo en sí mismo. Como de costumbre, el conocimiento es tu mejor
aliado en este tipo de cosas, así que echemos un vistazo rápido aquí.

En Python 3.3, los requisitos de espacio de atributos que no son ranuras se han reducido con un modelo
de diccionario de claves compartidas , en el que los diccionarios __dict__ utilizados para los atributos de
los objetos pueden compartir parte de su almacenamiento interno, incluido el de sus claves. Esto puede
disminuir parte del valor de __slots__ como herramienta de optimización; según los informes de referencia,
este cambio reduce el uso de memoria entre un 10 % y un 20 % para los programas orientados a objetos,
ofrece una pequeña mejora en la velocidad de los programas que crean muchos objetos similares y puede
optimizarse aún más en el futuro. Por otro lado, ¡esto no negará la presencia de __slots__ en el código
existente que tal vez necesite comprender!

Tragamonedas y diccionarios de

espacios de nombres Aparte de los beneficios potenciales, las tragamonedas pueden complicar sustancialmente el
modelo de clase y el código que se basa en él. De hecho, algunas instancias con ranuras pueden no tener un atributo __dict__

Extensiones de clase de nuevo estilo | 1011


Machine Translated by Google

diccionario de espacio de nombres en absoluto, y otros tendrán atributos de datos que este diccionario no incluye.
Para ser claros: esta es una gran incompatibilidad con el modelo de clase tradicional, que puede complicar
cualquier código que acceda a los atributos de forma genérica e incluso puede causar que algunos programas
fallen por completo.

Por ejemplo, los programas que enumeran o acceden a los atributos de instancia por cadena de nombre pueden
necesitar usar más interfaces independientes del almacenamiento que __dict__ si se pueden usar ranuras. Debido
a que los datos de una instancia pueden incluir nombres de nivel de clase, como ranuras, ya sea además o en
lugar del almacenamiento del diccionario de espacio de nombres, es posible que se deba consultar la integridad
de ambas fuentes de atributos.

Veamos qué significa esto en términos de código y exploremos más sobre las tragamonedas en el camino.
En primer lugar, cuando se usan ranuras, las instancias normalmente no tienen un diccionario de atributos; en su
lugar, Python usa la función de descriptores de clase que se presentó más adelante para asignar y administrar el
espacio reservado para los atributos de ranura en la instancia. En Python 3.X y en 2.X para las clases de nuevo
estilo derivadas del objeto:
>>> clase C: # Requiere "(objeto)" solo en 2.X #
__ranuras__ = ['a', 'b'] __slots__ significa que no __dict__ por defecto

>>> X = C()
>>> Xa = 1 >>>
Xa 1

>>> X.__dict__
AttributeError: el objeto 'C' no tiene atributo '__dict__'

Sin embargo, aún podemos buscar y establecer atributos basados en ranuras por cadena de nombre usando
herramientas neutrales de almacenamiento como getattr y setattr (que miran más allá de la instancia __dict__ y,
por lo tanto, incluyen nombres de nivel de clase como ranuras) y dir (que recopila todos los nombres heredados a
lo largo de un árbol de clases):

>>> getattr(X, 'a')


1
>>> setattr(X, 'b', 2) # Pero getattr() y setattr() todavía funcionan
>>> Xb 2

>>> 'a' en directorio(X) # Y dir() también encuentra atributos de ranura

Verdadero >>> 'b' en dir(X)


Verdadero

También tenga en cuenta que sin un diccionario de espacios de nombres de atributos, no es posible asignar
nuevos nombres a las instancias que no son nombres en la lista de espacios:
>>> clase D: # Use D (objeto) para el mismo resultado en 2.X
__slots__ = ['a', 'b'] def
__init__(self): self.d = 4
# No se pueden agregar nuevos nombres si no __dict__

>>> X = D()
AttributeError: el objeto 'D' no tiene atributo 'd'

1012 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

Sin embargo, aún podemos acomodar atributos adicionales al incluir __dict__ explícitamente en __slots__, para crear
también un diccionario de espacio de nombres de atributos:

>>> clase D:
__slots__ = ['a', 'b', '__dict__'] # Nombre __dict__ para incluir uno también
normalmente c = 3 # Los atributos de clase funcionan

def __init__(auto): auto.d


=4 # d almacenado en __dict__, a es un espacio

>>> X = D()
>>> Xdd

4 >>> Xc

3 >>> # Todos los atributos de la instancia no están definidos hasta que se asignan
Error de atributo Xa : a
>>> Xa = 1 >>> Xb = 2

En este caso, se utilizan ambos mecanismos de almacenamiento. Esto hace que __dict__ sea demasiado limitado para el
código que desea tratar las ranuras como datos de instancia, pero las herramientas genéricas como getattr aún nos
permiten procesar ambas formas de almacenamiento como un único conjunto de atributos:

>>> X.__dict__ {'d': # Algunos objetos tienen __dict__ y nombres de espacios


4} # getattr() puede obtener cualquier tipo de attr
>>> X.__ranuras__
['a', 'b', '__dict__'] >>> getattr(X,
'a'), getattr(X, 'c'), getattr(X, 'd') # Obtiene las 3 formas (1, 3, 4)

Sin embargo, debido a que dir también devuelve todos los atributos heredados , podría ser demasiado amplio en algunos
contextos; también incluye métodos a nivel de clase e incluso todos los valores predeterminados de los objetos . El código
que desea enumerar solo los atributos de instancia puede, en principio, tener que permitir explícitamente ambas formas de
almacenamiento. Al principio podríamos codificar ingenuamente esto de la siguiente manera:

>>> for attr en lista(X.__dict__) + X.__slots__: print(attr, '=>', # Equivocado...


getattr(X, attr))

Dado que cualquiera de los dos puede omitirse, podemos codificar esto de manera más correcta de la siguiente manera,
usando get attr para permitir los valores predeterminados, un enfoque noble pero no obstante inexacto, como se explicará
en la siguiente sección:

>>> for attr en lista(getattr(X, '__dict__', [])) + getattr(X, '__slots__', []):


imprimir(atributo, '=>', obtener atributo(X, atributo))

d => 4 a
=> 1 b # Menos mal...
=> 2
__dict__ => {'d': 4}

Múltiples listas de __slot__ en superclases

El código anterior funciona en este caso específico, pero en general no es del todo preciso. Específicamente, este código
aborda solo los nombres de las ranuras en el atributo __slots__ más bajo

Extensiones de clase de nuevo estilo | 1013


Machine Translated by Google

heredado por una instancia, pero las listas de ranuras pueden aparecer más de una vez en un árbol de clase. Es decir,
la ausencia de un nombre en la lista de __ranuras__ más bajas no excluye su existencia en una __ranuras__ superior.
Debido a que los nombres de las ranuras se convierten en atributos de nivel de clase, las instancias adquieren la unión
de todos los nombres de las ranuras en cualquier parte del árbol, según la regla de herencia normal:

>>> clase E:
__ranuras__ = ['c', 'd'] >>> # La superclase tiene tragamonedas

clase D(E): __ranuras__ = ['a', '__dict__']


# Pero también lo hace su subclase

>>> X = D()
>>> Xa = 1; Xb = 2; Xc = 3 >>> Xa, Xc # La instancia es la unión (ranuras: a, c)
(1, 3)

Inspeccionar solo la lista de espacios heredados no seleccionará los espacios definidos más arriba en un árbol de clase:

>>> E.__ranuras__ # Pero las ranuras no están concatenadas


['c', 'd']
>>> D.__ranuras__
['a', '__dict__']
>>> X.__ranuras__ # Instancia hereda *más bajo* __slots__
['a', '__dict__']
>>> X.__dict__ {'b': # Y tiene su propio an attr dict
2}

>>> for attr en lista(getattr(X, '__dict__', [])) + getattr(X, '__slots__', []):


imprimir(atributo, '=>', obtener atributo(X, atributo))

segundo => 2 # ¡Se perdieron otros espacios de superclase!


un => 1
__dict__ => {'b': 2}

>>> dir(X) # Pero dir() incluye todos los nombres de las ranuras

[...muchos nombres omitidos... 'a', 'b', 'c', 'd']

En otras palabras, en términos de enumerar atributos de instancia de forma genérica, un __slots__ no siempre es
suficiente: están potencialmente sujetos al procedimiento de búsqueda de herencia completa.
Consulte el mapattrs-slots.py anterior para ver otro ejemplo de ranuras que aparecen en varias superclases. Si varias
clases en un árbol de clases tienen sus propios atributos de __ranuras__ , los programas genéricos deben desarrollar
otras políticas para enumerar los atributos, como se explica en la siguiente sección.

Manejo de espacios y otros atributos "virtuales" de manera

genérica En este punto, es posible que desee revisar la discusión sobre las opciones de política de espacios en la
cobertura de las clases mixtas de visualización de lister.py cerca del final del capítulo anterior: un excelente ejemplo de
por qué Es posible que los programas genéricos deban preocuparse por las tragamonedas. Tales herramientas que
intentan enumerar atributos de datos de instancias deben tener en cuenta genéricamente las ranuras y quizás otros
atributos de instancias "virtuales" como las propiedades y los descriptores que se analizan más adelante, nombres que
residen de manera similar en las clases pero que pueden proporcionar valores de atributo para las instancias.

1014 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

bajo pedido. Las tragamonedas son las más centradas en datos de estos, pero son representativas de una categoría
más grande.

Dichos atributos requieren enfoques inclusivos, manejo especial o evitación general; el último de los cuales se vuelve
insatisfactorio tan pronto como cualquier programador usa espacios en el código de materia. En realidad, los atributos
de instancia a nivel de clase, como las ranuras, probablemente requieran una nueva definición del término datos de
instancia, como atributos almacenados localmente, la unión de todos los atributos heredados o algún subconjunto de
los mismos.

Por ejemplo, algunos programas pueden clasificar los nombres de las ranuras como atributos de clases en lugar de
instancias; después de todo, estos atributos no existen en los diccionarios de espacio de nombres de instancia.
De manera alternativa, como se mostró anteriormente, los programas pueden ser más inclusivos si confían en dir
para obtener todos los nombres de atributos heredados y en getattr para obtener sus valores correspondientes para
la instancia, sin tener en cuenta su ubicación física o implementación. Si debe admitir ranuras como datos de instancia,
esta es probablemente la forma más sólida de proceder:

>>> clase Slotful:


__slots__ = ['a', 'b', '__dict__'] def
__init__(self, data): self.c = data

>>> I = Slotful(3)
>>> Ia, Ib = 1, 2 >>>
Ia, Ib, Ic (1, 2, 3) # Obtención normal de atributos

>>> I.__dict__ # Tanto __dict__ como almacenamiento de ranuras


{'c': 3} >>> [x
for x in dir(I) if not x.startswith('__')] ['a', 'b', 'c']

>>> I.__dict__['c'] 3 # __dict__ es solo una fuente de atributo


>>> getattr(I, 'c'),
getattr(I, 'a') (3, 1) # dir+getattr es más amplio que __dict__ # se
aplica a espacios, propiedades, descripción

>>> for a in (x for x in dir(I) if not x.startswith('__')): print(a,


getattr(I, a))

un
1b
2c3

Bajo este modelo dir/getattr , aún puede asignar atributos a sus fuentes de herencia y filtrarlos más selectivamente
por fuente o tipo si es necesario, escaneando el MRO , como hicimos anteriormente tanto en mapattrs.py como en su
aplicación a las ranuras en mapattrs -ranuras.py.
Como beneficio adicional, dichas herramientas y políticas para el manejo de espacios también se aplicarán
potencialmente automáticamente a las propiedades y descriptores , aunque estos atributos son valores calculados de
forma más explícita y datos relacionados con instancias menos obvios que los espacios.

También tenga en cuenta que esto no es solo un problema de herramientas. Los atributos de instancia basados en
clases, como las ranuras, también afectan la codificación tradicional del método de sobrecarga del operador __setattr__

Extensiones de clase de nuevo estilo | 1015


Machine Translated by Google

vimos en el Capítulo 30. Debido a que las ranuras y algunos otros atributos no se almacenan en la instancia __dict__,
e incluso pueden implicar su ausencia, las clases de estilo nuevo generalmente deben ejecutar asignaciones de
atributos enrutándolas a la superclase de objetos . En la práctica, esto puede hacer que este método sea
fundamentalmente diferente en algunas clases clásicas y de estilo nuevo.

Reglas de uso de tragamonedas

Las declaraciones de ranuras pueden aparecer en varias clases en un árbol de clases, pero cuando lo hacen, están
sujetas a una serie de restricciones que son algo difíciles de racionalizar, a menos que comprenda la implementación
de las ranuras como descriptores de nivel de clase para cada nombre de ranura que son heredados por las instancias
donde se reserva el espacio administrado (los descriptores son una herramienta avanzada que estudiaremos en
detalle en la última parte de este libro):

• Los espacios en los subs no tienen sentido cuando están ausentes en los supers: si una subclase hereda de una
superclase sin __slots__, el atributo __dict__ de la instancia creado para la superclase siempre estará
accesible, lo que hace que un __slots__ en la subclase en gran medida tenga menos puntos. La subclase aún
administra sus espacios, pero no calcula sus valores de ninguna manera y no evita un diccionario, la razón
principal para usar espacios.

• Los espacios en supers no tienen sentido cuando están ausentes en subs: De manera similar, debido a que el
significado de una declaración de __slots__ se limita a la clase en la que aparece, las subclases producirán una
instancia __dict__ si no definen un __slots__, lo que representa un __slots__ en una superclase en gran medida
sin sentido.

• La redefinición hace que los superespacios no tengan sentido: si una clase define el mismo nombre de espacio
que una superclase, su redefinición oculta el espacio en la superclase por herencia normal.
Puede acceder a la versión del nombre definido por la ranura de la superclase solo obteniendo su descriptor
directamente de la superclase. • Las ranuras evitan los valores predeterminados de nivel de clase: debido a

que las ranuras se implementan como descriptores de nivel de clase (junto con el espacio por instancia), no puede
usar atributos de clase con el mismo nombre para proporcionar valores predeterminados como puede hacerlo
para atributos de instancia normales: asignar los mismos El nombre en la clase sobrescribe el descriptor de
ranura.

• Ranuras y __dict__: como se mostró anteriormente, __slots__ excluye tanto una instancia de __dict__ como la
asignación de nombres que no aparecen en la lista, a menos que __dict__ también se enumere explícitamente.

Ya hemos visto el último de estos en acción, y el anterior mapattrs-slots.py ilustra el tercero. Es fácil demostrar cómo las
nuevas reglas aquí se traducen en código real; lo más importante es que se crea un diccionario de espacio de nombres
cuando cualquier clase en un árbol omite ranuras, lo que niega el beneficio de optimización de memoria: >>> clase C:
pasar >>> clase D (C): __ranuras__ = ['a']

# Viñeta 1: tragamonedas en sub pero no en super


# Hace dictado de instancia para no tragamonedas
>>> X = D() # Pero el nombre de la ranura aún se administra en clase
>>> Xa = 1; Xb = 2 >>>
X.__dict__ {'b': 2}

>>> D.__dict__.teclas()

1016 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

dict_keys([... 'a', '__ranuras__', ...])

>>> clase C: __slots__ = ['a'] >>> clase # Viñeta 2: tragamonedas en super pero no en sub
D(C): pasar >>> X = D() # Hace dictado de instancia para no tragamonedas
# Pero el nombre de la ranura aún se administra en clase
>>> Xa = 1; Xb = 2 >>>
X.__dict__ {'b': 2}

>>> C.__dict__.keys()
dict_keys([... 'a', '__ranuras__', ...])

>>> clase C: __ranuras__ = ['a'] >>> # Viñeta 3: solo se puede acceder a la ranura más baja
clase D(C): __ranuras__ = ['a']

>>> clase C: __ranuras__ = ['a']; a = 99 # Viñeta 4: sin valores predeterminados de nivel de clase
ValueError: 'a' en __slots__ entra en conflicto con la variable de clase

En otras palabras, además de su potencial para romper programas, las tragamonedas esencialmente
requieren una implementación universal y cuidadosa para ser efectivas, debido a que las tragamonedas no
calculan valores dinámicamente como propiedades (que se abordarán en la siguiente sección), son en gran
parte inútiles a menos que cada clase en un tree los usa y tiene cuidado de definir solo nuevos nombres de
espacios no definidos por otras clases. Es una función de todo o nada , una propiedad desafortunada
compartida por la súper llamada discutida más adelante: >>> clase C: __ranuras__ = ['a'] >>> clase D(C):
__ranuras__ = ['b'] # Supone un uso universal, diferentes nombres

>>> X = D()
>>> Xa = 1; Xb = 2 >>>
X.__dict__ AttributeError:
el objeto 'D' no tiene atributo '__dict__'
>>> C.__dict__.keys(), D.__dict__.keys() (dict_keys([...
'a', '__slots__', ...]), dict_keys([... 'b', ' __ranuras__', ...]))

Dichas reglas, entre otras relacionadas con referencias débiles omitidas aquí por espacio, son parte de la
razón por la cual los espacios no se recomiendan generalmente, excepto en casos patológicos donde su
reducción de espacio es significativa. Incluso entonces, su potencial para complicar o descifrar el código
debería ser motivo suficiente para considerar cuidadosamente las compensaciones. No solo deben
propagarse casi neuróticamente a través de un marco, sino que también pueden romper las herramientas en las que confía
en.

Ejemplos de impactos de las tragamonedas:

ListTree y mapattrs Como un ejemplo más realista de los efectos de las tragamonedas, debido a la primera
viñeta de la sección anterior, la clase ListTree del Capítulo 31 no falla cuando se mezcla con una clase que
define __ranuras__, aunque escanea la instancia diccionarios de espacio de nombres. La propia falta de
ranuras de la clase lister es suficiente para garantizar que la instancia aún tendrá un __dict__ y, por lo tanto,
no desencadenará una excepción cuando se obtenga o se indexe. Por ejemplo, los dos siguientes se
muestran sin error; el segundo también permite que los nombres que no están en la lista de ranuras se
asignen como atributos de instancias, incluidos los requeridos por la superclase:
clase C (ListTree): pasar
X = C() # OK: no se usaron __slots__

Extensiones de clase de nuevo estilo | 1017


Machine Translated by Google

imprimir (X)

clase C (Árbol de lista): __ranuras__ = ['a', 'b'] # OK: la superclase produce __dict__
X = C()
Xc = 3
imprimir(X) # Muestra c en X, a y b en C

Las siguientes clases también se muestran correctamente: cualquier clase que no sea una ranura como ListTree genera
una instancia __dict__ y, por lo tanto, puede suponer su presencia con seguridad: clase A: __ranuras__ = ['a']

# Ambos están bien por la viñeta 1


arriba de la clase B (A, ListTree): pase

clase A: __ranuras__ = ['a'] clase


B(A, ListTree): __ranuras__ = ['b'] # Muestra b en B, a en A

Aunque hace que las ranuras de las subclases no tengan sentido, este es un efecto secundario positivo para las clases
de herramientas como ListTree (y su predecesor del Capítulo 28 ). Sin embargo, en general, algunas herramientas pueden
necesitar detectar excepciones cuando __dict__ está ausente o usar un hasattr o getattr para probar o proporcionar
valores predeterminados si el uso de ranuras puede impedir un diccionario de espacio de nombres en los objetos de
instancia inspeccionados.

Por ejemplo, ahora debería poder comprender por qué el programa mapattrs.py anteriormente en este capítulo debe
verificar la presencia de un __dict__ antes de obtenerlo: los objetos de instancia creados a partir de clases con __slots__
no tendrán uno. De hecho, si usamos la línea alternativa resaltada a continuación, la función mapattrs falla con una
excepción al intentar buscar un nombre de atributo en la instancia al frente de la secuencia de ruta de herencia:

def mapattrs(instancia, withobject=False, bysource=False): for attr in


dir(instancia): for obj in heres: if attr in obj.__dict__:

# Puede fallar si se usan __slots__

>>> clase C: __ranuras__ = ['a']


>>> X = C()
>>> mapas(X)
AttributeError: el objeto 'C' no tiene atributo '__dict__'

Cualquiera de los siguientes soluciona el problema y permite que la herramienta admita ranuras: el primero proporciona
un valor predeterminado y el segundo es más detallado pero parece un poco más explícito en su intención:

si attr en getattr(obj, '__dict__', {}):

si hasattr(obj, '__dict__') y attr en obj.__dict__:

Como se mencionó anteriormente, algunas herramientas pueden beneficiarse de la asignación de resultados de dir a
objetos en el MRO de esta manera, en lugar de escanear una instancia __dict__ en general; sin este enfoque más
inclusivo, los atributos implementados por herramientas de nivel de clase como las ranuras no se informarán como datos
de instancia. Aun así, ¡esto no excusa necesariamente que tales herramientas permitan que falte un __dict__ en la
instancia también!

1018 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

¿Qué pasa con la velocidad de las tragamonedas?

Finalmente, mientras que las tragamonedas principalmente optimizan el uso de la memoria, su impacto en la velocidad es menos claro.

Aquí hay un script de prueba simple que utiliza las técnicas de timeit que estudiamos en el Capítulo 21. Tanto para los modelos de

almacenamiento con ranuras como sin ranuras (diccionario de instancias), crea 1,000 instancias, asigna y recupera 4 atributos en cada

uno y repite 1,000 veces, para ambos modelos. tomando lo mejor de 3 ejecuciones, cada una de las cuales ejerce un total de 8 millones

de operaciones de atributos:

# File slots-
test.py from __future__ import print_function
import timeit
""" base = Is = [] for i in
range(1000): X = C()

Xa = 1; Xb = 2; Xc = 3; Xd = 4 t = Xa +
Xb + Xc + Xd
""" Es.añadir(X)

"""
sentencia
""" = clase C: __ranuras__ = ['a', 'b', 'c', 'd']
+ base
print('Ranuras =>', end=' ')
print(min(timeit.repeat(stmt, number=1000, repeat=3)))
"""
sentencia
= clase C:

""" pass
+ base
print('Nonslots=>', end=' ')
print(min(timeit.repeat(stmt, number=1000, repeat=3)))

Al menos en este código, en mi computadora portátil y en mis versiones instaladas (Python 3.3 y 2.7), los mejores tiempos implican

que las ranuras son un poco más rápidas en 3.X y un lavado en 2.X, aunque esto dice poco sobre el espacio de memoria. , y es

propenso a cambiar arbitrariamente en el futuro:

c:\code> py ÿ3 slots-test.py
Slots => 0.7780903942045899
Nonslots=> 0.9888108080898417

c:\code> py ÿ2 slots-test.py
Slots => 0.80868754371
Nonslots=> 0.802224740747

Para obtener más información sobre las tragamonedas en general, consulte el conjunto de manuales estándar de Python. También

observe el estudio de caso del decorador privado del Capítulo 39, un ejemplo que naturalmente permite atributos basados en el

almacenamiento de __slots__ y __dict__ , mediante el uso de herramientas de acceso neutrales de delegación y almacenamiento

como getattr.

Extensiones de clase de nuevo estilo | 1019


Machine Translated by Google

Propiedades: accesores de atributos Nuestra

próxima extensión de nuevo estilo son las propiedades, un mecanismo que proporciona otra forma para que las clases
de nuevo estilo definan métodos llamados automáticamente para acceso o asignación a atributos de instancia. Esta
característica es similar a las propiedades (también conocidas como "getters" y "setters") en lenguajes como Java y C#,
pero en Python generalmente se usa mejor con moderación, como una forma de agregar accesores a los atributos
después del hecho a medida que las necesidades evolucionan y lo justifican. Sin embargo, donde sea necesario, las
propiedades permiten que los valores de los atributos se calculen dinámicamente sin requerir llamadas a métodos en el
punto de acceso.

Aunque las propiedades no pueden admitir objetivos de enrutamiento de atributos genéricos, al menos para atributos
específicos son una alternativa a algunos usos tradicionales de los métodos de sobrecarga __getattr__ y __setattr__ que
estudiamos por primera vez en el Capítulo 30. Las propiedades tienen un efecto similar a estos dos métodos, pero por el
contrario incurren una llamada de método adicional solo para accesos a nombres que requieren cómputo dinámico;
normalmente se accede a otros nombres que no son de propiedad sin llamadas adicionales. Aunque __getattr__ solo se
invoca para nombres indefinidos, en su lugar se llama al método __setattr__ para la asignación a cada atributo.

Las propiedades y las máquinas tragamonedas también están relacionadas, pero tienen diferentes objetivos. Ambos
implementan atributos de instancia que no se almacenan físicamente en los diccionarios de espacio de nombres de
instancia, una especie de atributo “virtual”, y ambos se basan en la noción de descriptores de atributos a nivel de clase .
Por el contrario, las ranuras gestionan el almacenamiento de instancias, mientras que las propiedades interceptan el
acceso y calculan los valores de forma arbitraria. Debido a que su herramienta de implementación de descriptores
subyacente es demasiado avanzada para que la cubramos aquí, tanto las propiedades como los descriptores reciben un
tratamiento completo en el Capítulo 38.

Conceptos

básicos de propiedad Sin embargo, como breve introducción, una propiedad es un tipo de objeto asignado a un nombre
de atributo de clase. Una propiedad se genera llamando a la función integrada de la propiedad , pasando hasta tres
métodos de acceso (controladores para obtener, establecer y eliminar operaciones), así como una cadena de
documentación opcional para la propiedad. Si algún argumento se pasa como Ninguno o se omite, esa operación no se
admite.

El objeto de propiedad resultante normalmente se asigna a un nombre en el nivel superior de una declaración de clase
(por ejemplo, nombre=propiedad()), y una sintaxis @ especial que veremos más adelante está disponible para automatizar
este paso. Cuando se asignan así, los accesos posteriores al nombre de propiedad de la clase en sí como un atributo de
objeto (por ejemplo, obj.name) se enrutan automáticamente a uno de los métodos de acceso pasados a la llamada de
propiedad .

Por ejemplo, hemos visto cómo el método de sobrecarga del operador __getattr__ permite que las clases intercepten
referencias de atributos indefinidos tanto en las clases clásicas como en las de estilo nuevo:

>>> operadores de
clase: def __getattr__(self,
nombre): if nombre ==
'edad': return 40 else:

1020 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

aumentar AttributeError (nombre)

>>> x = operadores()
>>> x.edad # Ejecuta __getattr__
40
>>> x.nombre # Ejecuta __getattr__
Error de atributo: nombre

Aquí está el mismo ejemplo, codificado con propiedades en su lugar; tenga en cuenta que las propiedades son
disponible para todas las clases, pero requiere la derivación de objetos de estilo nuevo en 2.X para funcionar
correctamente para interceptar asignaciones de atributos (y no se quejará si olvida esto, pero
sobrescribirá silenciosamente su propiedad con los nuevos datos!):

>>> propiedades de clase (objeto): def # Necesita objeto en 2.X para setters
getage (auto):
volver 40
edad = propiedad (getage, None, None, None) # (get, set, del, docs), o use @

>>> x = propiedades()
>>> x.edad # Ejecuta la obtención
40
>>> x.nombre # Recuperación normal
AttributeError: el objeto 'propiedades' no tiene atributo 'nombre'

Para algunas tareas de codificación, las propiedades pueden ser menos complejas y más rápidas de ejecutar que las
técnicas tradicionales. Por ejemplo, cuando añadimos compatibilidad con la asignación de atributos, las propiedades
se vuelven más atractivas: hay menos código para escribir y no se requieren llamadas a métodos adicionales.
incurridos por asignaciones a atributos que no deseamos calcular dinámicamente:

>>> propiedades de clase (objeto): def # Necesita objeto en 2.X para setters
getage (auto):
volver 40
def setage(auto, valor):
print('establecer edad: %s' % valor)
self._edad = valor
edad = propiedad(getage, setage, None, None)

>>> x = propiedades()
>>> x.edad # Ejecuta la obtención
40
>>> x.edad = 42 # Corre escenario
establecer edad: 42
>>> x._edad # Recuperación normal: sin llamada de captura
42
>>> x.edad # Ejecuta la obtención
40
>>> x.trabajo = 'entrenador' # Asignación normal: sin llamada de setage
>>> x.trabajo 'entrenador' # Recuperación normal: sin llamada de captura

La clase equivalente basada en la sobrecarga del operador incurre en llamadas de método adicionales para
asignaciones a atributos que no se administran y necesita enrutar asignaciones de atributos
a través del diccionario de atributos para evitar bucles (o, para clases de nuevo estilo, al

Extensiones de clase de nuevo estilo | 1021


Machine Translated by Google

__setattr__ de la superclase de objeto para admitir mejor los atributos "virtuales" como las ranuras y
propiedades codificadas en otras clases):

>>> operadores de clase:


def __getattr__(self, nombre): if # En referencia indefinida
nombre == 'edad':
volver 40
más:
aumentar AttributeError (nombre)
def __setattr__(yo, nombre, valor): # En todas las asignaciones
print('establecer: %s %s' % (nombre, valor))
if nombre == 'edad':
self.__dict__['_edad'] = valor más: # O objeto.__setattr__()

self.__dict__[nombre] = valor

>>> x = operadores()
>>> x.edad # Ejecuta __getattr__
40
>>> x.edad = 41 # Ejecuta __setattr__
conjunto: 41 años
>>> x._edad # Definido: sin llamada __getattr__
41
>>> x.edad # Ejecuta __getattr__
40
>>> x.trabajo = 'entrenador' # Ejecuta __setattr__ de nuevo
conjunto: entrenador de trabajo
>>> x.trabajo # Definido: sin llamada __getattr__
'entrenador'

Las propiedades parecen una victoria para este ejemplo simple. Sin embargo, algunas aplicaciones de
__getattr__ y __setattr__ aún requieren interfaces más dinámicas o genéricas que
las propiedades proporcionan directamente.

Por ejemplo, en muchos casos no se puede determinar el conjunto de atributos que se admitirán
cuando la clase está codificada, y es posible que ni siquiera exista de forma tangible (p. ej., cuando
delegar referencias de atributos arbitrarios a un objeto envuelto/incrustado genéricamente). En
En tales contextos, generalmente es preferible un controlador de atributos genérico __getattr__ o __setattr__
con un nombre de atributo pasado. Debido a que tales controladores genéricos también pueden admitir
casos más simples, las propiedades son a menudo una extensión opcional y redundante, aunque una
que puede evitar llamadas adicionales en las asignaciones, y que algunos programadores pueden preferir
cuando sea aplicable.

Para más detalles sobre ambas opciones, esté atento al Capítulo 38 en la parte final de este
libro. Como veremos allí, también es posible codificar propiedades usando la función de símbolo @
Sintaxis del decorador: un tema que se presenta más adelante en este capítulo y una alternativa automática
y equivalente a la asignación manual en el ámbito de la clase:

propiedades de clase (objeto):


@property # Codificación de propiedades con decoradores: adelante
def age(self):
...
@age.setter

1022 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

def edad(auto, valor):


...

Sin embargo, para dar sentido a esta sintaxis del decorador, debemos seguir adelante.

__getattribute__ y descriptores: herramientas de atributos También en

el departamento de extensiones de clase, el método de sobrecarga del operador __getattribute__ , disponible


solo para clases de estilo nuevo, permite que una clase intercepte todas las referencias de atributos, no solo
referencias indefinidas. Esto lo hace más potente que su primo __get attr__ que usamos en la sección anterior,
pero también más complicado de usar: es propenso a bucles como __setattr__, pero de diferentes maneras.

Para objetivos de interceptación de atributos más especializados, además de propiedades y métodos de


sobrecarga de operadores, Python admite la noción de descriptores de atributos: clases con métodos __get__
y __set__ , asignados a atributos de clase y heredados por instancias, que interceptan accesos de lectura y
escritura a atributos específicos. Como vista previa, este es uno de los descriptores más simples que
probablemente encontrará:

>>> class AgeDesc(objeto):


def __get__(self, instancia, propietario): return 40
def __set__(self, instancia, valor): instancia._edad = valor

>>> descriptores de clase (objeto):


edad = AgeDesc()

>>> x = descriptores()
>>> x.edad 40 >>> # Ejecuta AgeDesc.__get__
x.edad = 42 >>> x._edad
42 # Ejecuta AgeDesc.__set__ #
Recuperación normal: sin llamada AgeDesc

Los descriptores tienen acceso al estado en instancias de ellos mismos así como de su clase de cliente, y en
cierto sentido son una forma más general de propiedades; de hecho, las propiedades son una forma simplificada
de definir un tipo específico de descriptor, uno que ejecuta funciones en el acceso. Los descriptores también se
utilizan para implementar la función de tragamonedas que conocimos anteriormente y otras herramientas de
Python.

Debido a que __getattribute__ y los descriptores son demasiado sustanciales para cubrirlos bien aquí,
remitiremos el resto de su cobertura, así como mucho más sobre las propiedades, al Capítulo 38 en la parte
final de este libro. También los emplearemos en ejemplos en el Capítulo 39 y estudiaremos cómo influyen en la
herencia en el Capítulo 40.

Otros cambios y extensiones de clase Como se

mencionó, también estamos posponiendo la cobertura de la súper incorporada, una extensión de clase adicional
importante de nuevo estilo que se basa en su MRO, hasta el final de este capítulo.
Sin embargo, antes de llegar allí, vamos a explorar cambios adicionales relacionados con las clases.

Extensiones de clase de nuevo estilo | 1023


Machine Translated by Google

y extensiones que no están necesariamente vinculadas a clases de nuevo estilo, pero que se introdujeron
aproximadamente al mismo tiempo: métodos estáticos y de clase, decoradores y más.

Muchos de los cambios y adiciones de características de las clases de nuevo estilo se integran con la
noción de tipos subclasificables mencionados anteriormente en este capítulo, porque los tipos
subclasificables y las clases de nuevo estilo se introdujeron junto con una fusión de la dicotomía tipo/
clase en Python 2.2 y más allá. Como hemos visto, en 3.X, esta fusión está completa: las clases ahora
son tipos y los tipos son clases, y las clases de Python todavía reflejan esa fusión conceptual y su
implementación.

Junto con estos cambios, Python también desarrolló un protocolo más coherente y generalizado para
codificar metaclases: clases que subclasifican el tipo de objeto, interceptan llamadas de creación de
clases y pueden proporcionar el comportamiento adquirido por las clases. En consecuencia, proporcionan
un enlace bien definido para la gestión y el aumento de objetos de clase. También son un tema avanzado
que es opcional para la mayoría de los programadores de Python, por lo que pospondremos más detalles
aquí. Volveremos a vislumbrar las metaclases más adelante en este capítulo junto con los decoradores
de clase, una característica cuyos roles a menudo se superponen, pero pospondremos su cobertura
completa hasta el Capítulo 40, en la parte final de este libro. Para nuestro propósito aquí, vamos a
pasar a un puñado de extensiones adicionales relacionadas con la clase.

Métodos estáticos y de clase


A partir de Python 2.2, es posible definir dos tipos de métodos dentro de una clase que se pueden llamar
sin una instancia: los métodos estáticos funcionan más o menos como funciones simples sin instancias
dentro de una clase, y los métodos de clase se pasan una clase en lugar de una instancia .
Ambos son similares a herramientas en otros lenguajes (p. ej., métodos estáticos de C++). Aunque esta
función se agregó junto con las clases de nuevo estilo discutidas en las secciones anteriores, los
métodos estáticos y de clase también funcionan para las clases clásicas.

Para habilitar estos modos de método, debe llamar a funciones integradas especiales denominadas
staticmethod y classmethod dentro de la clase, o invocarlas con la sintaxis especial de decoración
@name que veremos más adelante en este capítulo. Estas funciones son necesarias para habilitar estos
modos de métodos especiales en Python 2.X y generalmente se necesitan en 3.X. En Python 3.X, no se
requiere una declaración de método estático para los métodos sin instancia llamados solo a través de un
nombre de clase, pero aún se requiere si dichos métodos se llaman a través de instancias.

¿Por qué los métodos especiales?

Como hemos aprendido, al método de una clase normalmente se le pasa un objeto de instancia en su
primer argumento, para que sirva como sujeto implícito de la llamada al método; ese es el "objeto" en la
"programación orientada a objetos". Hoy, sin embargo, hay dos formas de modificar este modelo. Antes
de explicar qué son, debo explicar por qué esto podría ser importante para usted.

A veces, los programas necesitan procesar datos asociados con clases en lugar de instancias.
Considere realizar un seguimiento del número de instancias creadas a partir de una clase, o mantener

1024 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

una lista de todas las instancias de una clase que están actualmente en la memoria. Este tipo de
información y su procesamiento están asociados con la clase más que con sus instancias. Es decir, la
información generalmente se almacena en la clase misma y se procesa aparte de cualquier instancia.

Para tales tareas, las funciones simples codificadas fuera de una clase a menudo pueden ser suficientes,
ya que pueden acceder a los atributos de la clase a través del nombre de la clase, tienen acceso a los
datos de la clase y nunca requieren acceso a una instancia. Sin embargo, para asociar mejor dicho código
con una clase y permitir que dicho procesamiento se personalice con herencia como de costumbre, sería
mejor codificar este tipo de funciones dentro de la propia clase. Para que esto funcione, necesitamos
métodos en una clase que no se pasan, y no esperamos, un argumento de autoinstancia
mento

Python admite tales objetivos con la noción de métodos estáticos : funciones simples sin argumentos
propios que están anidadas en una clase y están diseñadas para trabajar en atributos de clase en lugar de
atributos de instancia. Los métodos estáticos nunca reciben un autoargumento automático , ya sea que se
llamen a través de una clase o una instancia. Por lo general, realizan un seguimiento de la información que
abarca todas las instancias, en lugar de proporcionar comportamiento para las instancias.

Aunque se usa con menos frecuencia, Python también admite la noción de métodos de clase: métodos de
una clase a los que se les pasa un objeto de clase en su primer argumento en lugar de una instancia,
independientemente de si se llaman a través de una instancia o una clase. Dichos métodos pueden
acceder a datos de clase a través de su argumento de clase, lo que hemos llamado self hasta ahora,
incluso si se llama a través de una instancia. Los métodos normales, ahora conocidos en los círculos
formales como métodos de instancia, aún reciben una instancia de sujeto cuando se les llama; los métodos
estáticos y de clase no.

Métodos Estáticos en 2.X y 3.X


El concepto de métodos estáticos es el mismo en Python 2.X y 3.X, pero sus requisitos de implementación
han evolucionado un poco en Python 3.X. Dado que este libro cubre ambas versiones, necesito explicar
las diferencias en los dos modelos subyacentes antes de llegar al código.

Realmente, ya comenzamos esta historia en el capítulo anterior, cuando exploramos la noción de métodos
no ligados. Recuerde que tanto Python 2.X como 3.X siempre pasan una instancia a un método que se
llama a través de una instancia. Sin embargo, Python 3.X trata los métodos obtenidos directamente de una
clase de manera diferente a 2.X, una diferencia en las líneas de Python que no tiene nada que ver con las
clases de nuevo estilo: • Tanto Python 2.X como 3.X producen un método vinculado cuando se obtiene un

método
a través de una instancia.

• En Python 2.X, obtener un método de una clase produce un método independiente, que
no se puede llamar sin pasar manualmente una instancia.

• En Python 3.X, obtener un método de una clase produce una función simple, que
se puede llamar normalmente sin instancia presente.

Métodos estáticos y de clase | 1025


Machine Translated by Google

En otras palabras, los métodos de clase de Python 2.X siempre requieren que se pase una instancia, ya sea
que se llamen a través de una instancia o una clase. Por el contrario, en Python 3.X estamos obligados a pasar
una instancia a un método solo si el método espera una: los métodos que no incluyen un argumento de instancia
pueden llamarse a través de la clase sin pasar una instancia. Es decir, 3.X permite funciones simples en una
clase, siempre que no esperen y no se les pase un argumento de instancia. El efecto neto es que:

• En Python 2.X, siempre debemos declarar un método como estático para llamarlo sin
una instancia, ya sea que se llame a través de una clase o una instancia.

• En Python 3.X, no necesitamos declarar estos métodos como estáticos si se llamarán solo a través de una
clase, pero debemos hacerlo para llamarlos a través de una instancia.

Para ilustrar, supongamos que queremos usar atributos de clase para contar cuántas instancias se generan a
partir de una clase. El siguiente archivo, spam.py, hace un primer intento: su clase tiene un contador almacenado
como un atributo de clase, un constructor que aumenta el contador en uno cada vez que se crea una nueva
instancia y un método que muestra el valor del contador.
Recuerde, los atributos de clase son compartidos por todas las instancias. Por lo tanto, almacenar el contador
en el objeto de clase en sí mismo garantiza que abarque efectivamente todas las instancias:

class Spam:
numInstances = 0
def __init__(self):
Spam.numInstances = Spam.numInstances + 1
def printNumInstances(): print("Número de instancias
creadas: %s" % Spam.numInstances)

El método printNumInstances está diseñado para procesar datos de clases, no datos de instancias; se trata de
todas las instancias, no de una en particular. Por eso, queremos poder llamarlo sin tener que pasar una instancia.
De hecho, no queremos crear una instancia para obtener la cantidad de instancias, ¡porque esto cambiaría la
cantidad de instancias que estamos tratando de obtener! En otras palabras, queremos un método “estático”
desinteresado.

Sin embargo, si printNumInstances de este código funciona o no, depende de qué Python use y de qué forma
llame al método: a través de la clase o a través de una instancia. En 2.X, las llamadas a una función de método
desinteresado a través de la clase y las instancias fallan (como de costumbre, he omitido un texto de error aquí
por espacio): C:\code> c:\python27\python >> > de spam import Spam >>> a = Spam() >>> b = Spam() >>>

c = Spam()

# No se puede llamar a métodos de clase independientes en

2.X # Los métodos esperan un objeto propio por defecto

>>> Spam.printNumInstances()
TypeError: el método no vinculado printNumInstances() debe llamarse con la instancia de Spam
como primer argumento (no obtuvo nada en su lugar) >>> a.printNumInstances()

TypeError: printNumInstances() no toma argumentos (1 dado)

El problema aquí es que los métodos de instancia independientes no son exactamente lo mismo que las
funciones simples en 2.X. Aunque no hay argumentos en el encabezado def , el método

1026 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

todavía espera que se pase una instancia cuando se llama, porque la función está asociada con una clase. En
Python 3.X, las llamadas a métodos desinteresados realizados a través de clases funcionan, pero las llamadas
desde instancias fallan: C:\code> c:\python33\python >>> from spam import Spam >>> a = Spam() > >> b =

Correo no deseado() >>> c = Correo no deseado()

# Puede llamar a funciones en clase en 3.X # Las


llamadas a través de instancias aún se pasan a sí mismas

>>> Spam.printNumInstances() # Difiere en 3.X


Número de instancias creadas: 3
>>> a.printNumInstances()
TypeError: printNumInstances() toma 0 argumentos posicionales pero se le dio 1

Es decir, las llamadas a métodos sin instancias como printNumInstances realizadas a través de la
clase fallan en Python 2.X pero funcionan en Python 3.X. Por otro lado, las llamadas realizadas a
través de una instancia fallan en ambos Pythons, porque una instancia se pasa automáticamente
a un método que no tiene un argumento para recibirla:
Spam.printNumInstances() # Falla en 2.X, funciona en 3.X #
instancia.printNumInstances() Falla tanto en 2.X como en 3.X (a menos que sea estático)

Si puede usar 3.X y seguir llamando a métodos desinteresados solo a través de clases, ya tiene
una función de método estático. Sin embargo, para permitir que los métodos desinteresados se
llamen a través de clases en 2.X y a través de instancias tanto en 2.X como en 3.X, debe adoptar
otros diseños o marcar de alguna manera dichos métodos como especiales. Veamos ambas
opciones a la vez.

Alternativas de métodos estáticos

Aparte de marcar un método desinteresado como especial, a veces puede lograr resultados
similares con diferentes estructuras de codificación. Por ejemplo, si solo desea llamar a funciones
que acceden a miembros de la clase sin una instancia, quizás la idea más simple sea usar
funciones normales fuera de la clase, no métodos de clase. De esta forma, no se espera una
instancia en la llamada. La siguiente mutación de spam.py ilustra y funciona igual en Python 3.X y
2.X:
def printNumInstances():
print("Número de instancias creadas: %s" % Spam.numInstances)

clase Spam:
numInstances = 0
def __init__(auto):
Spam.numInstances = Spam.numInstances + 1

C:\code> c:\python33\python
>>> importar spam >>> a =
spam.Spam() >>> b =
spam.Spam() >>> c =
spam.Spam() >>>
spam .imprimirNumInstancias() # Pero la función puede estar demasiado alejada

Métodos estáticos y de clase | 1027


Machine Translated by Google

Número de instancias creadas: 3 # Y no se puede cambiar por herencia


>>> spam.Spam.numInstances
3

Debido a que el nombre de la clase es accesible para la función simple como una variable global, esto
funciona bien. Además, tenga en cuenta que el nombre de la función se vuelve global, pero solo para este
único módulo; no chocará con nombres en otros archivos del programa.

Antes de los métodos estáticos en Python, esta estructura era la prescripción general. Debido a que
Python ya proporciona módulos como una herramienta de partición de espacios de nombres, se podría
argumentar que normalmente no hay necesidad de empaquetar funciones en clases a menos que
implementen el comportamiento de los objetos. Las funciones simples dentro de módulos como el que se
muestra aquí hacen mucho de lo que podrían hacer los métodos de clase sin instancia, y ya están
asociados con la clase porque viven en el mismo módulo.

Desafortunadamente, este enfoque es aún menos que ideal. Por un lado, agrega al alcance de este
archivo un nombre adicional que se usa solo para procesar una sola clase. Por otra parte, la función está
asociada mucho menos directamente con la clase por estructura; de hecho, su definición podría estar a
cientos de líneas de distancia. Quizás peor, las funciones simples como esta no se pueden personalizar
por herencia, ya que viven fuera del espacio de nombres de una clase: las subclases no pueden reemplazar
o extender directamente dicha función al redefinirla.

Podríamos tratar de hacer que este ejemplo funcione de una manera neutral a la versión usando un
método normal y siempre llamándolo a través de (o con) una instancia, como de costumbre:

clase Spam:
numInstances = 0
def __init__(self):
Spam.numInstances = Spam.numInstances +
1 def printNumInstances(self):
print("Número de instancias creadas: %s" % Spam.numInstances)

C:\code> c:\python33\python
>>> from spam import Spam
>>> a, b, c = Spam(), Spam(), Spam()
>>> a.printNumInstances()
Número de instancias creadas: 3
>>> Spam.printNumInstances(a)
Número de instancias creadas: 3
>>> Spam().printNumInstances() # ¡Pero ir a buscar el contador cambia el contador!
Número de instancias creadas: 4

Desafortunadamente, como se mencionó anteriormente, este enfoque es completamente inviable si no


tenemos una instancia disponible, y hacer una instancia cambia los datos de la clase, como se ilustra en
la última línea aquí. Una mejor solución sería marcar de alguna manera un método dentro de una clase
para que nunca requiera una instancia. La siguiente sección muestra cómo.

Uso de métodos estáticos y de clase Hoy

en día, existe otra opción para codificar funciones simples asociadas con una clase que pueden llamarse
a través de la clase o sus instancias. A partir de Python 2.2, podemos codificar

1028 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

clases con métodos estáticos y de clase, ninguno de los cuales requiere que se pase un argumento de instancia
cuando se invoca. Para designar tales métodos, las clases llaman a las funciones incorporadas staticmethod y
classmethod, como se indicó en la discusión anterior de las clases de nuevo estilo. Ambos marcan un objeto de
función como especial, es decir, que no requiere instancia si es estático y requiere un argumento de clase si es un
método de clase. Por ejemplo, en el archivo bothmethods.py (que unifica la impresión 2.X y 3.X con listas, aunque
las visualizaciones todavía varían ligeramente para las clases clásicas 2.X):

# Archivo ambosmétodos.py

Métodos de clase:
def imeth(self, x): print([self, # Método de instancia normal: pasó un auto
x])

def algo(x): print([x]) # Estático: ninguna instancia pasó

def cmeth(cls, x): print([cls, # Clase: obtiene clase, no instancia


x])

smeth = staticmethod(smeth) # Hacer que smeth sea un método estático (o @: adelante) cmeth =
classmethod(cmeth) # Hacer de cmeth un método de clase (o @: adelante)

Observe cómo las dos últimas asignaciones en este código simplemente reasignan (es decir, vuelven a vincular) los
nombres de método smeth y cmeth. Los atributos son creados y cambiados por cualquier asignación en una
declaración de clase , por lo que estas asignaciones finales simplemente sobrescriben las asignaciones realizadas
anteriormente por los defs. Como veremos en unos momentos, la sintaxis @ especial funciona aquí como una
alternativa a esto, al igual que lo hace con las propiedades, pero tiene poco sentido a menos que primero comprenda
el formulario de asignación aquí que automatiza.

Técnicamente, Python ahora admite tres tipos de métodos relacionados con clases, con diferentes protocolos de
argumentos:

• Métodos de instancia, se pasa un objeto de instancia propia (predeterminado) •

Métodos estáticos, no se pasa ningún objeto adicional (a través de un método

estático) • Métodos de clase, se pasa un objeto de clase (a través de un método de clase e inherente a las metaclases)

Además, Python 3.X amplía este modelo al permitir que funciones simples en una clase cumplan la función de
métodos estáticos sin protocolo adicional, cuando se llaman solo a través de un objeto de clase. A pesar de su
nombre, el módulo bothmethods.py ilustra los tres tipos de métodos, por lo que vamos a ampliarlos a su vez.

Los métodos de instancia son el caso normal y predeterminado que hemos visto en este libro. Un método de

instancia siempre debe llamarse con un objeto de instancia. Cuando lo llama a través de una instancia, Python pasa
la instancia al primer argumento (más a la izquierda) automáticamente; cuando lo llama a través de una clase, debe
pasar la instancia manualmente:

>>> from bothmethods import Methods # Métodos de instancia normal >>> obj = Methods()
# Invocable a través de instancia o clase
>>> obj.imeth(1) [<objeto ambos métodos. Métodos en 0x0000000002A15710>, 1]

Métodos estáticos y de clase | 1029


Machine Translated by Google

>>> Métodos.imeth(obj, 2)
[<objeto ambos métodos. Métodos en 0x0000000002A15710>, 2]

Los métodos estáticos, por el contrario, se llaman sin un argumento de instancia. A diferencia de simple
funciones fuera de una clase, sus nombres son locales a los ámbitos de las clases en las que
están definidos y pueden consultarse por herencia. Las funciones sin instancias pueden ser
llamado a través de una clase normalmente en Python 3.X, pero nunca por defecto en 2.X. Utilizando el
El método estático incorporado permite que dichos métodos también se llamen a través de una instancia en 3.X
y a través de una clase y una instancia en Python 2.X (es decir, la primera de las siguientes
funciona en 3.X sin staticmethod, pero el segundo no):

>>> Métodos.smeth(3) [3] >>> # Método estático: llamar a través de la clase


obj.smeth(4) [4] # Ninguna instancia pasada o esperada
# Método estático: llamada a través de instancia
# Instancia no aprobada

Los métodos de clase son similares, pero Python pasa automáticamente la clase (no una instancia)
en el primer argumento de un método de clase (extremo izquierdo), ya sea que se llame a través de una clase o
una instancia:

>>> Métodos.cmeth(5) [<clase # Método de clase: llamar a través de la clase


'ambosmétodos.Métodos'>, 5] >>> obj.cmeth(6) # Se convierte en cmeth(Métodos, 5)
[<clase 'ambosmétodos.Métodos'>, 6] # Método de clase: llamada a través de instancia
# Se convierte en cmeth(Métodos, 6)

En el Capítulo 40, también encontraremos que los métodos de metaclase (un tipo de método único, avanzado
y técnicamente distinto) se comportan de manera similar a los métodos de clase declarados explícitamente.
estamos explorando aquí.

Conteo de instancias con métodos estáticos


Ahora, dadas estas funciones integradas, aquí está el método estático equivalente al ejemplo de conteo de
instancias de esta sección: marca el método como especial, por lo que nunca se pasará
una instancia automáticamente:

correo no deseado de clase:

numInstances = 0 def # Usar método estático para datos de clase


__init__(self):
Spam.numInstancias += 1
def imprimirNumInstancias():
print("Número de instancias: %s" % Spam.numInstances)
imprimirNumInstancias = método estático(imprimirNumInstancias)

Usando el método estático incorporado, nuestro código ahora permite llamar al método desinteresado
a través de la clase o cualquier instancia de la misma, tanto en Python 2.X como en 3.X:

>>> de spam_static importar Spam


>>> a = Correo basura()
>>> b = correo basura()
>>> c = correo basura()
>>> Spam.printNumInstances() # Llamar como función simple
Número de instancias: 3

1030 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

>>> a.imprimirNumInstancias() # Argumento de instancia no aprobado


Número de instancias: 3

En comparación con simplemente mover printNumInstances fuera de la clase, como se indicó


anteriormente, esta versión requiere una llamada de método estático adicional ( o una línea @ que
veremos más adelante). Sin embargo, también localiza el nombre de la función en el ámbito de la
clase (para que no entre en conflicto con otros nombres en el módulo); mueve el código de función
más cerca de donde se usa (dentro de la declaración de clase ); y permite que las subclases
personalicen el método estático con herencia, un enfoque más conveniente y poderoso que importar
funciones de los archivos en los que se codifican las superclases. La siguiente subclase y la nueva
sesión de prueba ilustran (asegúrese de iniciar una nueva sesión después de cambiar los archivos,
para que sus importaciones carguen la última versión del archivo):

clase Sub(Spam): def


printNumInstances(): # Anular un método estático
print("Cosas extra...") # Pero vuelve a llamar al original
Spam.printNumInstances()
imprimirNumInstancias = método estático(imprimirNumInstancias)

>>> from spam_static import Spam, Sub >>> a =


Sub() >>> b = Sub() >>> a.printNumInstances()

# Llamar desde instancia de subclase


Cosas adicionales...
Número de instancias: 2
>>> Sub.imprimirNumInstancias() # Llamar desde la propia subclase
Cosas adicionales...
Número de instancias: 2 >>>
Spam.printNumInstances() # Llamar versión original
Número de instancias: 2

Además, las clases pueden heredar el método estático sin redefinirlo: se ejecuta sin una instancia, independientemente de dónde se

defina en un árbol de clases: >>> class Other(Spam): pass

# Heredar método estático palabra por palabra

>>> c = Otro() >>>


c.imprimirNumInstancias()
Número de instancias: 3

Observe cómo esto también aumenta el contador de instancias de la superclase , porque su


constructor se hereda y se ejecuta, un comportamiento que comienza a invadir el tema de la siguiente
sección.

Contar instancias con métodos de clase

Curiosamente, un método de clase puede hacer un trabajo similar aquí: el siguiente tiene el mismo
comportamiento que la versión del método estático enumerado anteriormente, pero usa un método de
clase que recibe la clase de la instancia en su primer argumento. En lugar de codificar el nombre de la
clase, el método de clase usa el objeto de clase pasado automáticamente de forma genérica:

Métodos estáticos y de clase | 1031


Machine Translated by Google

clase Spam:
numInstances = 0 def # Use el método de clase en lugar de estático
__init__(self):
Spam.numInstances += 1 def
printNumInstances(cls):
print("Número de instancias: %s" % cls.numInstances)
imprimirNumInstancias = classmethod(imprimirNumInstancias)

Esta clase se usa de la misma manera que las versiones anteriores, pero su método printNumInstances
recibe la clase Spam , no la instancia, cuando se llama tanto desde la clase como desde una instancia:

>>> from spam_class import Spam >>> a, b


= Spam(), Spam() >>> a.printNumInstances()
# Pasa la clase al primer argumento
Número de instancias: 2 >>>
Spam.printNumInstances() # También pasa la clase al primer argumento
Número de instancias: 2

Sin embargo, cuando utilice métodos de clase, tenga en cuenta que reciben la clase más específica (es
decir, la más baja) del sujeto de la llamada. Esto tiene algunas implicaciones sutiles al intentar actualizar
los datos de la clase a través de la clase pasada. Por ejemplo, si en el módulo spam_class.py
subclasificamos para personalizar como antes, aumente Spam.printNumInstances para mostrar también
su argumento cls e inicie una nueva sesión de prueba:
class Spam:
numInstances = 0 def # Clase de seguimiento pasada
__init__(self):
Spam.numInstances += 1 def
printNumInstances(cls): print("Número
de instancias: %s %s" % (cls.numInstances, cls)) printNumInstances =
classmethod(printNumInstances )

clase Sub(Spam): def


printNumInstances(cls): # Anular un método de clase
print("Cosas extra...", cls) # Pero vuelve a llamar al original
Spam.printNumInstances()
imprimirNumInstancias = classmethod(imprimirNumInstancias)

clase Otro(Spam): pasar # Heredar método de clase palabra por palabra

La clase más baja se pasa cada vez que se ejecuta un método de clase, incluso para las subclases que
no tienen métodos de clase propios:

>>> from spam_class import Spam, Sub, Otro >>> x = Sub()


>>> y = Spam() >>> x.printNumInstances()

# Llamar desde instancia de subclase


Cosas extra... <class 'spam_class.Sub'> Número de
instancias: 2 <class 'spam_class.Spam'> >>> Sub.printNumInstances()
# Llamar desde la propia subclase
Cosas extra... <class 'spam_class.Sub'> Número de
instancias: 2 <class 'spam_class.Spam'> >>> y.printNumInstances()
# Llamar desde una instancia de superclase
Número de instancias: 2 <clase 'spam_class.Spam'>

1032 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

En la primera llamada aquí, se realiza una llamada de método de clase a través de una instancia de la subclase
Sub , y Python pasa la clase más baja, Sub, al método de clase. Todo está bien en este caso, dado que la
redefinición del método de Sub llama explícitamente a la versión de la superclase Spam , el método de la
superclase en Spam recibe su propia clase en su primer argumento. Pero observe lo que sucede con un objeto
que hereda el método de clase palabra por palabra:

>>> z = Otro() >>> # Llamar desde la instancia del sub inferior


z.imprimirNumInstancias()
Número de instancias: 3 <clase 'spam_class.Other'>

Esta última llamada aquí pasa Otro al método de clase de Spam . Esto funciona en este ejemplo porque al buscar
el contador se encuentra en Spam por herencia. Sin embargo, si este método intentara asignar a los datos de la
clase pasada, actualizaría Otro, ¡ no Spam! En este caso específico, probablemente sea mejor que Spam codifique
su propio nombre de clase para actualizar sus datos si también significa contar instancias de todas sus subclases,
en lugar de confiar en el argumento de clase pasado.

Contando instancias por clase con métodos de clase

De hecho, debido a que los métodos de clase siempre reciben la clase más baja en el árbol de una instancia:

• Los métodos estáticos y los nombres de clases explícitos pueden ser una mejor solución para procesar datos
locales de una clase.

• Los métodos de clase pueden ser más adecuados para procesar datos que pueden diferir para cada clase
en una jerarquía.

El código que necesita administrar contadores de instancias por clase , por ejemplo, podría ser mejor aprovechando
los métodos de clase. A continuación, la superclase de nivel superior utiliza un método de clase para administrar
la información de estado que varía y se almacena en cada clase del árbol, similar en espíritu a la forma en que los
métodos de instancia administran la información de estado que varía según la instancia de clase:

clase Spam:
numInstances = 0
def count(cls): # Contadores de instancias por clase #
cls.numInstances += 1 def cls es la clase más baja por encima de la instancia

__init__(self): self.count() count


= classmethod(count) # Pasa self.__class__ para contar

subclase (correo no deseado):


númInstancias = 0
def __init__(auto): # Redefine __init__
Spam.__init__(auto)

clase Otro (correo no deseado): # Hereda __init__


númInstancias = 0

>>> from spam_class2 import Spam, Sub, Otro >>> x


= Spam() >>> y1, y2 = Sub(), Sub()

Métodos estáticos y de clase | 1033


Machine Translated by Google

>>> z1, z2, z3 = Otro(), Otro(), Otro() >>>


x.numInstancias, y1.numInstancias, z1.numInstancias (1, 2, 3) # ¡Datos por clase!

>>> Spam.numInstances, Sub.numInstances, Other.numInstances (1, 2, 3)

Los métodos estáticos y de clase tienen roles avanzados adicionales, que refinaremos aquí; vea otros recursos
para más casos de uso. Sin embargo, en las versiones recientes de Python, las designaciones de métodos
estáticos y de clase se han vuelto aún más simples con el advenimiento de la sintaxis de decoración de
funciones , una forma de aplicar una función a otra que tiene roles mucho más allá del caso de uso del método
estático que fue su motivación inicial. Esta sintaxis también nos permite aumentar las clases en Python 2.X y
3.X, para inicializar datos como el contador numInstances en el último ejemplo, por ejemplo. La siguiente sección
explica cómo.

Para una posdata sobre los tipos de métodos de Python, asegúrese de estar atento a la
cobertura de los métodos de metaclase en el Capítulo 40; debido a que están diseñados
para procesar una clase que es una instancia de una metaclase, resultan ser muy similares
a los métodos de clase definidos. aquí, pero no requieren declaración de método de clase ,
y se aplican solo al reino de la metaclase en la sombra.

Decoradores y Metaclases: Parte 1


Debido a que la técnica de llamada al método estático y al método de clase descrita en la sección anterior
inicialmente parecía oscura para algunos observadores, finalmente se agregó un dispositivo para simplificar la
operación. Los decoradores de Python , similares a la noción y la sintaxis de las anotaciones en Java, abordaron
esta necesidad específica y proporcionaron una herramienta general para agregar lógica que administra
funciones y clases, o llamadas posteriores a ellas.

Esto se denomina "decoración", pero en términos más concretos es realmente solo una forma de ejecutar pasos
de procesamiento adicionales en el momento de la definición de funciones y clases con sintaxis explícita. Viene
en dos sabores:

• Decoradores de funciones: la entrada inicial en este conjunto, agregada en Python 2.4: definiciones de
funciones de aumento. Especifican modos de operación especiales tanto para funciones simples como
para métodos de clases envolviéndolos en una capa adicional de lógica implementada como otra función,
generalmente llamada metafunción.

• Los decoradores de clase, una extensión posterior, agregada en Python 2.6 y 3.0, aumentan las definiciones
de clase. Hacen lo mismo con las clases, añadiendo soporte para la gestión de objetos completos y sus
interfaces. Aunque quizás sea más simple, a menudo se superponen en roles con metaclases.

Los decoradores de funciones resultan ser herramientas muy generales: son útiles para agregar muchos tipos
de lógica a las funciones además de los casos de uso de métodos estáticos y de clase. Por ejemplo, se pueden
usar para aumentar las funciones con código que registra las llamadas que se les hacen, verifica los tipos de
argumentos pasados durante la depuración, etc. Los decoradores de funciones se pueden usar para administrar
funciones en sí mismas o llamadas posteriores a ellas. En este último modo,

1034 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

Los decoradores de funciones son similares al patrón de diseño de delegación que exploramos en el Capítulo
31, pero están diseñados para aumentar una llamada de método o función específica, no una interfaz de objeto
completa.

Python proporciona algunos decoradores de funciones incorporados para operaciones tales como marcar
métodos estáticos y de clase y definir propiedades (como se esbozó anteriormente, la propiedad incorporada
funciona como un decorador automáticamente), pero los programadores también pueden codificar sus propios
decoradores arbitrarios. Aunque no están estrictamente vinculados a las clases, los decoradores de funciones
definidos por el usuario a menudo se codifican como clases para guardar las funciones originales para su envío
posterior, junto con otros datos como información de estado.

Esto demostró ser un gancho tan útil que se amplió en Python 2.6, 2.7 y 3.X: los decoradores de clases también
aportan aumento a las clases y están más directamente vinculados al modelo de clase. Al igual que sus
cohortes de funciones, los decoradores de clases pueden administrar las clases ellos mismos o las llamadas
de creación de instancias posteriores y, a menudo, emplean la delegación en este último modo. Como veremos,
sus funciones también suelen superponerse con las metaclases; cuando lo hacen, los decoradores de clase
más nuevos pueden ofrecer una forma más ligera de lograr los mismos objetivos.

Conceptos básicos del decorador de funciones

Sintácticamente, un decorador de funciones es una especie de declaración en tiempo de ejecución sobre la


función que sigue. Un decorador de función está codificado en una línea justo antes de la declaración de
definición que define una función o método. Consiste en el símbolo @ , seguido de lo que llamamos una
metafunción: una función (u otro objeto invocable) que administra otra función. Los métodos estáticos desde
Python 2.4, por ejemplo, se pueden codificar con una sintaxis de decorador como esta:

clase C:
@staticmethod # Sintaxis de decoración de funciones
def meth():
...

Internamente, esta sintaxis tiene el mismo efecto que el siguiente: pasar la función a través del decorador y
asignar el resultado al nombre original:
clase C:
def met():
...
meth = método estático(meth) # Equivalente de reenlace de nombre

La decoración vuelve a enlazar el nombre del método con el resultado del decorador. El efecto neto es que
llamar más tarde al nombre de la función del método activa primero el resultado de su decorador de método
estático . Debido a que un decorador puede devolver cualquier tipo de objeto, esto le permite insertar una capa
de lógica para que se ejecute en cada llamada. La función de decorador es libre de devolver la función original
en sí misma o un nuevo objeto proxy que guarda la función original pasada al decorador para que se invoque
indirectamente después de que se ejecute la capa de lógica adicional.

Con esta adición, aquí hay una mejor manera de codificar nuestro ejemplo de método estático de la sección
anterior en Python 2.X o 3.X:

Decoradores y Metaclases: Parte 1 | 1035


Machine Translated by Google

clase Spam:
numInstances = 0 def
__init__(self):
Spam.numInstances = Spam.numInstances + 1

@staticmethod def
printNumInstances(): print("Número
de instancias creadas: %s" % Spam.numInstances)

>>> from spam_static_deco import Spam >>> a =


Spam() >>> b = Spam() >>> c = Spam()

>>> Spam.printNumInstances() # Las llamadas de clases e instancias funcionan


Número de instancias creadas: 3 >>>
a.printNumInstances()
Número de instancias creadas: 3

Debido a que también aceptan y devuelven funciones, las funciones incorporadas classmethod y
property pueden usarse como decoradores de la misma manera, como en la siguiente mutación del
anterior bothmethods.py:
# Archivo ambosmétodos_decoradores.py

Métodos de clase (objeto): def # objeto necesario en 2.X para establecer


imeth (self, x): print ([self, x]) propiedades # Método de instancia normal: pasó un auto

@staticmethod def
smeth(x): print([x]) # Estático: ninguna instancia pasó

@classmethod
def cmeth(cls, x): print([cls, # Clase: obtiene clase, no instancia
x])

@property # Propiedad: calculada al buscar


def nombre(self):
return 'Bob' + self.__clase__.__nombre__

>>> from bothmethods_decorators import Methods >>> obj =


Methods() >>> obj.imeth(1) [<bothmethods_decorators.Methods
object at 0x0000000002A256A0>, 1] >>> obj.smeth(2) [2] >> >
obj.cmeth(3) [<clase 'ambosmétodos_decoradores.Métodos'>, 3] >>> obj.name 'Métodos Bob'

Tenga en cuenta que staticmethod y sus parientes aquí todavía son funciones integradas; se pueden
usar en la sintaxis de decoración, solo porque toman una función como argumento y devuelven un
invocable al que se puede volver a vincular el nombre de la función original. De hecho, cualquier tal

1036 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

La función se puede usar de esta manera, incluso las funciones definidas por el usuario las codificamos nosotros mismos,
como se explica en la siguiente sección.

Un primer vistazo a los decoradores de funciones definidos por el usuario

Aunque Python proporciona un puñado de funciones integradas que se pueden usar como decoradores, también podemos
escribir nuestros propios decoradores personalizados. Debido a su amplia utilidad, vamos a dedicar un capítulo completo a
codificar decoradores en la parte final de este libro. Sin embargo, como un ejemplo rápido, veamos un decorador simple
definido por el usuario en el trabajo.

Recuerde del Capítulo 30 que el método de sobrecarga del operador __call__ implementa una interfaz de llamada de función
para instancias de clase. El siguiente código usa esto para definir una clase de proxy de llamada que guarda la función
decorada en la instancia y detecta las llamadas al nombre original. Debido a que esta es una clase, también tiene información
de estado: un contador de llamadas realizadas:

rastreador de clase:

def __init__(self, func): self.calls = 0 # Recordar original, contador de inicio


self.func = func def __call__(self,
*args): self.calls += 1 print('llamar

a %s a %s' % (self.calls, # En llamadas posteriores: agregar lógica, ejecutar original


self .func.__name__)) return

self.func(*args)

@trazador # Igual que el spam = rastreador(spam)


def spam(a, b, c): # Envuelve el spam en un objeto decorador
devuelve a + b + c

imprimir (correo no deseado (1, # Realmente llama al objeto contenedor del rastreador
2, 3)) imprimir (correo no deseado ('a', 'b', 'c')) # Invoca __call__ en clase

Debido a que la función de spam se ejecuta a través del decorador del rastreador , cuando se llama al nombre de spam
original , en realidad activa el método __call__ en la clase. Este método cuenta y registra la llamada y luego la envía a la
función envuelta original. Tenga en cuenta cómo se usa la sintaxis del argumento *name para empaquetar y desempaquetar
los argumentos pasados; debido a esto, este decorador se puede usar para envolver cualquier función con cualquier cantidad
de argumentos posicionales.

El efecto neto, nuevamente, es agregar una capa de lógica a la función de spam original . Este es el resultado de la secuencia
de comandos 3.X y 2.X: la primera línea proviene de la clase rastreadora y la segunda proporciona el valor de retorno de la
función de correo no deseado en sí:

c:\code> python tracer1.py llamada 1


para spam
6
llamar 2 para spam
abc

Siga el código de este ejemplo para obtener más información. Tal como está, este decorador funciona para cualquier función
que tome argumentos posicionales, pero no maneja argumentos de palabras clave .

Decoradores y Metaclases: Parte 1 | 1037


Machine Translated by Google

mentos, y no puede decorar funciones de método de nivel de clase (en resumen, para los métodos, su
__llamada__ se pasaría solo a una instancia de rastreador ). Como veremos en la Parte VIII, hay una variedad
de formas de codificar decoradores de funciones, incluidas declaraciones de definición anidadas ; algunas de
las alternativas se adaptan mejor a los métodos que la versión que se muestra aquí.

Por ejemplo, mediante el uso de funciones anidadas con ámbitos adjuntos para el estado, en lugar de
instancias de clase invocables con atributos, los decoradores de funciones a menudo también se pueden
aplicar de manera más amplia a los métodos de nivel de clase. Pospondremos los detalles completos sobre
esto, pero aquí hay un breve vistazo a este modelo de codificación basado en cierres ; utiliza atributos de
función para el estado del contador para la portabilidad, pero podría aprovechar variables y no locales en su
lugar solo en 3.X: def tracer(func): # Recuerde laposteriores
definición original oncall(*args):
oncall.calls += 1 print # En llamadas
('llamar a %s a %s' %
(oncall.calls, func.__name__)) return func(*args)

oncall.calls = 0
volver oncall

clase C:
@tracer
def spam(self,a, b, c): devuelve a + b + c

x = C()
print(x.correo no deseado(1,
2, 3)) print(x.correo no deseado('a', 'b', 'c')) # Misma salida que tracer1 (en tracer2.py)

Un primer vistazo a los decoradores de clase y las metaclases

Los decoradores de funciones resultaron ser tan útiles que Python 2.6 y 3.0 ampliaron el modelo, lo que
permitió que los decoradores se aplicaran tanto a las clases como a las funciones. En resumen, los
decoradores de clases son similares a los decoradores de funciones, pero se ejecutan al final de una
declaración de clase para volver a vincular un nombre de clase a un invocable. Como tal, se pueden usar para
administrar clases justo después de que se crean, o insertar una capa de lógica contenedora para administrar
instancias cuando se crean más tarde. Simbólicamente, la estructura del código:

def decorador(aClass): ...

@decorador # Sintaxis de decoración de clase


clase C: ...

se asigna al siguiente equivalente: def


decorador(aClass): ...

clase C: ... # Equivalente de reenlace de nombre


C = decorador (C)

El decorador de clases es libre de aumentar la clase en sí o devolver un objeto proxy que intercepte llamadas
de construcción de instancias posteriores. Por ejemplo, en el código de la sección “Contar instancias por clase
con métodos de clase” en la página 1033, podríamos usar esto

1038 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

gancho para aumentar automáticamente las clases con contadores de instancias y cualquier otro dato
requerido:

def cuenta(unaClase):
aClass.numInstances = 0
devolver una clase # Devuelve la clase en sí misma, en lugar de un contenedor

@contar
clase Spam: ... # Igual que Spam = cuenta(Spam)

@contar
subclase (correo no deseado): ... # numInstances = 0 no es necesario aquí

@contar
clase Otro(Spam): ...

De hecho, tal como está codificado, este decorador se puede aplicar a clases o funciones; felizmente devuelve
el objeto se define en cualquier contexto después de inicializar el atributo del objeto:

@contar
def spam(): pasar # Me gusta spam = recuento (spam)

@contar
clase Otro: pase # Me gusta Otro = contar (Otro)

spam.numInstances # Ambos se ponen a cero


Other.numInstances

Aunque este decorador gestiona una función o clase por sí mismo, como veremos más adelante en este libro,
Los decoradores de clases también pueden administrar la interfaz completa de un objeto interceptando llamadas de
construcción y envolviendo el nuevo objeto de instancia en un proxy que implementa herramientas de acceso a
atributos para interceptar solicitudes posteriores, una técnica de codificación multinivel que usaremos para
implementar la privacidad de atributos de clase en Capítulo 39. Aquí hay una vista previa del modelo:

def decorador(cls): clase # En @ decoración


Proxy:
def __init__(self, *argumentos): # En la creación de instancias: hacer un cls
self.envuelto = cls(*args)
def __getattr__(self, nombre): return # En la obtención de atributos: operaciones adicionales aquí

getattr(self.wrapped, nombre)
devolver proxy

@decorador
clase C: ... # Me gusta C = decorador(C)
X = C() # Hace un Proxy que envuelve una C, y luego captura X.attr

Las metaclases, mencionadas brevemente antes, son una herramienta basada en clases similarmente avanzada cuyo
los roles a menudo se cruzan con los de los decoradores de clase. Proporcionan un modelo alternativo,
que enruta la creación de un objeto de clase a una subclase de la clase de tipo de nivel superior , en
la conclusión de una declaración de clase :

clase Meta(tipo):
def __nuevo__(meta, nombre de clase, supers, classdict):
... lógica adicional + creación de clase a través de llamada de tipo ...

Decoradores y Metaclases: Parte 1 | 1039


Machine Translated by Google

clase C(metaclase=Meta): ...mi


creación enrutada a Meta... # Como C = Meta('C', (), {...})

En Python 2.X, el efecto es el mismo, pero la codificación difiere: use un atributo de clase en lugar de un argumento de
palabra clave en el encabezado de clase :

clase C:
__metaclass__ = Meta...
mi creación enrutada a Meta...

En cualquier línea, Python llama a la metaclase de una clase para crear el nuevo objeto de clase, pasando los datos
definidos durante la ejecución de la instrucción de clase ; en 2.X, la metaclase simplemente tiene como valor
predeterminado el creador de clase clásico:

nombreclase = Meta(nombreclase, superclases, atributodict)

Para asumir el control de la creación o inicialización de un nuevo objeto de clase, una metaclase generalmente redefine
el método __nuevo__ o __init__ de la clase de tipo que normalmente intercepta esta llamada. El efecto neto, al igual
que con los decoradores de clases, es definir el código que se ejecutará automáticamente en el momento de la creación
de la clase. Aquí, este paso vincula el nombre de la clase al resultado de una llamada a una metaclase definida por el
usuario. De hecho, una metaclase no necesita ser una clase en absoluto, una posibilidad que exploraremos más
adelante que desdibuja parte de la distinción entre esta herramienta y los decoradores, e incluso puede calificar a los
dos como funcionalmente equivalentes en muchos roles.

Ambos esquemas, decoradores de clase y metaclases, son libres de aumentar una clase o devolver un objeto arbitrario
para reemplazarlo: un protocolo con posibilidades de personalización basadas en clases casi ilimitadas. Como veremos
más adelante, las metaclases también pueden definir métodos que procesan sus clases de instancia, en lugar de
instancias normales de ellas, una técnica que es similar a los métodos de clase, y podría ser emulada en espíritu por
métodos y datos en proxies decoradores de clase, o incluso un decorador de clase que devuelve una instancia de
metaclase. Tales conceptos vinculantes para la mente requerirán el trabajo de base conceptual del Capítulo 40 (¡y muy
posiblemente sedación!).

Para más detalles


Naturalmente, hay mucho más en las historias de decoradores y metaclases de lo que he mostrado aquí. Aunque son
un mecanismo general cuyo uso puede ser requerido por algunos paquetes, la codificación de nuevos decoradores y
metaclases definidos por el usuario es un tema avanzado de interés principalmente para los creadores de herramientas,
no para los programadores de aplicaciones. Debido a esto, postergaremos la cobertura adicional hasta la parte final y
opcional de este libro:

• El Capítulo 38 muestra cómo codificar propiedades usando la sintaxis del decorador de funciones en más
profundidad.

• El Capítulo 39 tiene mucho más sobre decoradores, incluido un examen más completo.
por favor

• El Capítulo 40 cubre las metaclases y más sobre la gestión de clases e instancias.


historia.

1040 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

Aunque estos capítulos cubren temas avanzados, también nos brindarán la oportunidad de ver a Python
en funcionamiento en ejemplos más sustanciales que los que se pudieron proporcionar en gran parte del
resto del libro. Por ahora, pasemos a nuestro tema final relacionado con la clase.

La función súper integrada: ¿para bien o para mal?


Hasta ahora, he mencionado la función súper incorporada de Python solo brevemente de pasada porque
es relativamente poco común e incluso puede ser controvertida de usar. Sin embargo, dada la mayor
visibilidad de esta convocatoria en los últimos años, merece una mayor elaboración en esta edición.
Además de presentar super, esta sección también sirve como estudio de caso de diseño de lenguaje para
cerrar un capítulo sobre tantas herramientas cuya presencia puede parecer curiosa para algunos en un
lenguaje de secuencias de comandos como Python.

Parte de esta sección cuestiona esta proliferación de herramientas, y lo animo a que juzgue cualquier
contenido subjetivo aquí por sí mismo (y volveremos a estas cosas al final de este libro después de haber
ampliado otras herramientas avanzadas como metaclases y descriptores). Aún así, la rápida tasa de
crecimiento de Python en los últimos años representa un punto de decisión estratégica para el futuro de
su comunidad, y super parece un ejemplo representativo tan bueno como cualquier otro.

El gran superdebate Como se

señaló en los capítulos 28 y 29, Python tiene una función superintegrada que se puede utilizar para
invocar métodos de superclase de forma genérica, pero se pospuso hasta este punto del libro. Esto fue
deliberado, debido a que super tiene desventajas sustanciales en el código típico, y un caso de uso único
que parece oscuro y complejo para muchos observadores, la mayoría de los principiantes están mejor
atendidos por el esquema tradicional de llamadas de nombre explícito utilizado hasta ahora. Consulte la
barra lateral "¿Qué pasa con súper?" en la página 831 en el Capítulo 28 para un breve resumen de la
justificación de esta política.

La comunidad de Python en sí misma parece dividida sobre este tema, con artículos en línea que van
desde "Python's Super Considered Harmful" hasta "Python's super() ¡considerado super!"3 Francamente,
en mis clases en vivo, esta llamada parece ser la más frecuente . interés para los programadores de Java
que comienzan a usar Python de nuevo, debido a su similitud conceptual con una herramienta en ese
lenguaje (muchas características nuevas de Python finalmente deben su existencia a los programadores
de otros lenguajes que traen sus viejos hábitos a un nuevo modelo). El super de Python no es el de Java:
se traduce de manera diferente a la herencia múltiple de Python y tiene

3. Ambos son en parte artículos de opinión, pero se recomienda su lectura. El primero finalmente se retituló "Python's Super
es ingenioso, pero no se puede usar", y se encuentra hoy en https:// fuhm.net/ super-harmful. Extrañamente, ya pesar de
su tono subjetivo, el segundo artículo ("¡Super() de Python considerado super!") solo de alguna manera encontró su
camino en el manual oficial de la biblioteca de Python; vea su enlace en la súper sección del manual... y considere exigir
que las opiniones diferentes se representen de manera más uniforme en la documentación de sus herramientas, o que
se omitan por completo. ¡Los manuales de Python no son el lugar para la opinión personal y la propaganda unilateral!

La función súper integrada: ¿para bien o para mal? | 1041


Machine Translated by Google

un caso de uso más allá de Java, pero ha logrado generar tanto controversia como malentendidos desde
su concepción.

Este libro pospuso la súper llamada hasta ahora (y la omitió casi por completo en ediciones anteriores)
porque tiene problemas importantes: es prohibitivamente engorroso para usar en 2.X, difiere en forma
entre 2.X y 3.X, se basa en inusual semántica en 3.X, y se mezcla mal con la herencia múltiple y la
sobrecarga de operadores de Python en el código típico de Python. De hecho, como veremos, en algunos
códigos super puede enmascarar problemas y desalentar un estilo de codificación más explícito que
ofrece un mejor control.

En su defensa, esta llamada también tiene un caso de uso válido (despacho de método cooperativo del
mismo nombre en árboles de herencia múltiple de diamantes), pero parece pedir a muchos recién llegados.
Requiere que super se use universal y consistentemente (si no neuróticamente), al igual que __slots__
discutido anteriormente; se basa en el algoritmo MRO posiblemente oscuro para ordenar llamadas; y
aborda un caso de uso que parece mucho más la excepción que la norma en los programas de Python.
En este papel, super parece una herramienta avanzada basada en principios esotéricos, que pueden
estar más allá de la audiencia de Python, y parece artificial para los objetivos reales del programa. Aparte
de eso, su expectativa de uso universal parece poco realista para la gran cantidad de código Python
existente.

Debido a todos estos factores, este libro de nivel introductorio ha preferido hasta ahora el esquema
tradicional de llamadas de nombre explícito y recomienda lo mismo para los recién llegados. Es mejor
que aprendas primero el esquema tradicional, y quizás sea mejor que sigas con eso en general, en lugar
de usar una herramienta adicional para casos especiales que puede no funcionar en algunos contextos y
depende de la magia arcana en el uso válido pero atípico. caso al que se dirige. Esta no es solo la opinión
de su autor; a pesar de las mejores intenciones de su defensor, super no es ampliamente reconocido
como "mejor práctica" en Python hoy en día, por razones completamente válidas.

Por otro lado, al igual que con otras herramientas, el uso cada vez mayor de esta llamada en el código de
Python en los últimos años hace que ya no sea opcional para muchos programadores de Python: ¡la
primera vez que la ve, es oficialmente obligatoria! Para los lectores que deseen experimentar con super,
y para otros lectores a los que se les imponga, esta sección proporciona una breve mirada a esta
herramienta y su razón de ser, comenzando con alternativas.

Formulario de llamada de superclase tradicional: portátil, general

En general, los ejemplos de este libro prefieren volver a llamar a los métodos de superclase cuando sea
necesario nombrando la superclase explícitamente, porque esta técnica es tradicional en Python, porque
funciona igual en Python 2.X y 3 .X, y porque evita las limitaciones y complejidades relacionadas con esta
llamada tanto en 2.X como en 3.X. Como se mostró anteriormente, el esquema de llamada de método de
superclase tradicional para aumentar un método de superclase funciona de la siguiente manera:

>>> clase # En Python 2.X y 3.X


C: def
act(self): print('spam')

1042 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

>>> clase D(C): def


act(self): C.act(self)
print('huevos') # Nombra la superclase explícitamente, pásate a ti mismo

>>> X = D()
>>> X.act()
huevos de spam

Este formulario funciona igual en 2.X y 3.X, sigue el modelo de ping de mapa de llamada de método
normal de Python, se aplica a todos los formularios de árbol de herencia y no conduce a un
comportamiento confuso cuando se utiliza la sobrecarga de operadores. Para ver por qué importan estas
distinciones, veamos cómo se compara super .

Uso de super básico y sus ventajas y

desventajas En esta sección, presentaremos super en el modo básico de herencia única y veremos las
desventajas que se perciben en esta función. Como veremos, en este contexto super funciona como se
anuncia, pero no es muy diferente de las llamadas tradicionales, se basa en una semántica inusual y es
engorroso de implementar en 2.X. Más importante aún, tan pronto como sus clases crezcan para usar
herencia múltiple, este modo de súper uso puede enmascarar problemas en su código y enrutar llamadas
de maneras que no espera.

Semántica impar: un proxy mágico en

Python 3.X El súper incorporado en realidad tiene dos funciones previstas. El más esotérico de estos —
protocolos cooperativos de envío de herencia múltiple en árboles de herencia múltiple de diamantes (sí,
un trabalenguas!)— se basa en 3.X MRO, se tomó prestado del lenguaje Dylan y se tratará más adelante
en esta sección.

El rol que nos interesa aquí se usa más comúnmente y es más solicitado por personas con experiencia
en Java: permitir que las superclases se nombren de forma genérica en los árboles de herencia. Esto
tiene como objetivo promover un mantenimiento de código más simple y evitar tener que escribir rutas de
referencia de superclase largas en las llamadas. En Python 3.X, esta llamada parece, al menos a primera
vista, lograr bien este propósito:
>>> clase C: # En Python 3.X (solo: vea el super formulario 2.X más adelante)
def act(self):
print('spam')

>>> clase D(C): def


act(self): super().act()
print('huevos') # Hacer referencia a la superclase de forma genérica, omitirse a sí mismo

>>> X = D()
>>> X.act()
huevos de spam

La función súper integrada: ¿para bien o para mal? | 1043


Machine Translated by Google

Esto funciona y minimiza los cambios de código: no necesita actualizar la llamada si D's
cambios de superclase en el futuro. Una de las mayores desventajas de esta llamada en 3.X,
sin embargo, es su confianza en la magia profunda: aunque propenso a cambiar, opera hoy por
inspeccionando la pila de llamadas para ubicar automáticamente el argumento propio y encontrar
la superclase, y empareja los dos en un objeto proxy especial que enruta la llamada posterior a
la versión de superclase del método. Si eso suena complicado y extraño, es porque lo es. De hecho, este formulario
de llamada no funciona en absoluto fuera del contexto de una clase.
método:

>>> súper # Un objeto proxy "mágico" que enruta llamadas posteriores


<clase 'súper'>
>>> súper()
SystemError: super (): sin argumentos

>>> clase E(C):


def método(auto): # self está implícito en super... ¡solo!
proxy = super() # Esta forma no tiene significado fuera de un método
print(proxy) # Mostrar el objeto proxy normalmente oculto
proxy.act() # Sin argumentos: implícitamente llama al método de la superclase.

>>> E().método()
<súper: <clase 'E'>, <objeto E>>
correo no deseado

Realmente, la semántica de esta llamada no se parece a nada más en Python: no es un límite ni


método no vinculado, y de alguna manera encuentra un yo a pesar de que omite uno en la llamada. En
árboles de herencia única, una superclase está disponible desde uno mismo a través de la ruta
self.__class__.__bases__[0], pero la naturaleza fuertemente implícita de esta llamada hace que esta
difícil de ver, e incluso va en contra de la autopolítica explícita de Python que es cierta
en todos lados. Es decir, esta llamada viola un modismo fundamental de Python para un solo uso
caso. También contradice rotundamente la regla de diseño EIBTI de larga data de Python (ejecute "im portar esto"
para obtener más información sobre esta regla).

Trampa: Agregar herencia múltiple ingenuamente

Además de su semántica inusual, incluso en 3.X, este súper rol se aplica más directamente a los usuarios individuales .
árboles de herencia, y puede volverse problemático tan pronto como las clases emplean múltiples en herencia con
clases codificadas tradicionalmente. Esto parece una gran limitación de alcance; adeudado
Para la utilidad de las clases mixtas en Python, la herencia múltiple de superclases separadas e independientes es
probablemente más la norma que la excepción en el código realista.
La súper llamada parece una receta para el desastre en clases codificadas para usar ingenuamente su modo básico,
sin permitir sus implicaciones mucho más sutiles en árboles de herencia múltiple.

A continuación se ilustra la trampa. Este código comienza su vida felizmente desplegando super en
modo de herencia única para invocar un método un nivel por encima de C:

>>> clase A: # En Python 3.X


def act(self): print('A')
>>> clase B:
def act(self): print('B')

1044 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

>>> clase C(A): def


act(self): super().act()
# súper aplicado a un árbol de herencia simple
>>> X = C()
>>> X.act()
A

Sin embargo, si dichas clases luego crecen para usar más de una superclase, super puede volverse
propensa a errores e incluso inutilizable; no genera una excepción para árboles de herencia múltiple, pero
ingenuamente elegirá solo la superclase más a la izquierda que tiene el método que se está ejecutando
( técnicamente, el primero según el MRO), que puede o no ser el que desea: >>> class C(A, B): def
act(self): super().act() # Agregue una clase de combinación B con el mismo método

# ¡No falla en múltiples herencias, pero elige solo una!


>>> X = C()
>>> X.act()
A

>>> clase C(B, A): def


act(self): super().act()
# ¡Si B aparece primero, A.act() ya no se ejecuta!
>>> X = C()
>>> X.act()
B

Quizás lo que es peor, esto enmascara silenciosamente el hecho de que probablemente debería estar
seleccionando superclases explícitamente en este caso, como aprendimos anteriormente tanto en este
capítulo como en su predecesor. En otras palabras, el superuso puede oscurecer una fuente común de
errores en Python, uno tan común que vuelve a aparecer en los "Problemas" de esta parte. Si es posible
que necesite usar llamadas directas más tarde, ¿por qué no usarlas antes también? >>> clase C(A, B):
def act(self): A.act(self) # Forma tradicional #
Probablemente necesite ser más explícito aquí # Esta
forma maneja tanto una como múltiples inher # Y funciona
B.act (uno mismo) igual en Python 3.X y 2.X # Entonces, ¿por qué usar el caso
>>> X = C() especial super()?
>>> X.act()

AB

Como veremos en unos momentos, también podría abordar estos casos implementando superllamadas en
cada clase del árbol. Pero esa es también una de las mayores desventajas de super: ¿por qué codificarlo
en cada clase, cuando generalmente no es necesario, y cuando usar la forma tradicional más simple
anterior en una sola clase generalmente será suficiente? Especialmente en el código existente, y en el
código nuevo que utiliza el código existente, este súper requisito parece duro, si no poco realista.

Mucho más sutilmente, como también veremos más adelante, una vez que pasa a varias llamadas de
herencia de esta manera, es posible que las súper llamadas en su código no invoquen la clase que espera
que hagan. Se enrutarán según el orden MRO, que, dependiendo de dónde más se pueda usar super ,
puede invocar un método en una clase que no es la superclase de la persona que llama en absoluto: una

La función súper integrada: ¿para bien o para mal? | 1045


Machine Translated by Google

ordenamiento implícito que podría generar sesiones de depuración interesantes. A menos que
comprenda completamente lo que significa super una vez que se introduce la herencia múltiple, es
mejor que no lo implemente en modo de herencia única tampoco.

Esta situación de codificación no es tan abstracta como parece. Aquí hay un ejemplo del mundo real
de tal caso, tomado del estudio de caso de PyMailGUI en Programación de Python: las siguientes
clases de Python muy típicas usan herencia múltiple para mezclar tanto la lógica de la aplicación como
las herramientas de ventana de clases independientes y, por lo tanto, deben invocar a ambos .
constructores de superclase explícitamente con llamadas directas por nombre. Tal como está
codificado, un super().__init__() aquí ejecutaría solo un constructor, y agregar super a lo largo de los
árboles de clases disjuntos de este ejemplo sería más trabajo, no sería más simple y no tendría sentido
en herramientas diseñadas para implementación arbitraria en clientes que pueden usar super o no:

class PyMailServerWindow(PyMailServer, windows.MainWindow): "un Tk, con protocolo


adicional y métodos combinados" def __init__(self): windows.MainWindow.__init__(self,
appname, srvrname)

PyMailServer.__init__(auto)

class PyMailFileWindow(PyMailFile, windows.PopupWindow): "un nivel superior,


con protocolo adicional y métodos combinados" def __init__(self, nombre de
archivo):
windows.PopupWindow.__init__(self, nombre de aplicación, nombre de archivo)
PyMailFile.__init__(self, nombre de archivo)

El punto crucial aquí es que usar super solo para los casos de herencia simple donde se aplica más
claramente es una fuente potencial de error y confusión, y significa que los programadores deben
recordar dos formas de lograr el mismo objetivo, cuando solo una: llamadas directas explícitas. —
podría ser suficiente para todos los casos.

En otras palabras, a menos que pueda estar seguro de que nunca agregará una segunda superclase
a una clase en un árbol durante toda la vida útil de su software, no puede usar super en modo de
herencia única sin comprender y permitir su papel mucho más sofisticado en múltiples- árboles de
herencia. Discutiremos esto último más adelante, pero no es opcional si implementa super en absoluto.

Desde un punto de vista más práctico, tampoco está claro que la cantidad insignificante de
mantenimiento de código que este súper rol pretende evitar justifique por completo su presencia. En la
práctica de Python, los nombres de las superclases en los encabezados rara vez se cambian; cuando
lo son, normalmente hay como máximo un número muy pequeño de llamadas de superclase para
actualizar dentro de la clase. Y considere esto: si agrega una nueva superclase en el futuro que no usa
super (como en el ejemplo anterior), tendrá que envolverla en un proxy de adaptador o aumentar todas
las superllamadas en su clase para usar el esquema tradicional de llamada de nombre explícito de
todos modos, una tarea de mantenimiento que parece igual de probable, pero quizás más propensa a
errores si ha llegado a depender de la súper magia.

1046 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

Limitación: sobrecarga de

operadores Como se señaló brevemente en el manual de la biblioteca de Python, super tampoco funciona
completamente en presencia de métodos de sobrecarga de operadores __X__ . Si estudia el siguiente código,
verá que las llamadas directas con nombre a los métodos de sobrecarga en la superclase funcionan
normalmente, pero el uso del superresultado en una expresión no se envía al método de sobrecarga de la
superclase:

>>> clase C: # En Python 3.X #


def __getitem__(self, ix): Método de sobrecarga de indexación
print('Índice C')

>>> clase D(C):


def __getitem__(self, ix): # Redefinir para extender aquí
print('Índice D')
C.__getitem__(self, ix) # El formulario de llamada tradicional
super().__getitem__(ix) funciona # Las llamadas de nombre directo

super()[ix] también funcionan # ¡Pero los operadores no! (__obteneratributo__)

>>> X = C()
>>> X[99]
índice C
>>> X = D()
>>> X[99]
índice D
índice C
índice C
Rastreo (llamadas recientes más última):
Archivo "", línea 1, en
Archivo "", línea 6, en __getitem__
TypeError: el objeto 'super' no se puede suscribir

Este comportamiento se debe al mismo cambio de clase de estilo nuevo (y 3.X) descrito anteriormente en
este capítulo (consulte “Obtención de atributos para instancias de omisiones integradas” en la página 987),
porque el objeto proxy devuelto por los superusos __getattribute__ para capturar y enviar llamadas de métodos
posteriores, no puede interceptar las invocaciones automáticas de métodos __X__ ejecutadas por operaciones
integradas que incluyen expresiones, ya que estas comienzan su búsqueda en la clase en lugar de la instancia.
Esto puede parecer menos severo que la limitación de herencia múltiple, pero los operadores generalmente
deberían funcionar igual que la llamada de método equivalente, especialmente para un integrado como este.
No admitir esto agrega otra excepción para que los superusuarios
adelante y recuerda.

El kilometraje de otros lenguajes puede variar, pero en Python, el yo es explícito, las mezclas de herencia
múltiple y la sobrecarga de operadores son comunes, y las actualizaciones de nombres de superclases son raras.
Debido a que super agrega un caso especial extraño al lenguaje, uno con semántica extraña, alcance limitado,
requisitos rígidos y recompensa cuestionable, la mayoría de los programadores de Python pueden estar mejor
atendidos por el esquema de llamada tradicional de aplicación más amplia. Si bien super también tiene
algunas aplicaciones avanzadas que estudiaremos más adelante, pueden ser demasiado oscuras para
garantizar que sea una parte obligatoria de la caja de herramientas de todos los programadores de Python.

La función súper integrada: ¿para bien o para mal? | 1047


Machine Translated by Google

El uso difiere en Python 2.X: llamadas

detalladas Si usted es un usuario de Python 2.X que lee este libro de versión dual, también debe saber que la
supertécnica no es portátil entre las líneas de Python. Su forma difiere entre 2.X y 3.X, y no solo entre las
clases clásicas y las de estilo nuevo. Es realmente una herramienta diferente en 2.X, que no puede ejecutar la
forma más simple de 3.X.

Para que esta llamada funcione en Python 2.X, primero debe usar clases de nuevo estilo. Incluso entonces,
también debe pasar explícitamente el nombre de la clase inmediata y self a super, lo que hace que esta llamada
sea tan compleja y detallada que, en la mayoría de los casos, probablemente sea más fácil evitarla por
completo, y simplemente nombrar la superclase explícitamente según el patrón de código tradicional anterior
( para abreviar, dejaré que los lectores consideren qué significa cambiar el nombre propio de una clase para el
mantenimiento del código cuando se usa el súper formulario 2.X):

>>> clase C(objeto): def # En Python 2.X: solo para clases de estilo nuevo
act(self):
print('spam')

>>> class D(C): def


act(self): super(D,
self).act() # 2.X: formato de llamada diferente - parece demasiado complejo print('eggs')
# ¡"D" puede ser tanto para escribir/ cambiar como "C"!

>>> X = D()
>>> X.act()
huevos de spam

Aunque puede usar el formulario de llamada 2.X en 3.X para la compatibilidad con versiones anteriores, es
demasiado engorroso implementarlo en el código solo 3.X, y el formulario 3.X más razonable no se puede usar
en 2.X:

>>> clase D(C): def


act(self):
super().act() # El formato de llamada 3.X más simple falla en 2.X
print('huevos')

>>> X = D()
>>> X.act()
TypeError: super() toma al menos 1 argumento (0 dado)

Por otro lado, el formulario de llamada tradicional con nombres de clases explícitos funciona en 2.X tanto en
las clases clásicas como en las de nuevo estilo, y exactamente como lo hace en 3.X:

>>> clase D(C): def


act(self): C.act(self)
print('huevos') # Pero el patrón tradicional funciona de forma portátil
# Y a menudo puede ser más simple en el código 2.X

>>> X = D()
>>> X.act()
huevos de spam

1048 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

Entonces, ¿por qué usar una técnica que funciona solo en contextos limitados en lugar de una que funciona en
muchos más? Aunque su base es compleja, las siguientes secciones intentan reunir apoyo para la supercausa .

Las ventajas de super: Cambios en el árbol y envío Habiendo

mostrado las desventajas de super, también debo confesar que he tenido la tentación de usar esta llamada en
código que solo se ejecutaría en 3.X, y que usaba una superclase muy larga ruta de referencia a través de un
paquete de módulos (es decir, principalmente por pereza, pero la brevedad de la codificación también puede ser
importante). Para ser justos, super aún puede ser útil en algunos casos de uso, el principal de los cuales merece
una breve introducción aquí:

• Cambio de árboles de clases en tiempo de ejecución: cuando se puede cambiar una superclase en tiempo de
ejecución, no es posible codificar su nombre en una expresión de llamada, pero es posible enviar llamadas
a través de super.

Por otro lado, este caso es extremadamente raro en la programación de Python y, a menudo, también se
pueden usar otras técnicas en este contexto. • Envío cooperativo de métodos de herencia múltiple: cuando

varios árboles de herencia deben enviarse al método del mismo nombre en varias clases, super puede
proporcionar un protocolo para el enrutamiento ordenado de llamadas.

Por otro lado, el árbol de clases debe basarse en la ordenación de las clases por parte del MRO, una
herramienta compleja por derecho propio que es artificial para el problema que un programa debe abordar,
y debe codificarse o aumentarse para usar super en cada uno . versión del método en el árbol para que sea
efectivo. Dicho envío también puede implementarse a menudo de otras formas (por ejemplo, a través del
estado de instancia).

Como se discutió anteriormente, super también se puede usar para seleccionar una superclase genéricamente,
siempre que el valor predeterminado de MRO tenga sentido, aunque en el código tradicional, la denominación
explícita de una superclase a menudo es preferible, e incluso puede ser necesaria. Además, incluso los casos de
superuso válidos tienden a ser poco comunes en muchos programas de Python, hasta el punto de parecer
curiosidad académica para algunos. Sin embargo, los dos casos que acabamos de enumerar se citan con mayor
frecuencia como súper razones, así que echemos un vistazo rápido a cada uno.

Los cambios de clase en tiempo de ejecución y

super superclase que se pueden cambiar en tiempo de ejecución impiden dinámicamente codificar sus nombres
en los métodos de una subclase, mientras que super felizmente buscará dinámicamente la superclase actual. Aún
así, este caso puede ser demasiado raro en la práctica para garantizar el supermodelo por sí mismo y, a menudo,
se puede implementar de otras maneras en los casos excepcionales en los que se necesita. Para ilustrar, lo
siguiente cambia la superclase de C dinámicamente al cambiar la tupla __bases__ de la subclase en 3.X:

>>> clase X:
def m(self): print('Xm')
>>> clase Y:

La función súper integrada: ¿para bien o para mal? | 1049


Machine Translated by Google

def m(self): print('Ym') >>>


class C(X): def m(self): super().m() # Comenzar heredando de X
# No se puede codificar el nombre de la clase aquí

>>> i = C()
>>> im()
Xm
>>> C.__bases__ = (Y,) # ¡Cambia la superclase en tiempo de ejecución!
>>> im()
Ym

Esto funciona (y comparte objetivos de transformación del comportamiento con otra magia profunda,
como cambiar la __clase__ de una instancia), pero parece extremadamente raro. Además, puede haber
otras formas de lograr el mismo efecto, quizás la más simple, llamando indirectamente a través del
valor de la tupla de la superclase actual: código especial para estar seguro, pero solo para un caso muy
especial (y quizás no más especial que el enrutamiento implícito por MRO):

>>> class C(X):


def m(self): C.__bases__[0].m(self) # Código especial para un caso especial

>>> i = C()
>>> im()
Xm
>>> C.__bases__ = (Y,) # Mismo efecto, sin super()
>>> im()
Ym

Dadas las alternativas preexistentes, este caso por sí solo no parece justificar la superclase , aunque
en árboles más complejos, la siguiente lógica, basada en el orden MRO del árbol en lugar de los
enlaces físicos de la superclase, también puede aplicarse aquí.

Envío del método cooperativo de herencia múltiple El segundo de

los casos de uso enumerados anteriormente es el principal fundamento comúnmente dado para super,
y también toma prestado de otros lenguajes de programación (sobre todo, Dylan), donde su caso de
uso puede ser más común que en Python típico. código. Por lo general, se aplica a los árboles de
herencia múltiple con patrón de diamante, discutidos anteriormente en este capítulo, y permite que las
clases cooperativas y conformes enruten las llamadas a un método con el mismo nombre de manera
coherente entre implementaciones de múltiples clases. Especialmente para los constructores, que
normalmente tienen múltiples implementaciones, esto puede simplificar el protocolo de enrutamiento de
llamadas cuando se usa de manera consistente.

En este modo, cada superllamada selecciona el método de una clase siguiente que lo sigue en el
ordenamiento MRO de la clase del propio sujeto de una llamada de método. El MRO se introdujo antes;
es el camino que sigue Python para la herencia en las clases de nuevo estilo. Debido a que el orden
lineal de MRO depende de qué clase se creó, el orden de envío del método orquestado por super
puede variar según el árbol de clase y visita cada clase solo una vez, siempre que todas las clases
usen super para enviar.

1050 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

Dado que cada clase participa en un objeto de diamante en 3.X (y clases de nuevo estilo 2.X), las aplicaciones
son más amplias de lo que cabría esperar. De hecho, algunos de los ejemplos anteriores que demostraron
superdeficiencias en árboles de herencia múltiple podrían usar esta llamada para lograr sus objetivos de envío.
Sin embargo, para hacerlo, super debe usarse universalmente en el árbol de clases para garantizar que se
transmitan las cadenas de llamadas a métodos, un requisito bastante importante que puede ser difícil de aplicar
en gran parte del código nuevo y existente.

Lo básico: Súper llamado cooperativo en

acción Echemos un vistazo a lo que significa este rol en el código. En esta sección y las siguientes, aprenderemos
cómo funciona el súper y exploraremos las ventajas y desventajas que implica en el camino.
Para comenzar, considere las siguientes clases de Python codificadas tradicionalmente (resumidas un poco aquí
como de costumbre por espacio):
>>> clase B:
def __init__(self): print('B.__init__') >>> clase C: def # Ramas de árboles de clases disjuntas
__init__(self): print('C.__init__') >>> clase D(B, C) : pasar

>>> x = D() # Se ejecuta más a la izquierda solo por defecto

B.__init__

En este caso, las ramas del árbol de la superclase son inconexas (no comparten un ancestro explícito común),
por lo que las subclases que las combinan deben llamar a cada superclase por su nombre, una situación común
en gran parte del código Python existente que super no puede abordar directamente sin cambios en el código. :
>>> clase D(B, C): def __init__(auto): B.__init__(auto)

# Forma tradicional
# Invocar supers por nombre
C.__init__(uno mismo)

>>> x = D()
B.__init__
C.__init__

Sin embargo , en los patrones de árbol de clase de diamante , las llamadas de nombre explícito pueden activar de
forma predeterminada el método de la clase de nivel superior más de una vez, aunque esto podría subvertirse con
protocolos adicionales (p. ej., marcadores de estado en la instancia):
>>> clase A:
def __init__(self): print('A.__init__') >>> clase B(A):
def __init__(self): print('B.__init__'); A.__init__(self) >>> class
C(A): def __init__(self): print('C.__init__'); A.__init__(uno mismo)

>>> x = B()
B.__init__
A.__init__ >>>
x = C() # Cada súper funciona por sí mismo
C.__init__

La función súper integrada: ¿para bien o para mal? | 1051


Machine Translated by Google

A.__init__

>>> clase D(B, C): paso >>> x # Todavía se ejecuta solo hacia la izquierda

= D()
B.__init__
A.__init__

>>> clase D(B, C): def


__init__(auto): # Forma tradicional
B.__init__(auto) # Invocar ambos supers por nombre
C.__init__(uno mismo)

>>> x = D() # ¡Pero esto ahora invoca a A dos veces!

B.__init__
A.__init__
C.__init__
A.__init__

Por el contrario, si todas las clases usan super, o los proxies las coaccionan adecuadamente para que se
comporten como si lo hicieran, las llamadas al método se envían de acuerdo con el orden de clase en el MRO, de
modo que el método de la clase de nivel superior se ejecuta solo una vez:

>>> clase A:
def __init__(self): print('A.__init__') >>> clase B(A):
def __init__(self): print('B.__init__'); super().__init__() >>>
clase C(A): def __init__(self): print('C.__init__'); super().__init__()

>>> x = B() # Ejecuta B.__init__, A es el próximo super en B MRO propio


B.__init__
A.__init__ >>>
x = C()
C.__init__
A.__init__

>>> clase D(B, C): paso >>> x


= D() # Ejecuta B.__init__, ¡C es el próximo súper en el D MRO de uno mismo!
B.__init__
C.__init__
A.__init__

La verdadera magia detrás de esto es la lista MRO lineal construida para la clase de sí mismo , ya que cada clase
aparece solo una vez en esta lista y debido a que super envía a la siguiente clase en esta lista, asegura una
cadena de invocación ordenada que visita cada clase solo una vez. De manera crucial, la siguiente clase que
sigue a B en el MRO difiere según la clase de sí mismo: es A para una instancia B , pero C para una instancia D ,
lo que explica el orden de estafa.
los estructuras ejecutan:

>>> B.__mro__
(<clase '__principal__.B'>, <clase '__principal__.A'>, <clase 'objeto'>)

>>> D.__mro__
(<clase '__principal__.D'>, <clase '__principal__.B'>, <clase '__principal__.C'>, <clase
'__principal__.A'>, <clase 'objeto'>)

1052 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

El MRO y su algoritmo se presentaron anteriormente en este capítulo. Al seleccionar una clase siguiente
en la secuencia MRO, una súper llamada en el método de una clase propaga la llamada a través del
árbol, siempre que todas las clases hagan lo mismo. En este modo, super no necesariamente elige una
superclase en absoluto; elige el siguiente en el MRO linealizado, que podría ser un hermano, o incluso
un pariente inferior , en el árbol de clases de una instancia determinada. Consulte “Rastreo del MRO” en
la página 1002 para ver otros ejemplos del camino que seguiría el superdespacho , especialmente para
los que no son diamantes.

Los trabajos anteriores, e incluso pueden parecer ingeniosos a primera vista, pero su alcance también
puede parecer limitado para algunos. La mayoría de los programas de Python no se basan en los matices
de los árboles de herencia múltiple con patrón de diamante (de hecho, muchos programadores de Python
que he conocido no saben lo que significa el término). Además, super se aplica más directamente a los
casos de herencia única y de diamantes cooperativos, y puede parecer superfluo para los casos disjuntos
que no son de diamantes, en los que podríamos querer invocar métodos de superclase de forma selectiva
o independiente. Incluso los diamantes cooperativos se pueden administrar de otras maneras que pueden
brindar a los programadores más control que un pedido automático de MRO. Sin embargo, para evaluar
esta herramienta de manera objetiva, debemos profundizar más.

Restricción: Requisito de anclaje de la

cadena de llamadas La súper llamada viene con complejidades que pueden no ser evidentes en el
primer encuentro, e incluso pueden parecer características inicialmente. Por ejemplo, debido a que todas
las clases heredan del objeto en 3.X automáticamente (y explícitamente en las clases de nuevo estilo
2.X), la ordenación MRO se puede usar incluso en casos en los que el diamante solo está implícito; en lo
siguiente, desencadenar constructores en clases independientes automáticamente:
>>> clase B:
def __init__(self): print('B.__init__'); super().__init__() >>> clase
C: def __init__(self): print('C.__init__'); super().__init__()

>>> x = B() # objeto es un super implícito al final de MRO


B.__init__
>>> x = C()
C.__init__

>>> clase D(B, C): paso # Hereda B.__init__ pero el MRO de B difiere para D #
>>> x = D() Ejecuta B.__init__, ¡C es el siguiente super en el MRO de D propio!
B.__init__
C.__init__

Técnicamente, este modelo de envío generalmente requiere que el método al que llama super debe
existir, y debe tener la misma firma de argumento en todo el árbol de clases, y todas las apariencias del
método, excepto la última, deben usar super . Este ejemplo anterior funciona solo porque la superclase
de objeto implícita al final del MRO de las tres clases tiene un __init__ compatible que cumple con estas
reglas:

>>> B.__mro__
(<clase '__principal__.B'>, <clase 'objeto'>)

La función súper integrada: ¿para bien o para mal? | 1053


Machine Translated by Google

>>> D.__mro__
(<clase '__principal__.D'>, <clase '__principal__.B'>, <clase '__principal__.C'>, <clase 'objeto'>)

Aquí, para una instancia de D , la siguiente clase en el MRO después de B es C, seguida por el objeto cuyo
__init__ acepta silenciosamente la llamada de C y finaliza la cadena. Por lo tanto, el método de B llama a C,
que termina en la versión del objeto , aunque C no es una superclase de B.

Realmente, sin embargo, este ejemplo es atípico, y tal vez incluso afortunado. En la mayoría de los casos,
no existirá un valor predeterminado adecuado en el objeto, y puede ser menos trivial satisfacer las
expectativas de este modelo. La mayoría de los árboles requerirán una superclase explícita, y posiblemente
adicional, para cumplir el papel de anclaje que el objeto tiene aquí, para aceptar pero no reenviar la llamada.
Otros árboles pueden requerir un diseño cuidadoso para cumplir con este requisito. Además, a menos que
Python lo optimice, la llamada a los valores predeterminados del objeto (u otro ancla) al final de la cadena
también puede agregar costos de rendimiento adicionales.

Por el contrario, en tales casos, las llamadas directas no implican requisitos de codificación adicionales ni
costos de rendimiento adicionales, y hacen que el envío sea más explícito y directo:
>>> clase B:
def __init__(self): print('B.__init__') >>>
clase C: def __init__(self): print('C.__init__') >>>
clase D(B, C) : def __init__(auto):
B.__init__(auto); C.__init__(uno mismo)

>>> x = D()
B.__init__
C.__init__

Alcance: un modelo de todo o

nada También tenga en cuenta que las clases tradicionales que no se escribieron para usar super en este rol
no se pueden usar directamente en dichos árboles de despacho cooperativos, ya que no reenviarán llamadas
a lo largo de la cadena MRO. Es posible incorporar dichas clases con proxies que envuelven el objeto original
y agregan las súper llamadas requeridas, pero esto impone requisitos de codificación adicionales y costos de
rendimiento en el modelo. Dado que hay muchos millones de líneas de código Python existentes que no usan
super, esto parece un perjuicio importante.

Mire lo que sucede, por ejemplo, si una clase falla en pasar la cadena de llamada al omitir un super,
terminando la cadena de llamada prematuramente, como __slots__, super es generalmente una característica
de todo o nada :
>>> clase B:
def __init__(self): print('B.__init__'); super().__init__() >>> clase
C: def __init__(self): print('C.__init__'); super().__init__() >>> clase D(B,
C): def __init__(self): print('D.__init__'); super().__init__()

>>> X = D()
D.__init__
B.__init__

1054 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

C.__init__
>>> D.__mro__
(<clase '__principal__.D'>, <clase '__principal__.B'>, <clase '__principal__.C'>, <clase 'objeto'>)

# ¿Qué sucede si debe usar una clase que no llama super?

>>> clase B:
def __init__(self): print('B.__init__') >>> class D(B,
C): def __init__(self): print('D.__init__'); super().__init__()

>>> X = D()
D.__init__
B.__init__ # Es una herramienta de todo o nada...

Satisfacer este requisito de propagación obligatorio puede no ser más simple que las llamadas directas
por nombre, que aún puede olvidar, pero que no necesitará exigir de todo el código que emplean sus
clases. Como se mencionó, es posible adaptar una clase como B heredándola de una clase proxy que
incrusta instancias de B , pero eso parece artificial para los objetivos del programa, agrega una llamada
adicional a cada método envuelto, está sujeto a los problemas de clase de nuevo estilo que mencionamos.
se reunió anteriormente con respecto a los proxies de interfaz y las funciones integradas, y parece un
requisito de codificación adicional extra ordinario e incluso sorprendente inherente a un modelo destinado
a simplificar el código.

Flexibilidad: supuestos de

ordenación de llamadas El enrutamiento con super también asume que usted realmente pretende pasar
llamadas de método a través de todas sus clases según el MRO, que puede o no coincidir con sus
requisitos de ordenación de llamadas . Por ejemplo, imagine que, independientemente de otras
necesidades de orden de herencia, lo siguiente requiere que la versión de la clase C de un método dado
se ejecute antes que la B en algunos contextos. Si el MRO dice lo contrario, volverá a las llamadas
tradicionales, lo que puede entrar en conflicto con el súper uso; en lo siguiente, invoque el método de C dos veces:
# ¿Qué pasa si las necesidades de pedido de llamadas de método difieren de las de MRO?

>>> clase B:
def __init__(self): print('B.__init__'); super().__init__() >>> clase C: def
__init__(self): print('C.__init__'); super().__init__() >>> clase D(B, C): def __init__(self):
print('D.__init__'); C.__init__(uno mismo); B.__init__(uno mismo)

>>> X = D()
D.__init__
C.__init__
B.__init__
C.__init__ # Es el MRO xor llamadas explícitas...

Del mismo modo, si desea que algunos métodos no se ejecuten en absoluto, la ruta superautomática no
se aplicará tan directamente como las llamadas explícitas y dificultará un control más explícito del proceso
de envío. En programas realistas con muchos métodos, recursos y variables de estado, estos escenarios
parecen completamente plausibles. Si bien podría reordenar superclases en D para este método, eso
puede romper otras expectativas.

La función súper integrada: ¿para bien o para mal? | 1055


Machine Translated by Google

Personalización: reemplazo de

métodos En una nota relacionada, las expectativas de implementación universal de super pueden
dificultar que una sola clase reemplace (anule) un método heredado por completo. No pasar la
llamada más alto con super (intencionalmente en este caso) funciona bien para la clase en sí, pero
puede romper la cadena de llamadas de los árboles en los que se mezcla, evitando así que se
ejecuten métodos en otras partes del árbol. Considere el siguiente árbol:
>>> clase A:
def método(auto): print('A.método'); super().método() >>> clase
B(A): def método(self): print('B.método'); super().method() >>> class C: def
method(self): print('C.method') >>> class D(B, C): def method(self):
print('D.method '); super().método()
# No super: hay que anclar la cadena!

>>> X = D()
>>> X.método()
D.método
B.método
A.método # Envío a todos por el MRO automáticamente
C.método

El reemplazo del método aquí rompe el supermodelo y probablemente nos lleva de vuelta a la forma
tradicional:

# ¿Qué sucede si una clase necesita reemplazar por completo el valor predeterminado de un super?

>>> clase B(A):


def metodo(self): print('B.method') >>> # Soltar super para reemplazar el método de A
clase D(B, C): def metodo(self): print('D.method');
super().método()
>>> X = D()
>>> X.método()
D.método
B.método # Pero el reemplazo también rompe la cadena de llamadas...

>>> clase D(B, C): def


método(self): print('D.método'); B.método(auto); C.método(uno mismo)
>>> D().método()
D.método
B.método
C.método # Volvemos a las llamadas explícitas...

Una vez más, el problema con las suposiciones es que asumen cosas. Aunque la suposición de
enrutamiento universal podría ser razonable para los constructores, también parecería entrar en
conflicto con uno de los principios básicos de la programación orientada a objetos: la personalización
de subclases sin restricciones. Esto podría sugerir restringir el uso de super a los constructores, pero
incluso estos a veces pueden justificar el reemplazo, y esto agrega un requisito de caso especial
extraño para un contexto específico. Una herramienta que se puede usar solo para ciertas categorías
de métodos puede ser vista por algunos como redundante e incluso espuria, dada la complejidad
adicional que implica.

1056 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

Acoplamiento: aplicación para mezclar

clases Sutilmente, cuando decimos que super selecciona la siguiente clase en el MRO, en realidad nos
referimos a la siguiente clase en el MRO que implementa el método solicitado; técnicamente salta hacia
adelante hasta que encuentra una clase con el nombre solicitado. . Esto es importante para las clases mixtas
independientes, que pueden agregarse a árboles de clientes arbitrarios. Sin este comportamiento de salto
adelante, estos complementos no funcionarían en absoluto; de lo contrario, abandonarían la cadena de
llamadas de los métodos arbitrarios de sus clientes y no podrían confiar en que sus propias súper llamadas
funcionaran como se esperaba.

En las siguientes ramas independientes, por ejemplo, la llamada al método de C se transmite, aunque Mixin,
la siguiente clase en el MRO de la instancia de C , no define el nombre de ese método. Siempre que los
conjuntos de nombres de métodos sean disjuntos, esto simplemente funciona: las cadenas de llamadas de
cada rama pueden existir de forma independiente:

# Los complementos funcionan para conjuntos de métodos disjuntos

>>> clase A:
def otro(yo): print('A.otro') >>> class
Mixin(A): def otro(self): print('Mixin.otro');
super().otro()

>>> clase B:
def método(self): print('B.method') >>>
clase C(Mixin, B):
def método(auto): print('C.método'); super().otro(); super().método()

>>> C().método()
C.método
Mixin.otro
A.otro
B.método

>>> C.__mro__
(<clase '__principal__.C'>, <clase '__principal__.Mezcla'>, <clase '__principal__.A'>,
<clase '__principal__.B'>, <clase 'objeto'>)

Del mismo modo, mezclar al revés tampoco rompe las cadenas de llamadas de la mezcla. Por ejemplo, a
continuación, aunque B no define otro cuando se llama en C, las clases lo hacen más adelante en el MRO.
De hecho, las cadenas de llamadas funcionan incluso si una de las sucursales no usa super en absoluto;
siempre que se defina un método en algún lugar adelante en el MRO, su llamada funciona:

>>> clase C(B, mezclando):


def método(auto): print('C.método'); super().otro(); super().método()

>>> C().método()
C.método
Mixin.otro
A.otro
B.método

>>> C.__mro__

La función súper integrada: ¿para bien o para mal? | 1057


Machine Translated by Google

(<clase '__main__.C'>, <clase '__main__.B'>, <clase '__main__.Mixin'>, <clase


'__main__.A'>, <clase 'objeto'>)

Esto también es cierto en presencia de rombos: los conjuntos de métodos separados se envían
como se esperaba, incluso si cada rama separada no los implementa, porque seleccionamos el
siguiente en el MRO con el método. Realmente, debido a que el MRO contiene las mismas clases
en estos casos, y debido a que una subclase siempre aparece antes que su superclase en el MRO,
son contextos equivalentes. Por ejemplo, la llamada en Mixin a otro en lo siguiente todavía la
encuentra en A, aunque la siguiente clase después de Mixin en el MRO es B (la llamada al método
en C funciona nuevamente por razones similares): # Los diamantes explícitos también funcionan

>>> class A:
def otro(yo): print('A.otro') >>> class
Mixin(A): def otro(self): print('Mixin.otro');
super().otro()

>>> clase B(A):


def metodo(self): print('B.method') >>>
clase C(Mixin, B):
def método(auto): print('C.método'); super().otro(); super().método()

>>> C().método()
C.método
Mixin.otro
A.otro
B.método

>>> C.__mro__
(<clase '__principal__.C'>, <clase '__principal__.Mezcla'>, <clase '__principal__.B'>,
<clase '__principal__.A'>, <clase 'objeto'>)

# Otros pedidos combinados también funcionan

>>> clase C(B, mezclando):


def método(auto): print('C.método'); super().otro(); super().método()

>>> C().método()
C.método
Mixin.otro
A.otro
B.método

>>> C.__mro__
(<clase '__main__.C'>, <clase '__main__.B'>, <clase '__main__.Mixin'>, <clase
'__main__.A'>, <clase 'objeto'>)

Aún así, esto tiene un efecto que no es diferente, pero puede parecer mucho más implícito, que las
llamadas directas por nombre, que también funcionan igual en este caso, independientemente del
orden de las superclases, y si hay un diamante o no. En este caso, la motivación para confiar en los
pedidos de MRO parece estar en terreno inestable, si la forma tradicional es más simple y más
explícita, y ofrece más control y flexibilidad:

1058 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

# Pero las llamadas directas también funcionan aquí: lo explícito es mejor que lo implícito

>>> clase C(Mezcla, B):


def método(auto): print('C.método'); Mixin.other(self); B.método(uno mismo)

>>> X = C()
>>> X.método()
C.método
Mixin.otro
A.otro B.método

Más importante aún, este ejemplo hasta ahora asume que los nombres de los métodos están separados
en sus ramas; la orden de envío para métodos del mismo nombre en diamantes como este puede ser
mucho menos fortuita. En un diamante como el anterior, por ejemplo, no es imposible que una clase de
cliente pueda invalidar la intención de una súper llamada: la llamada al método en Mixin en lo siguiente
funciona para ejecutar la versión de A como se esperaba, a menos que se mezcle en un árbol que
elimina el cadena de llamadas:

# Pero para métodos no disjuntos: super crea un acoplamiento demasiado fuerte

>>> clase A:
def método(self): print('A.method') >>>
class Mixin(A): def method(self): print('Mixin.method');
super().método()
>>> Mezclar().método()
Mixin.método
A.método

>>> class B(A):


def method(self): print('B.method') >>> # super aquí invocaría A después de B
class C(Mixin, B): def method(self): print('C.method');
super().método()
>>> C().método()
Método
C. Método
Mixin. Método B. # ¡Extrañamos A solo en este contexto!

Puede ser que B no deba redefinir este método de todos modos (y, francamente, podemos estar
invadiendo los problemas inherentes a la herencia múltiple en general), pero esto no tiene por qué
romper la combinación: las llamadas directas le dan más control en tales casos, y permite que la
combinación de clases sea mucho más independiente de los contextos de uso:
# Y las llamadas directas no: son inmunes al contexto de uso

>>> clase A:
def método(self): print('A.method') >>>
class Mixin(A): def method(self): print('Mixin.method');
A.método(uno mismo) #C irrelevante

>>> class C(Mixin, B): def


método(self): print('C.method'); Mixin.method(auto)
>>> C().método()
C.método

La función súper integrada: ¿para bien o para mal? | 1059


Machine Translated by Google

Mixin.método
A.método

Más concretamente, al hacer que los complementos sean más autónomos, las llamadas directas minimizan el
acoplamiento de componentes que siempre aumenta la complejidad del programa, un principio fundamental del
software que parece descuidado por el modelo de despacho variable y específico del contexto de super .

Personalización: Restricciones del mismo

argumento Como nota final, también debe considerar las consecuencias de usar super cuando los argumentos del
método difieren según la clase, porque un codificador de clase no puede estar seguro de qué versión de un método
podría invocar super (de hecho, esto puede variar). por árbol!), cada versión del método generalmente debe aceptar
la misma lista de argumentos, o elegir sus entradas con análisis de listas de argumentos genéricos, cualquiera de
los cuales impone requisitos adicionales en su código. En programas realistas, esta restricción puede, de hecho, ser
un verdadero obstáculo para muchas súper aplicaciones potenciales , impidiendo su uso por completo.

Para ilustrar por qué esto puede ser importante, recuerde las clases de empleados de pizzerías que escribimos en
el Capítulo 31. Según lo codificado allí, ambas subclases usan llamadas directas por su nombre para invocar al
constructor de la superclase, completando automáticamente un argumento de salario esperado; la lógica es que el
subclase implica el grado de pago:

>>> clase Empleado:


def __init__(self, nombre, salario): # Superclase común
self.name = nombre self.salary =
salario

>>> class Chef1(Empleado): def


__init__(yo, nombre): # Argumentos diferentes
Empleado.__init__(yo, nombre, 50000) # Envío por llamada directa

>>> clase Servidor1(Empleado):


def __init__(yo, nombre):
Empleado.__init__(yo, nombre, 40000)

>>> bob = Chef1('Bob') >>>


sue = Servidor1('Sue') >>>
bob.salario, sue.salario (50000,
40000)

Esto funciona, pero dado que se trata de un árbol de herencia única, es posible que tengamos la tentación de
implementar super aquí para enrutar las llamadas al constructor de forma genérica. Hacerlo funciona para cualquiera
de las subclases de forma aislada, ya que su MRO se incluye solo a sí mismo y a su superclase real:

>>> class Chef2(Empleado): def


__init__(self, nombre):
super().__init__(nombre, 50000) # Envío por super()

>>> clase Servidor2(Empleado):


def __init__(self, nombre):
super().__init__(nombre, 40000)

1060 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

>>> bob = Chef2('Bob')


>>> sue = Servidor2('Sue')
>>> bob.salario, sue.salario
(50000, 40000)

Sin embargo, observe lo que sucede cuando un empleado es miembro de ambas categorías. Debido a que
los constructores en el árbol tienen diferentes listas de argumentos, estamos en problemas:

>>> clase DosTrabajos(Chef2, Servidor2): pasar

>>> tom = DosTrabajos('Tom')


TypeError: __init__() toma 2 argumentos posicionales pero se dieron 3

El problema aquí es que la súper llamada en Chef2 ya no invoca a su súper clase Empleado , sino que
invoca a su clase hermana y seguidor en el MRO, Servidor2. Dado que este hermano tiene una lista de
argumentos diferente a la de la verdadera superclase, esperando solo el yo y el nombre, el código se rompe.
Esto es inherente al superuso : debido a que el MRO puede diferir según el árbol, podría llamar a diferentes
versiones de un método en diferentes árboles, incluso algunos que quizás no pueda anticipar al codificar
una clase por sí mismo:

>>> DosTrabajos.__mro__
(<clase '__principal__.DosTrabajos'>, <clase '__principal__.Chef2'>, <clase '__principal__.Servidor2'>
<clase '__principal__.Empleado'>, <clase 'objeto'>)

>>> Chef2.__mro__
(<clase '__principal__.Chef2'>, <clase '__principal__.Empleado'>, <clase 'objeto'>)

Por el contrario, el esquema de llamadas directas por nombre todavía funciona cuando las clases se
mezclan, aunque los resultados son un poco dudosos: la categoría combinada recibe la paga de la superclase
más a la izquierda:

>>> clase DosTrabajos(Chef1, Servidor1): pasar

>>> tom = DosTrabajos('Tom')


>>> tom.salario 50000

Realmente, probablemente queramos enrutar la llamada a la clase de nivel superior en este evento con un
nuevo salario, un modelo que es posible con llamadas directas pero no solo con súper . Además, llamar a
Employee directamente en esta clase significa que nuestro código usa dos técnicas de envío cuando solo
una, llamadas directas, sería suficiente:

>>> class TwoJobs(Chef1, Server1): def


__init__(self, name): Employee.__init__(self, name, 70000)

>>> tom = DosTrabajos('Tom')


>>> tom.salario
70000

>>> class TwoJobs(Chef2, Server2): def


__init__(self, nombre): super().__init__(nombre, 70000)

>>> tom = DosTrabajos('Tom')


TypeError: __init__() toma 2 argumentos posicionales pero se dieron 3

La función súper integrada: ¿para bien o para mal? | 1061


Machine Translated by Google

Este ejemplo puede justificar un rediseño en general, separando partes compartibles de Chef y Server para
mezclar clases sin un constructor, por ejemplo. También es cierto que el polimorfismo en general supone que
los métodos en la interfaz externa de un objeto tienen la misma firma de argumento, aunque esto no se aplica
del todo a la personalización de los métodos de superclase, una técnica de implementación interna que, por
naturaleza, debería soportar la variación, especialmente en los constructores.

Pero el punto crucial aquí es que debido a que las llamadas directas no hacen que el código dependa de un
orden mágico que puede variar según el árbol, soportan más directamente la flexibilidad de la lista de
argumentos. En términos más generales, los rendimientos cuestionables (o débiles) que resultan en el
reemplazo de métodos, el acoplamiento mixto, el orden de llamadas y las restricciones de argumentos
deberían hacer que evalúe su implementación con cuidado. Incluso en el modo de herencia única, su potencial
para impactos posteriores a medida que crecen los árboles es considerable.

En resumen, los tres requisitos de super en este rol también son la fuente de la mayoría de sus problemas de
usabilidad:

• El método llamado por super debe existir, lo que requiere código adicional si no hay un ancla.
presente.

• El método llamado por super debe tener la misma firma de argumento en todo el árbol de clases, lo que
perjudica la flexibilidad, especialmente para los métodos de nivel de implementación como los
constructores.

• Cada apariencia del método llamado por super , pero la última debe usar super en sí mismo, lo que dificulta
el uso del código existente, cambiar el orden de las llamadas, anular métodos y codificar clases
independientes.

Tomados en conjunto, estos parecen crear una herramienta con una complejidad sustancial y compensaciones
significativas, inconvenientes que se afirmarán en el momento en que el código crezca para incorporar la
herencia múltiple.

Naturalmente, puede haber soluciones alternativas creativas para los súper dilemas que acabamos de
plantear, pero los pasos de codificación adicionales diluirían aún más los beneficios de la llamada y, de todos
modos, nos hemos quedado sin espacio aquí. También existen soluciones alternativas que no son súper para
algunos problemas de envío del método de diamante, pero también deberán dejarse como un ejercicio de
usuario por razones de espacio. En general, cuando los métodos de superclase se llaman por un nombre
explícito, las clases raíz de diamantes pueden verificar el estado en instancias para evitar disparar dos veces:
un patrón de codificación igualmente complejo, pero que rara vez se requiere en la mayoría del código, y que
para algunos puede parecer no más difícil que súper en sí mismo.

El súper Resumen Así que

ahí está: lo malo y lo bueno. Al igual que con todas las extensiones de Python, también debe ser el juez en
esta. He tratado de darles a ambos lados del debate una oportunidad justa aquí para ayudarlo a decidir. Pero
debido a la súper llamada:

• Difiere en forma entre 2.X y 3.X

1062 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

• En 3.X, se basa posiblemente en magia no Pythonic y no se aplica completamente a la sobrecarga de operadores o


árboles de herencia múltiple codificados tradicionalmente • En 2.X, parece tan detallado en esta función prevista

que puede hacer que el código sea más complejo


en lugar de menos

• Beneficios de mantenimiento del código de reclamos que pueden ser más hipotéticos que reales en
práctica de Python

incluso los ex programadores de Java también deberían considerar que la técnica tradicional preferida de este libro de
llamadas a superclases de nombres explícitos es una solución al menos tan válida como la súper de Python, una llamada
que en algunos niveles parece una respuesta inusual y limitada a una pregunta que no era siendo preguntado por la
mayoría de los programadores de Python, y no se consideró importante durante gran parte de la historia de Python.

Al mismo tiempo, la superllamada ofrece una solución al difícil problema del envío del método con el mismo nombre en
múltiples árboles de herencia, para los programas que eligen usarlo universal y consistentemente. Pero ahí radica uno
de sus mayores obstáculos: requiere un despliegue universal para abordar un problema que la mayoría de los
programadores probablemente no tienen.
Además, en este punto de la historia de Python, pedir a los programadores que cambien su código existente para usar
esta llamada lo suficientemente amplio como para que sea confiable parece muy poco realista.

Sin embargo, quizás el principal problema de esta función es la función en sí misma: el envío del método del mismo
nombre en árboles de herencia múltiple es relativamente raro en los programas reales de Python, y lo suficientemente
oscuro como para haber generado mucha controversia y muchos malentendidos en torno a esta función. Las personas
no usan Python de la misma manera que usan C++, Java o Dylan, y las lecciones de otros lenguajes similares no se
aplican necesariamente.

También tenga en cuenta que el uso de super hace que el comportamiento de su programa dependa del algoritmo MRO,
un procedimiento que hemos cubierto solo informalmente aquí debido a su complejidad, que es artificial para el propósito
de su programa y que parece documentado y entendido concisamente en el Mundo pitón. Como hemos visto, incluso si
comprende el MRO, sus implicaciones en la personalización, el acoplamiento y la flexibilidad son notablemente sutiles.
Si no comprende completamente este algoritmo, o tiene objetivos que su aplicación no aborda, es mejor que no confíe
en él para desencadenar acciones implícitamente en su código.

O, para citar un lema de Python de su importación este credo:

Si la implementación es difícil de explicar, es una mala idea.

La llamada súper parece firmemente en esta categoría. La mayoría de los programadores no usarán una herramienta
arcana dirigida a un caso de uso raro, sin importar cuán inteligente pueda ser. Esto es especialmente cierto en un
lenguaje de secuencias de comandos que se anuncia a sí mismo como amigable para los no especialistas.
Lamentablemente, el uso por parte de cualquier programador puede imponer dicha herramienta a otros de todos modos:
la verdadera razón por la que lo he cubierto aquí, y un tema que revisaremos al final de este libro.

Como de costumbre, el tiempo y la base de usuarios dirán si las compensaciones o el impulso de esta llamada conducen
a una adopción más amplia o no. Como mínimo, también le conviene conocer la técnica tradicional de llamada de
superclase de nombre explícito, ya que todavía se usa comúnmente y, a menudo, es más simple o

La función súper integrada: ¿para bien o para mal? | 1063


Machine Translated by Google

requerido en la programación de Python del mundo real de hoy. Si elige usar esta herramienta, mi propio consejo para los
lectores es que recuerden que usar super:

• En el modo de herencia única , puede enmascarar problemas posteriores y provocar problemas inesperados.
comportamiento a medida que crecen los árboles

• En el modo de herencia múltiple trae consigo una complejidad sustancial para un atípico
Caso de uso de Python

Para conocer otras opiniones sobre el super de Python que detallan tanto lo bueno como lo malo, busque en la Web artículos
relacionados. Puede encontrar muchos puestos adicionales, aunque al final, el futuro de Python depende tanto del suyo como
de cualquier otro.

Problemas de clase

Hemos llegado al final de la cobertura principal de OOP en este libro. Después de las excepciones, exploraremos ejemplos y
temas adicionales relacionados con la clase en la última parte del libro, pero esa parte en su mayoría solo brinda una
cobertura ampliada a los conceptos presentados aquí. Como de costumbre, terminemos esta parte con las advertencias
estándar sobre las trampas que se deben evitar.

La mayoría de los problemas de clase se pueden reducir a problemas de espacio de nombres, lo cual tiene sentido, dado que
las clases son solo espacios de nombres con algunos trucos adicionales. Algunos de los elementos de esta sección se
parecen más a indicadores de uso de clases que a problemas, pero incluso las clases experimentadas
Se sabe que los codificadores se tropiezan con algunos.

Cambiar los atributos de clase puede tener efectos secundarios Teóricamente

hablando, las clases (y las instancias de clase) son objetos mutables . Al igual que con las listas y los diccionarios integrados,
puede cambiarlos asignándolos a sus atributos, y al igual que con las listas y los diccionarios, esto significa que cambiar una
clase o un objeto de instancia puede afectar múltiples referencias a él.

Por lo general, eso es lo que queremos, y es cómo los objetos cambian su estado en general, pero la conciencia de este
problema se vuelve especialmente crítica cuando se cambian los atributos de clase. Debido a que todas las instancias
generadas a partir de una clase comparten el espacio de nombres de la clase, cualquier cambio a nivel de clase se refleja en
todas las instancias, a menos que tengan sus propias versiones de los atributos de clase modificados.

Debido a que las clases, los módulos y las instancias son solo objetos con espacios de nombres de atributos, normalmente
puede cambiar sus atributos en tiempo de ejecución mediante asignaciones. Considere la siguiente clase. Dentro del cuerpo
de la clase, la asignación al nombre a genera un atributo Xa, que vive en el objeto de la clase en tiempo de ejecución y será
heredado por todas las instancias de X:

>>> clase X:
un = 1 # Atributo de clase

>>> Yo = X()
>>> Yo # Heredado por instancia

1064 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

1 >>> Xa
1

Hasta ahora, todo bien, este es el caso normal. Pero observe lo que sucede cuando cambiamos el atributo
de clase dinámicamente fuera de la declaración de clase : también cambia el atributo en cada objeto que
hereda de la clase. Además, las nuevas instancias creadas a partir de la clase durante esta sesión o
ejecución del programa también obtienen el valor establecido dinámicamente, independientemente de lo que
diga el código fuente de la clase:

>>> Xa = 2 >>> # Puede cambiar más de X


Ia # yo tambien cambio
2
>>> J = X() # J hereda de los valores de tiempo de ejecución
>>> Ya 2 de X # (pero la asignación a Ja cambia a en J, no en X ni en I)

¿Es esta una característica útil o una trampa peligrosa? Sea usted el juez. Como aprendimos en el Capítulo
27, puede hacer el trabajo cambiando los atributos de clase sin tener que crear una sola instancia, una
técnica que puede simular el uso de "registros" o "estructuras" en otros lenguajes. Como repaso, considere
el siguiente programa de Python inusual pero legal:

clase X: aprobado # Hacer algunos espacios de nombres de atributos


clase Y: aprobado

Xa = 1 Xb # Usar atributos de clase como variables


=2 # No hay instancias en ningún lado para ser encontradas
Xc = 3 Ya
= Xa + Xb + Xc

para Xi en el rango (Ya): imprimir (Xi) # Imprime 0..5

Aquí, las clases X e Y funcionan como módulos "sin archivos": espacios de nombres para almacenar variables
que no queremos que coincidan. Este es un truco de programación de Python perfectamente legal, pero es
menos apropiado cuando se aplica a clases escritas por otros; no siempre puede estar seguro de que los
atributos de clase que cambie no sean críticos para el comportamiento interno de la clase. Si desea simular
una estructura C, es mejor que cambie las instancias que las clases, ya que de esa manera solo se ve
afectado un objeto:

Registro de clase: pase X


= Registro ()
X.nombre = 'bob'
X.trabajo = 'Pizzero'

Cambiar los atributos de clase mutable también puede tener efectos secundarios

Este problema es realmente una extensión del anterior. Debido a que los atributos de clase son compartidos
por todas las instancias, si un atributo de clase hace referencia a un objeto mutable, cambiar ese objeto en
su lugar desde cualquier instancia afecta a todas las instancias a la vez:

Problemas de clase | 1065


Machine Translated by Google

>>> clase C:
compartido = [] # Atributo de clase
def __init__(auto):
self.perobj = [] # Atributo de instancia

>>> x = C() >>> # Dos instancias


y = C() >>> # Implícitamente compartir atributos de clase

y.compartido, y.perobj
([], [])

>>> x.shared.append('spam') >>> # ¡Impacta la vista de usted también!

x.perobj.append('spam') >>> x.shared, # Afecta solo a los datos de x


x.perobj
(['correo no deseado'], ['correo no deseado'])

>>> y.compartido, y.perobj # y ve el cambio realizado a través de x


(['spam'], [])
>>> C.compartido # Almacenado en clase y compartido

['spam']

Este efecto no es diferente de muchos que ya hemos visto en este libro: objetos mutables
son compartidos por variables simples, los globales son compartidos por funciones, objetos a nivel de módulo
son compartidos por múltiples importadores, y los argumentos de funciones mutables son compartidos por el
llamante y el llamado. Todos estos son casos de comportamiento general: múltiples referencias a
un objeto mutable, y todos se ven afectados si el objeto compartido se cambia en su lugar de
cualquier referencia. Aquí, esto ocurre en los atributos de clase compartidos por todas las instancias a través de la
herencia, pero es el mismo fenómeno en el trabajo. Puede ser hecho más sutil por la
diferente comportamiento de las asignaciones a los atributos de instancia en sí mismos:

x.shared.append('spam') # Cambia el objeto compartido adjunto a la clase en su lugar


x.compartido = 'correo no deseado' # Cambia o crea un atributo de instancia adjunto a x

Pero, de nuevo, esto no es un problema, es solo algo a tener en cuenta; clase mutable compartida
Los atributos pueden tener muchos usos válidos en los programas de Python.

Herencia múltiple: el orden importa


Esto puede ser obvio ahora, pero vale la pena subrayarlo: si usa herencia múltiple, el orden en que se enumeran las
superclases en el encabezado de declaración de clase puede ser
crítico. Python siempre busca superclases de izquierda a derecha, según su orden
en la línea de encabezado.

Por ejemplo, en el ejemplo de herencia múltiple que estudiamos en el Capítulo 31, supongamos
que la clase Super también implementó un método __str__ :
clase Árbol de lista:
def __str__(uno mismo): ...

clase súper:
def __str__(uno mismo): ...

class Sub(ListTree, Super): # Obtener el __str__ de ListTree enumerándolo primero

1066 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

x = sub() # La herencia busca ListTree antes que Super

¿De qué clase lo heredaríamos, ListTree o Super? Como las búsquedas de herencia proceden de izquierda a
derecha, obtendríamos el método de la clase que aparece primero
(más a la izquierda) en el encabezado de clase de Sub . Presumiblemente, enumeraríamos ListTree primero porque su
todo el propósito es su costumbre __str__ (de hecho, tuvimos que hacer esto en el Capítulo 31 cuando
mezclando esta clase con un tkinter.Button que tenía un __str__ propio).

Pero ahora supongamos que Super y ListTree tienen sus propias versiones de otros del mismo nombre
atributos, también. Si queremos un nombre de Super y otro de ListTree, el orden
en el que los enumeramos en el encabezado de la clase no ayudará; tendremos que anular la herencia asignando
manualmente el nombre del atributo en la Subclase :

clase Árbol de lista:


def __str__(uno mismo): ...
def otro(yo): ...

clase súper:
def __str__(uno mismo): ...
def otro(yo): ...

class Sub(ListTree, Super): # Obtener el __str__ de ListTree enumerándolo primero


otro = Super.otro def # Pero elija explícitamente la versión de Super de otros
__init__(self):
...

x = sub() # La herencia busca Sub antes que ListTree/ Super

Aquí, la asignación a otro dentro de la clase Sub crea Sub.otro, una referencia hacia atrás
al objeto Super.otro . Debido a que está más bajo en el árbol, Sub.other oculta efectivamente
ListTree.other, el atributo que normalmente encontraría la búsqueda de herencia. De manera similar, si enumeramos
Super primero en el encabezado de la clase para seleccionar el otro, necesitaríamos
seleccione el método de ListTree explícitamente:

clase Sub(Super, ListTree): __str__ # Consigue el otro de Super por pedido


= Lister.__str__ # Elija explícitamente Lister.__str__

La herencia múltiple es una herramienta avanzada. Incluso si entendiste el último párrafo,


sigue siendo una buena idea usarlo con moderación y cuidado. De lo contrario, el significado de un nombre
puede llegar a depender del orden en que se mezclan las clases en una subclase arbitrariamente alejada. (Para ver
otro ejemplo de la técnica que se muestra aquí en acción, vea el
discusión de la resolución explícita de conflictos en "El modelo de clase 'nuevo estilo'", así como
la súper cobertura anterior).

Como regla general, la herencia múltiple funciona mejor cuando las clases combinadas son tan
autosuficientes como sea posible, ya que pueden usarse en una variedad de contextos,
no debe hacer suposiciones sobre nombres relacionados con otras clases en un árbol. La función de atributos
pseudoprivados __X que estudiamos en el Capítulo 31 puede ayudar a localizar nombres
que una clase se basa en poseer y limitar los nombres que sus clases mixtas agregan al
mezcla. En este ejemplo, por ejemplo, si ListTree solo significa exportar su personalizado

Problemas de clase | 1067


Machine Translated by Google

__str__, puede nombrar su otro método __other para evitar conflictos con clases del mismo nombre
en el árbol.

Ámbitos en métodos y clases Al calcular el

significado de los nombres en el código basado en clases, es útil recordar que las clases introducen ámbitos locales,
al igual que las funciones, y los métodos son simplemente otras funciones anidadas. En el siguiente ejemplo, la función
de generación devuelve una instancia de la clase Spam anidada . Dentro de su código, el nombre de clase Spam se
asigna en el ámbito local de la función generada y, por lo tanto, es visible para cualquier otra función anidada, incluido
el código dentro del método ; es la E en la regla de búsqueda de alcance "LEGB":

def generar(): clase


Spam: # Spam es un nombre en el ámbito local de generar
cuenta = 1
def method(self):
print(Spam.count) # Visible en el ámbito de generación, según la regla LEGB (E) return
Spam()

generar().método()

Este ejemplo funciona en Python desde la versión 2.2 porque los ámbitos locales de todas las definiciones de funciones
adjuntas son automáticamente visibles para las definiciones anidadas ( incluidas las definiciones de métodos anidados,
como en este ejemplo).

Aun así, tenga en cuenta que el método defs no puede ver el ámbito local de la clase envolvente; solo pueden ver los
ámbitos locales de las definiciones adjuntas. Es por eso que los métodos deben pasar por la instancia propia o el
nombre de la clase para hacer referencia a los métodos y otros atributos definidos en la instrucción de clase adjunta.
Por ejemplo, el código del método debe usar self.count o Spam.count, no solo contar.

Para evitar el anidamiento, podríamos reestructurar este código de modo que la clase Spam se defina en el nivel
superior del módulo: la función de método anidado y la generación de nivel superior encontrarán Spam en sus ámbitos
globales; no está localizado en el ámbito de una función, pero aún es local en un solo módulo: def generar (): devolver
Spam ()

correo no deseado de clase: # Definir en el nivel superior del módulo


cuenta = 1
método def (auto):
imprimir (Spam.count) # Obras: en global (módulo envolvente)

generar().método()

De hecho, este enfoque se recomienda para todas las versiones de Python: el código tiende a ser más simple en
general si evita anidar clases y funciones. Por otro lado, el anidamiento de clases es útil en contextos de cierre , donde
el alcance de la función envolvente retiene el estado utilizado por la clase o sus métodos. A continuación, el método
anidado tiene acceso a su propio alcance,

1068 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

el alcance de la función envolvente (para la etiqueta), el alcance global del módulo envolvente, cualquier cosa guardada en la
instancia propia por la clase y la clase misma a través de su nombre no local: >>> def generar (etiqueta): clase Spam:

cuenta = 1 def method(self): print("%s=%s" % (etiqueta,


# Devuelve Spam.count))
una clase en lugar dereturn Spam
una instancia

>>> una clase = generar ('Gotchas')


>>> I = una clase()
>>> I.método()
errores = 1

Problemas varios de clase


Aquí hay un puñado de advertencias adicionales relacionadas con la clase, principalmente como revisión.

Elija sabiamente el almacenamiento por instancia

o clase De manera similar, tenga cuidado cuando decida si un atributo debe almacenarse en una clase o sus instancias: el
primero es compartido por todas las instancias, y el último diferirá por instancia. Esto puede ser un problema de diseño crucial
en la práctica. En un programa GUI, por ejemplo, si desea que la información sea compartida por todos los objetos de clase de
ventana que creará su aplicación (por ejemplo, el último directorio utilizado para una operación Guardar o una contraseña ya
ingresada), debe almacenarse como datos a nivel de clase; si se almacena en la instancia como atributos propios , variará
según la ventana o se perderá por completo cuando se busque en la herencia.

Por lo general, desea llamar a los constructores de

superclases . Recuerde que Python ejecuta solo un método constructor __init__ cuando se crea una instancia: el más bajo en
el árbol de herencia de clases. No ejecuta automáticamente los constructores de todas las superclases superiores. Debido a
que los constructores normalmente realizan el trabajo de inicio requerido, generalmente necesitará ejecutar un constructor de
superclase desde un constructor de subclase, usando una llamada manual a través del nombre de la superclase (o super),
pasando los argumentos necesarios, a menos que pretenda reemplazar el el constructor de super en conjunto, o la superclase
no tiene ni hereda un constructor en absoluto.

Clases basadas en delegación en 3.X: __getattr__ y funciones

integradas Otro recordatorio: como se describió anteriormente en este capítulo y en otros lugares, las clases que usan el
método de sobrecarga del operador __getattr__ para delegar la obtención de atributos a objetos envueltos pueden fallar en
Python 3.X (y 2.X cuando se usan clases de nuevo estilo) a menos que los métodos de sobrecarga de operadores se redefinan
en la clase contenedora. Los nombres de los métodos de sobrecarga de operadores extraídos implícitamente por operaciones
integradas no se enrutan a través de métodos genéricos de interceptación de atributos. Para evitar esto, debe redefinir tales

Problemas de clase | 1069


Machine Translated by Google

métodos en clases contenedoras, ya sea manualmente, con herramientas o por definición en superclases;
Veremos cómo en el capítulo 40.

KISS Revisited: "Sobreenvoltura-itis"


Cuando se usa bien, las funciones de reutilización de código de OOP lo hacen excelente para reducir el tiempo
de desarrollo. A veces, sin embargo, se puede abusar del potencial de abstracción de OOP hasta el punto de
hacer que el código sea difícil de entender. Si las clases están en capas demasiado profundas, el código puede
volverse oscuro; puede que tenga que buscar a través de muchas clases para descubrir lo que hace una
operación.

Por ejemplo, una vez trabajé en una tienda de C++ con miles de clases (algunas generadas por máquina) y
hasta 15 niveles de herencia. Descifrar las llamadas a métodos en un sistema tan complejo a menudo era una
tarea monumental: había que consultar varias clases incluso para las operaciones más básicas. De hecho, la
lógica del sistema estaba tan profundamente envuelta que comprender un fragmento de código en algunos
casos requería días de lectura de archivos relacionados. ¡Obviamente, esto no es ideal para la productividad
del programador!

La regla general más general de la programación de Python también se aplica aquí: no compliques las cosas
a menos que realmente deban serlo. Envolver su código en varias capas de clases hasta el punto de que sea
incomprensible siempre es una mala idea. La abstracción es la base del polimorfismo y la encapsulación, y
puede ser una herramienta muy efectiva cuando se usa bien.
Sin embargo, simplificará la depuración y facilitará el mantenimiento si hace que las interfaces de su clase
sean intuitivas, evite que su código sea demasiado abstracto y mantenga las jerarquías de su clase cortas y
planas, a menos que haya una buena razón para hacerlo de otra manera. Recuerde: el código que escribe es
generalmente código que otros deben leer. Consulte el Capítulo 20 para obtener más información sobre KISS.

Resumen del capítulo


Este capítulo presentó una variedad de temas avanzados relacionados con las clases, incluidos tipos
integrados de subclases, clases de nuevo estilo, métodos estáticos y decoradores. La mayoría de estos son
extensiones opcionales del modelo OOP en Python, pero pueden volverse más útiles a medida que comienza
a escribir programas orientados a objetos más grandes, y son un juego justo si aparecen en un código que
debe comprender. Como se mencionó anteriormente, nuestra discusión de algunas de las herramientas de
clase más avanzadas continúa en la parte final de este libro; asegúrese de mirar hacia adelante si necesita
más detalles sobre propiedades, descriptores, decoradores y metaclases.

Este es el final de la parte de clase de este libro, por lo que encontrará los ejercicios de laboratorio habituales
al final del capítulo: asegúrese de trabajar con ellos para practicar la codificación de clases reales. En el
próximo capítulo, comenzaremos a analizar nuestro último tema central del lenguaje, las excepciones: el
mecanismo de Python para comunicar errores y otras condiciones a su código. Este es un tema relativamente
ligero, pero lo dejé para el final porque se supone que las nuevas excepciones se codifican como clases hoy.
Sin embargo, antes de abordar ese tema central final, eche un vistazo a la prueba de este capítulo y los
ejercicios de laboratorio.

1070 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

Pon a prueba tus conocimientos: Cuestionario

1. Mencione dos formas de extender un tipo de objeto incorporado.

2. ¿Para qué se utilizan los decoradores de funciones y clases?

3. ¿Cómo se codifica una clase de estilo nuevo?

4. ¿En qué se diferencian las clases clásicas y las de estilo nuevo?

5. ¿En qué se diferencian los métodos normal y estático?

6. ¿Las herramientas como __slots__ y super son válidas para usar en su código?

7. ¿Cuánto tiempo debe esperar antes de lanzar una "Granada de mano sagrada"?

Ponga a prueba sus conocimientos: Respuestas

1. Puede incrustar un objeto integrado en una clase contenedora o subclasificar el tipo integrado
directamente. El último enfoque tiende a ser más simple, ya que la mayor parte del comportamiento
original se hereda automáticamente.

2. Los decoradores de funciones generalmente se usan para administrar una función o método, o agregarle una capa de lógica
que se ejecuta cada vez que se llama a la función o método. Se pueden usar para registrar o contar llamadas a una función,
verificar sus tipos de argumentos, etc.
También se utilizan para "declarar" métodos estáticos (funciones simples en una clase que no pasan una instancia cuando
se les llama), así como métodos y propiedades de clase. Los decoradores de clase son similares, pero administran objetos
completos y sus interfaces en lugar de un
Llamada de función.

3. Las clases de nuevo estilo se codifican heredando de la clase integrada del objeto (o cualquier otro tipo integrado). En Python
3.X, todas las clases tienen un nuevo estilo automáticamente, por lo que esta derivación no es necesaria (pero no duele); en
2.X, las clases con esta derivación explícita son de estilo nuevo y las que no la tienen son “clásicas”.

4. Las clases de estilo nuevo buscan el patrón de diamantes de múltiples árboles de herencia de manera diferente: esencialmente
buscan primero en anchura (a lo ancho), en lugar de primero en profundidad (arriba) en los árboles de diamantes. Las clases
de nuevo estilo también cambian el resultado del tipo incorporado para instancias y clases, no ejecutan métodos de obtención
de atributos genéricos como __get attr__ para métodos de operación incorporados y admiten un conjunto de herramientas
adicionales avanzadas que incluyen propiedades, descriptores, listas de atributos de instancia super y __slots__ .

5. Los métodos normales (de instancia) reciben un argumento propio (la instancia implícita), pero los métodos estáticos no. Los
métodos estáticos son funciones simples anidadas en objetos de clase.
Para hacer que un método sea estático, debe ejecutarse a través de una función integrada especial o estar decorado con
sintaxis de decorador. Python 3.X permite llamar a funciones simples en una clase a través de la clase sin este paso, pero
las llamadas a través de instancias aún requieren una declaración de método estático.

6. Por supuesto, pero no debe usar herramientas avanzadas automáticamente sin considerar cuidadosamente sus implicaciones.
Las tragamonedas, por ejemplo, pueden descifrar el código; súper puede máscara

Pon a prueba tus conocimientos: respuestas | 1071


Machine Translated by Google

problemas posteriores cuando se usa para herencia única, y en herencia múltiple trae consigo una
complejidad sustancial para un caso de uso aislado; y ambos requieren un despliegue universal para
ser más útiles. La evaluación de herramientas nuevas o avanzadas es una tarea principal de cualquier
ingeniero, y es por eso que exploramos las ventajas y desventajas con tanto cuidado en este capítulo.
El objetivo de este libro no es decirle qué herramientas usar, sino subrayar la importancia de
analizarlas objetivamente, una tarea que a menudo se le da una prioridad demasiado baja en el
campo del software.

7. Tres segundos. (O, más exactamente: “Y el Señor habló, diciendo: 'Primero sacarás el Alfiler Sagrado.
Luego, contarás hasta tres, ni más, ni menos. Tres será el número que contarás, y el número de la
cuenta será tres. No contarás cuatro, ni tampoco contarás dos, excepto que luego procedas a tres.
Cinco está fuera. Una vez que se alcanza el número tres, que es el tercer número, entonces lanzas
tu Santo Granada de mano de Antioquía hacia tu enemigo, quien, siendo travieso a mis ojos, la
apagará'”).

Pon a prueba tus conocimientos: Ejercicios de la Parte VI

Estos ejercicios le piden que escriba algunas clases y experimente con código existente.
Por supuesto, el problema con el código existente es que debe existir. Para trabajar con la clase set en el
ejercicio 5, extraiga el código fuente de la clase del sitio web de este libro (consulte el prefacio para ver un
puntero) o escríbalo a mano (es bastante breve). Estos programas están empezando a ser más sofisticados,
así que asegúrese de consultar las soluciones al final del libro para obtener sugerencias. Los encontrará
en el Apéndice D, en la Parte VI.

1. Herencia. Escriba una clase llamada Adder que exporte un método add(self, x, y) que imprima un
mensaje "No implementado". Luego, defina dos subclases de Adder que implementen el método add :

sumador de listas

Con un método add que devuelve la concatenación de sus dos argumentos de lista
DictAdder
Con un método de adición que devuelve un nuevo diccionario que contiene los elementos en
sus dos argumentos de diccionario (cualquier definición de adición de diccionario servirá)

Experimente creando instancias de sus tres clases de forma interactiva y llamando a sus métodos
add .

Ahora, extienda su superclase Adder para guardar un objeto en la instancia con un constructor (p.
ej., asigne una lista o un diccionario a self.data ) y sobrecargue el operador + con un método __add__
para enviar automáticamente a sus métodos add (p. ej., X + Y activa X.add(X.data,Y)). ¿Cuál es el
mejor lugar para poner a los constructores y

4. Esta cita es de Monty Python y el Santo Grial (y si no lo sabías, puede ser hora de encontrar
¡una copia!).

1072 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

métodos de sobrecarga de operadores (es decir, en qué clases)? ¿Qué tipo de objetos puede agregar
a sus instancias de clase?

En la práctica, puede que le resulte más fácil codificar sus métodos add para aceptar solo un argumento
real (p. ej., add(self,y)), y agregar ese argumento a los datos actuales de la instancia (p. ej., self.data +
y). ¿Tiene esto más sentido que pasar dos argumentos para agregar? ¿Diría que esto hace que sus
clases estén más "orientadas a objetos"?

2. Sobrecarga del operador. Escriba una clase llamada MyList que sombree ("envuelva") una lista de
Python: debería sobrecargar la mayoría de los operadores y operaciones de lista, incluidos +, indexación,
iteración, división y métodos de lista como agregar y ordenar. Consulte el manual de referencia de
Python u otra documentación para obtener una lista de todos los métodos posibles para admitir.
Además, proporcione un constructor para su clase que tome una lista existente (o una instancia de
MyList ) y copie sus componentes en un atributo de instancia. Experimenta con tu clase de forma
interactiva. Cosas para explorar: a. ¿Por qué es importante copiar el valor inicial aquí? b. ¿Puede usar

un segmento vacío (por ejemplo, start[:]) para copiar el valor inicial si es una instancia de MyList ?

C. ¿Existe una forma general de enrutar llamadas de métodos de lista a la lista envuelta?

d. ¿Puede agregar una lista MyList y una lista normal? ¿Qué tal una lista y una instancia de MyList ?

mi. ¿Qué tipo de objeto deberían devolver operaciones como + y segmentación? ¿Qué pasa con las
operaciones de indexación?

F. Si está trabajando con una versión de Python razonablemente reciente (versión 2.2 o posterior),
puede implementar este tipo de clase contenedora incrustando una lista real en una clase
independiente o extendiendo el tipo de lista integrada con una subclase.
¿Cuál es más fácil y por qué?

3. Subclasificación. Cree una subclase de MyList del ejercicio 2 llamada MyListSub, que extiende MyList
para imprimir un mensaje en stdout antes de cada llamada a la operación + sobrecargada y cuenta el
número de dichas llamadas. MyListSub debe heredar el comportamiento del método básico de MyList.
Agregar una secuencia a MyListSub debería imprimir un mensaje, incrementar el contador para llamadas
+ y realizar el método de la superclase.
Además, introduzca un nuevo método que imprima los contadores de operaciones en la salida estándar
y experimente con su clase de forma interactiva. ¿Sus contadores cuentan las llamadas por instancia o
por clase (para todas las instancias de la clase)? ¿Cómo programarías la otra opción? (Sugerencia:
depende del objeto al que se asignen los miembros de recuento: las instancias comparten los miembros
de clase, pero los miembros propios son datos por instancia) .

4. Métodos de atributos. Escriba una clase llamada Attrs con métodos que intercepten cada calificación de
atributo (tanto recuperaciones como asignaciones) e imprima mensajes que enumeren sus argumentos
en stdout. Cree una instancia de Attrs y experimente calificándola de forma interactiva. ¿Qué sucede
cuando intentas usar la instancia en expresiones? Intente agregar, indexar y dividir la instancia de su
clase. (Nota: un enfoque totalmente genérico basado en __getattr__ funcionará en las clases clásicas
de 2.X pero no en las clases de estilo nuevo de 3.X, que son opcionales en 2.X, por las razones que se
indican en el capítulo

Pon a prueba tus conocimientos: Ejercicios de la Parte VI | 1073


Machine Translated by Google

ter 28, Capítulo 31 y Capítulo 32, y resumido en la solución de este ejercicio).

5. Establecer objetos. Experimente con la clase de conjunto descrita en "Extensión de tipos mediante
incrustación". Ejecute comandos para realizar los siguientes tipos de operaciones: a. Cree dos

conjuntos de números enteros y calcule su intersección y unión usando


& y | expresiones de operadores.

b. Cree un conjunto a partir de una cadena y experimente con la indexación de su conjunto. ¿Qué
métodos de la clase se llaman?

C. Intente iterar a través de los elementos en su conjunto de cadenas usando un bucle for . ¿Qué
métodos se ejecutan esta vez?

d. Intente calcular la intersección y la unión de su conjunto de cuerdas y un simple


Cadena de pitón. ¿Funciona?

mi. Ahora, amplíe su conjunto creando subclases para manejar arbitrariamente muchos operandos
usando la forma de argumento *args . (Sugerencia: vea las versiones de función de estos
algoritmos en el Capítulo 18.) Calcule intersecciones y uniones de múltiples operandos con su
subclase establecida. ¿Cómo puedes intersecar tres o más conjuntos, dado que & tiene solo dos
lados?

F. ¿Cómo haría para emular otras operaciones de lista en la clase de conjunto? (Sugerencia:
__add__ puede detectar la concatenación, y __getattr__ puede pasar la mayoría de las llamadas
a métodos de lista con nombre, como agregar a la lista envuelta).

6. Enlaces del árbol de clases. En "Espacios de nombres: toda la historia" en el capítulo 29 y en "Herencia
múltiple: clases 'mixtas'" en el capítulo 31, aprendimos que las clases tienen un atributo __bases__ que
devuelve una tupla de sus objetos de superclase (los que se enumeran entre paréntesis). en el
encabezado de la clase). Use __bases__ para extender las clases mixtas de lister.py que escribimos
en el Capítulo 31 para que impriman los nombres de las superclases inmediatas de la clase de la
instancia. Cuando haya terminado, la primera línea de la representación de la cadena debería verse así
(su dirección casi seguramente variará):

<Instancia de Sub(Super, Lister), domicilio 7841200: 7.

Composición. Simule un escenario de pedido de comida rápida definiendo cuatro clases:


Almuerzo

Una clase de contenedor y controlador

Cliente
El actor que compra comida.

Empleado
El actor a quien un cliente ordena

Alimento

Lo que compra el cliente

Para comenzar, estas son las clases y los métodos que definirá:
Almuerzo de
clase: def __init__(self) # Hacer/ incrustar Cliente y Empleado

1074 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

def order(self, foodName) def # Iniciar una simulación de pedido del cliente
result(self) # Preguntar al Cliente que Comida tiene

clase Cliente:
def __init__(self) def # Inicializar mi comida a Ninguno
placeOrder(self, foodName, employee) # Realizar pedido con un empleado def printFood(self)
# Imprimir el nombre de mi comida

class Employee: def


takeOrder(self, foodName) # Devuelve un alimento, con el nombre solicitado

clase Comida:
def __init__(self, nombre) # Nombre de la comida de la tienda

La simulación de orden debería funcionar de la siguiente manera:

una. El constructor de la clase Lunch debe crear e incorporar una instancia de


Cliente y una instancia de Empleado, y debe exportar un método llamado
pedido. Cuando se llama, este método de pedido debe pedirle al Cliente que
realice un pedido llamando a su método placeOrder . El método placeOrder del
Cliente debe, a su vez, solicitar al objeto Employee un nuevo objeto Food
llamando al método takeOrder del Employee.
b. Los objetos de comida deben almacenar una cadena de nombre de comida (por ejemplo, "burritos"), transmitida
desde Lunch.order, a Customer.placeOrder, a Employee.takeOrder, y finalmente al constructor de Food . La
clase de Almuerzo de nivel superior también debe exportar un método llamado resultado, que le pide al cliente
que imprima el nombre de la comida que recibió del Empleado a través del pedido (esto puede usarse para
probar su simulación).

Tenga en cuenta que Lunch necesita pasar el Empleado o él mismo al Cliente para permitir que el Cliente llame a los
métodos del Empleado .

Experimente con sus clases de forma interactiva importando la clase Lunch , llamando a su método de pedido para
ejecutar una interacción y luego llamando a su método de resultado para verificar que el Cliente obtuvo lo que pidió.
Si lo prefiere, también puede simplemente codificar casos de prueba como código de autoevaluación en el archivo
donde se definen sus clases, usando el truco del módulo __name__ del Capítulo 25. En esta simulación, el Cliente
es el agente activo; ¿Cómo cambiarían sus clases si el Empleado fuera el objeto que inició la interacción cliente/
empleado?

8. Jerarquía de animales de zoológico. Considere el árbol de clases que se muestra en la figura 32-1.

Codifique un conjunto de seis declaraciones de clase para modelar esta taxonomía con la herencia de Python .
Luego, agregue un método de habla a cada una de sus clases que imprima un mensaje único y un método de
respuesta en su superclase Animal de nivel superior que simplemente llame a self.speak para invocar la impresora
de mensajes específica de la categoría en una subclase a continuación (esto iniciar una búsqueda de herencia
independiente de uno mismo). Finalmente, elimine el método de hablar de su clase Hacker para que tome el valor
predeterminado que se encuentra arriba. Cuando hayas terminado, tus clases deberían funcionar de esta manera:

% python
>>> from zoo import Cat, Hacker

Pon a prueba tus conocimientos: Ejercicios de la Parte VI | 1075


Machine Translated by Google

>>> lugar = Gato()


>>> lugar.respuesta() # Animal.reply: llamadas Cat.speak
maullar

>>> datos = Hacker() # Animal.reply: llama a Primate.speak


>>> datos.respuesta()
¡Hola Mundo!

Figura 32-1. Una jerarquía de zoológico compuesta de clases vinculadas en un árbol para buscar por herencia de
atributos. Animal tiene un método de "respuesta" común, pero cada clase puede tener su propio método personalizado
de "hablar" llamado "respuesta".

9. El boceto del loro muerto. Considere la estructura de incrustación de objetos capturada en


Figura 32-2.
Codifique un conjunto de clases de Python para implementar esta estructura con
composición. Codifique su objeto Escena para definir un método de acción e incruste
instancias de las clases Cliente, Empleado y Parrot (cada una de las cuales debe definir un
método de línea que imprima un mensaje único). Los objetos incrustados pueden heredar
de una superclase común que define la línea y simplemente proporcionar el texto del
mensaje, o definir la línea ellos mismos. Al final, tus clases deberían funcionar así:
% python
>>> import loro >>>
loro.Escena().acción() cliente: "¡Ese # Activar objetos anidados
es un ex-pájaro!" empleado: "no, no lo
es..." loro: ninguno

1076 | Capítulo 32: Temas de clase avanzada


Machine Translated by Google

Figura 32-2. Un compuesto de escena con una clase de controlador (Escena) que incrusta y dirige instancias de otras tres clases (Cliente, Empleado, Loro). Las

clases de la instancia incrustada también pueden participar en una jerarquía de herencia; la composición y la herencia suelen ser formas igualmente útiles de

estructurar clases para la reutilización de código.

Por qué te importará: Programación orientada a objetos

por los maestros Cuando enseño clases de Python, invariablemente descubro que aproximadamente a la mitad
de la clase, las personas que han usado programación orientada a objetos en el pasado lo siguen intensamente,
mientras que las personas que no lo han hecho comienzan a mirar fijamente (o cabecear por completo). El punto
detrás de la tecnología simplemente no es evidente.

En un libro como este, tengo el lujo de incluir material como la nueva descripción general de Big Picture en el
Capítulo 26 y el tutorial gradual del Capítulo 28; de hecho, probablemente debería revisar esa sección si
comienza a sentir que OOP es solo un poco de galimatías de informática. Aunque agrega mucha más
estructura que los generadores que conocimos anteriormente, OOP también se basa en algo de magia
(búsqueda de herencia y un primer argumento especial) que los principiantes pueden encontrar difíciles de
racionalizar.

En clases reales, sin embargo, para ayudar a que los recién llegados participen (y mantenerlos despiertos),
se sabe que me detengo y pregunto a los expertos en la audiencia por qué usan OOP. Las respuestas que
han dado pueden arrojar algo de luz sobre el propósito de la programación orientada a objetos, si eres nuevo
en el tema.

Aquí, entonces, con solo unos pocos adornos, están las razones más comunes para usar la POO, citadas por
mis alumnos a lo largo de los años:

Reutilización
de código Esta es fácil (y es la razón principal para usar OOP). Al admitir la herencia, las clases le
permiten programar por personalización en lugar de comenzar cada proyecto desde cero.

Encapsulación
Resumir los detalles de implementación detrás de las interfaces de objetos aísla a los usuarios de una
clase de los cambios de código.
Las clases
de estructura proporcionan nuevos ámbitos locales, lo que minimiza los conflictos de nombres. También
proporcionan un lugar natural para escribir y buscar código de implementación y para administrar el
estado del objeto.

Pon a prueba tus conocimientos: Ejercicios de la Parte VI | 1077


Machine Translated by Google

Mantenimiento
Las clases promueven naturalmente la factorización de código, lo que nos permite minimizar la
redundancia. Gracias a la estructura y al soporte de reutilización de código de las clases, por lo
general solo se necesita cambiar una copia del código.

Las clases de
consistencia y la herencia le permiten implementar interfaces comunes y, por lo tanto, crear una
apariencia común en su código; esto facilita la depuración, la comprensión y el mantenimiento.

Polimorfismo Esto
es más una propiedad de OOP que una razón para usarlo, pero al admitir la generalidad del código,
el polimorfismo hace que el código sea más flexible y ampliamente aplicable y, por lo tanto, más
reutilizable.

Otro
Y, por supuesto, la razón número uno que dieron los estudiantes para usar OOP: ¡se ve bien en un
currículum! (Está bien, lancé esto como una broma, pero es importante estar familiarizado con la
programación orientada a objetos si planea trabajar en el campo del software hoy).

Finalmente, tenga en cuenta lo que dije al comienzo de esta parte del libro: no apreciará plenamente la
programación orientada a objetos hasta que la haya usado por un tiempo. Elija un proyecto, estudie
ejemplos más grandes, trabaje en los ejercicios: haga lo que sea necesario para mojarse los pies con el
código OO; vale la pena el esfuerzo

1078 | Capítulo 32: Temas de clase avanzada

También podría gustarte