Programacion 1
Programacion 1
CAPÍTULO 27
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.
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
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.
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.
• 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:
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
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):
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:
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
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:
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:
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.
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 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.
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.
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:
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:
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()
O equivalente:
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:
tenemos que pasar por el módulo para buscar la clase como de costumbre:
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 :
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:
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.
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.
• 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
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
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:
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.
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
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__.
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.
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).
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
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.
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
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)
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()
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:
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:
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
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
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 ):
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:
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:
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
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.
Beto
>>> 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
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.
>>>
>>> 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
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
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
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:
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.
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.
9. ¿Cuáles son los dos conceptos clave necesarios para comprender el código Python OOP?
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
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.
CAPÍTULO 28
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.
• Persona: una clase que crea y procesa información sobre personas • Gerente: una
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
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:
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
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:
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.
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
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:
Persona de clase:
def __init__(self, nombre, trabajo=Ninguno, pago=0):
self.name = nombre self.job = trabajo self.pay = pago
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.
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.
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
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:
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
>>>
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á
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:
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):
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:
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!
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'
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):
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:
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
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
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:
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
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:
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>
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
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]
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:
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.
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):
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.
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
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:
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.
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:
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.
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...)
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:
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]
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)
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.
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
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]
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.
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:
tom = Gerente()
tom.apellido() # Heredado palabra por palabra
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.
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.
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.
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.
# 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
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)
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)
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:
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.
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.
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
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
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...
si __nombre__ == '__principal__':
bob = Persona('Bob Smith') sue =
Persona('Sue Jones', trabajo='dev', pago=100000) tom = Gerente('Tom
Jones', 50000)
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]
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
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.
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...
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
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!
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.
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,
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:
pago => 0
trabajo =>
Ninguno nombre => Bob Smith
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
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:
clase AttrDisplay:
"""
si __nombre__ == '__principal__':
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.
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
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):
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.
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
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.
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...
return self.name.split()[-1]
= int(self.pay * (1 + percent))
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]
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
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.
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
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
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.
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.
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:
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
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
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
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:
>>> 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:
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
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.
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
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
todo "simplemente funciona" para objetos basados en el modelo de herencia de OOP, incluso cuando viven en
un archivo:
estantería de importación
for key in sorted(db): print(key, '\t=>', # Iterar para mostrar los objetos de la base de datos
db[key]) # Imprime con formato personalizado
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
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
>>> 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,
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
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.
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.
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?
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?
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?
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
__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.
CAPÍTULO 29
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:
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:
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.
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...)
class.method(instancia, argumentos...)
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:
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:
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
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:
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...
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
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.
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.
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
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.
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.
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:
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
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.
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
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
method(self): print('starting
Extender.method')
Super.method(self)
print('ending Extender.method')
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
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:
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!
>>> 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!).
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:
class Super(metaclass=ABCMeta):
@abstractmethod def
method(self, ...): pasar
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:
>>> X = Súper()
TypeError: no se puede crear una instancia de clase abstracta Super con acción de métodos abstractos
>>> X = Sub()
TypeError: no se puede instanciar la clase abstracta Sub con la acción de métodos abstractos
>>> 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
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
á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.
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.
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:
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.
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
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
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
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
X = 66
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.
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
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
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()
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()
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.
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:
>>> X = Sub()
>>> X.__dict__ {} # Dict de espacio de nombres de instancia
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()
>>> 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')
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
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.
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
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:
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
.........objeto Árbol
de <__main__.selftest.<locales>.F objeto en
0x00000000029216A0> ...F ......D .........B ... .........A ...............objeto .........C ............A
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
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.
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__)
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__
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)
NOMBRE
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).
• Módulos
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.
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í.
2. ¿Qué sucede cuando aparece una declaración de asignación simple en el nivel superior de un
declaración de clase ?
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?
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, ...).
6. Ashur (o Qalat Sherqat), Calah (o Nimrud), el efímero Dur Sharrukin (o Khorsabad), y finalmente
Nínive.
CAPÍTULO 30
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.
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
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
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.
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.
Llamadas de función
__llamar__ X(*argumentos, **cargos)
__getattr__
Obtención de atributo X.indefinido
__obtiene el objeto__ Indexación, rebanado, iteración X[clave], X[i:j], bucles for y otras iteraciones si no
__iter__
__eq__, __ne__
__í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__
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 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.
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:
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!
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)
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:
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
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]
>>> 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[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:
>>> X = Indexador()
>>> X[99]
indexación 99
>>> X[1:99:2]
rebanado 1 99 2
>>> X[1:]
rebanar 1 Ninguno Ninguno
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.
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
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.
>>> 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:
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.
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 :
>>>
>>> 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:
>>> 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.
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
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.
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
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
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)
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 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:
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:
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:
Mejoraremos esto para admitir múltiples escaneos más directamente adelante, después de un poco de comparación
y contraste.
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
1 4 9 16 25
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.
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:
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.
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.
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
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:
% 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.
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.
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:
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
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
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=' ')
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:
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:
% 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__.
• 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.
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=' ')
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
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=' ')
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):
# Archivo skipper_yield.py
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!
“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
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
regresar a sí mismo
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)))
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
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]:
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:
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.
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.
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 >>>
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í.
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:
'
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 :
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 :
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.
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.
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
• 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.
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
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!
si __nombre__ == '__principal__':
x = Prueba1() y = Prueba2()
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.
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
>>> imprimir(x)
<__main__.objeto sumador en 0x00000000029736D8>
>>> x
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.
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.
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,
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:
>>> 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:
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:
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:
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
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__.
>>> 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)
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):
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.
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__:
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
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:
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
-------------------------------------------------- ----------
...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
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:
>>> 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:
El método __add__ normal se ejecuta como respaldo, pero es posible que no pueda optimizarse en el lugar
casos:
>>> 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.
Se enviaron argumentos opcionales o de palabras clave. Esto permite que las instancias se ajusten a una API basada
en funciones:
>>> 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
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:
>>> 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__ .
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:
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
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
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 :
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:
self.color)
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:
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.
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
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'
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.
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'
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.
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í!
>>> 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:
>>> 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):
>>> 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.
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:
>>> 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
>>> 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.
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.
• 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
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.
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:
capítulo 38 utiliza los métodos de obtención /establecimiento del descriptor de clase __get__ y __set__ .
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.
1. ¿Qué dos métodos de sobrecarga de operadores puede usar para admitir la iteración en su
clases?
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.
CAPÍTULO 31
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.
Comencemos con una revisión: la implementación de Python de OOP se puede resumir en tres ideas:
Herencia
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
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
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.
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 :
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)
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:
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
dispositivos como puertos seriales, placas Arduino y Raspberry Pi si está tomando esta sección
demasiado literalmente).
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:
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")
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
Cuando ejecutamos este módulo, nuestra pizzería maneja dos pedidos, uno de Homer y otro de Shaggy:
...
Ó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.
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
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:
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 :
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'),
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())
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).
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.
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:
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:
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:
>>> obj.orden('LSP')
Pedidos de LSP de <Empleado: nombre=Pat,
salario=40000> Bob hace hornos de pizza
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.
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],
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 :
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.
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.
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.
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.
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)
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:
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
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
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:
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.
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.
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.
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:
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:
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:
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.
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):
>>> 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'
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.
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:
>>> 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
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:
25
7
15
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:
>>> 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
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__.
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
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.
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
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.
Persona de clase:
def __init__(self, nombre, trabajo=Ninguno):
self.name = nombre self.job = trabajo
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.
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.
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:
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.
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
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
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.
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
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: >>>
>>> 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
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.
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 __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.
• 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:
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”):
>>> 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
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
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)
"""
importar importlib
clase súper:
def __init__(self): self.data1 # Superclase __init__
= 'spam' def ham(self): # Crear atributos de instancia
pasar
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!):
datos2=huevos
datos3=42
>
datos2=huevos
datos3=42
>
...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:
>>> 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.
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)
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
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
>
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:
__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:
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>>
>
>
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.
á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)
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:
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).
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:
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:
........>
........>
....>
>
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):
__repr__
__setattr__
__tamaño
de__ __str__
__subclasshook__
............>
........>
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).
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:
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:
........>
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
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:\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__
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'>"
>>> '{0}'.format(str(objeto.__reduce__))
"<método '__reduce__' de objetos 'objeto'>"
c:\code> py ÿ3.3
>>> '%s' % objeto.__reduce__
"<método '__reduce__' de objetos 'objeto'>"
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.
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__
_ListTree__visited={}
_name=43363688
_tclCommands=[] _w=.43363688
children={}
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:
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'> >>
>>> 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.
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í.
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.
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.
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.
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:
(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.
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.
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.
2. ¿Qué es la delegación?
3. ¿Qué es la composición?
4. ¿Qué son los métodos enlazados?
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.
CAPÍTULO 32
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
¿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
res.append(x)
conjunto de retorno (res)
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]
Para usar esta clase, creamos instancias, llamamos a métodos y ejecutamos operadores definidos como de costumbre:
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,
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:
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
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
% python typesubclass.py
['a B C']
['a B C']
(indexando ['a', 'b', 'c'] en 1)
a
(indexando ['a', 'b', 'c'] en 3)
C
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:
self.append(x)
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.
• 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.
• 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):
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.
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
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.
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.
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!
Dichos métodos deben redefinirse en aras de la distribución integrada diferente en las nuevas clases de estilo.
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.
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.
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í.
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.
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.
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.
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.
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.
's'
>>> imprimir(X) # Classic no hereda por defecto
__str__ spam
>>> 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
Este comportamiento termina siendo heredado por el método de interceptación de atributos __getattr__ :
>>> 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'
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:
>>> 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 + '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:
>>> X = C()
>>> X.upper
getattr: superior
<método integrado superior del objeto str en 0x0233D670> >>> X.upper() getattr:
superior 'SPAM'
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
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.
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
>>> 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__'
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
>>> 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
>>> 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.
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
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
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
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.
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)
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
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.
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'>)
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
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__']
>>> 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
>>> X = C()
>>> X.__repr__
<envoltura de método '__repr__' del objeto C en 0x00000000020B5978>
c:\código> py ÿ3
>>> 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.
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
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.
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.
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:
>>> 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
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.
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:
>>> 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')
pasar
>>> clase D(B, C): pasar >>> x = # Usar el orden de búsqueda predeterminado
>>> 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:
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!
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.
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
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:
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):
El MRO del siguiente árbol, por ejemplo, es el mismo que el diamante anterior, por
DFLR:
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'>]
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:
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.
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:
"""
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.
import pprint
def trace(X, label='', end='\n'):
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):
"""
Secuencia
""" de orden de herencia: nuevo estilo (MRO) o clásico (DFLR)
si hasattr(instancia.__clase__, '__mro__'):
return (instancia,) + instancia.__clase__.__mro__ más:
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:
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?
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
OBJS
{<clase __main__.A en 0x0000000002248708>: ['attr1'], <clase __main__.B
en 0x0000000002248768>: ['attr2'], <clase __main__.D en
0x0000000002248828>: ['__doc__', '__module__']}
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!
<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'>}
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!
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
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
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.
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!
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__:
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__
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):
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'
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
>>> 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:
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:
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:
d => 4 a
=> 1 b # Menos mal...
=> 2
__dict__ => {'d': 4}
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
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
>>> 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:
>>> dir(X) # Pero dir() incluye todos los nombres de las ranuras
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.
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.
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:
>>> I = Slotful(3)
>>> Ia, Ib = 1, 2 >>>
Ia, Ib, Ic (1, 2, 3) # Obtención normal de atributos
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__
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.
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']
>>> D.__dict__.teclas()
>>> 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.
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__
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']
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:
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:
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!
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
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.
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:
>>> 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
__setattr__ de la superclase de objeto para admitir mejor los atributos "virtuales" como las ranuras y
propiedades codificadas en otras clases):
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:
Sin embargo, para dar sentido a esta sintaxis del decorador, debemos seguir adelante.
>>> 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.
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.
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.
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.
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
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.
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.
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()
>>> 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()
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
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 =
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.
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
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
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
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])
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:
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.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):
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:
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í.
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:
Además, las clases pueden heredar el método estático sin redefinirlo: se ejecuta sin una instancia, independientemente de dónde se
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:
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:
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 )
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:
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:
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.
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
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.
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,
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.
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:
clase Spam:
numInstances = 0 def
__init__(self):
Spam.numInstances = Spam.numInstances + 1
@staticmethod def
printNumInstances(): print("Número
de instancias creadas: %s" % Spam.numInstances)
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
@staticmethod def
smeth(x): print([x]) # Estático: ninguna instancia pasó
@classmethod
def cmeth(cls, x): print([cls, # Clase: obtiene clase, no instancia
x])
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
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.
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:
self.func(*args)
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í:
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 .
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)
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:
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
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)
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:
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 ...
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:
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!).
• 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
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.
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.
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!
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.
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:
>>> 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 .
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.
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')
>>> X = D()
>>> X.act()
huevos de spam
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:
>>> E().método()
<súper: <clase 'E'>, <objeto E>>
correo no deseado
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:
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
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
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:
PyMailServer.__init__(auto)
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.
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:
>>> 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.
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')
>>> 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:
>>> 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:
>>> X = D()
>>> X.act()
huevos de spam
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 .
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.
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:
>>> 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):
>>> 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í.
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.
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.
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
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__
A.__init__
>>> clase D(B, C): paso >>> x # Todavía se ejecuta solo hacia la izquierda
= D()
B.__init__
A.__init__
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__()
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'>)
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.
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__()
>>> 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'>)
>>> 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__
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__
C.__init__
>>> D.__mro__
(<clase '__principal__.D'>, <clase '__principal__.B'>, <clase '__principal__.C'>, <clase 'objeto'>)
>>> 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.
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?
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.
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:
>>> 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:
>>> C().método()
C.método
Mixin.otro
A.otro
B.método
>>> C.__mro__
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()
>>> 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'>)
>>> 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:
# Pero las llamadas directas también funcionan aquí: lo explícito es mejor que lo implícito
>>> 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:
>>> 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
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
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 .
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:
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:
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:
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:
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:
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.
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:
• 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.
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
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.
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
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:
¿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:
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:
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:
>>> clase C:
compartido = [] # Atributo de clase
def __init__(auto):
self.perobj = [] # Atributo de instancia
y.compartido, y.perobj
([], [])
['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:
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.
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): ...
¿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 súper:
def __str__(uno mismo): ...
def otro(yo): ...
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:
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
__str__, puede nombrar su otro método __other para evitar conflictos con clases del mismo nombre
en el árbol.
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":
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 ()
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,
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:
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.
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.
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
métodos en clases contenedoras, ya sea manualmente, con herramientas o por definición en superclases;
Veremos cómo en el capítulo 40.
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.
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.
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"?
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
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á'”).
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!).
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
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
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?
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á):
Cliente
El actor que compra comida.
Empleado
El actor a quien un cliente ordena
Alimento
Para comenzar, estas son las clases y los métodos que definirá:
Almuerzo de
clase: def __init__(self) # Hacer/ incrustar Cliente y Empleado
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
clase Comida:
def __init__(self, nombre) # Nombre de la comida de la tienda
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
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".
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
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.
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