Capítulo 4. Python orientado a objetos
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com
Python es un lenguaje de programación orientado a objetos (OO). Sin embargo, a diferencia de otros lenguajes orientados a objetos, Python no te obliga a utilizar exclusivamente el paradigma orientado a objetos: también admite la programación procedimental, con módulos y funciones, para que puedas seleccionar el mejor paradigma para cada parte de tu programa. El paradigma orientado a objetos te ayuda a agrupar el estado (datos) y el comportamiento (código) en prácticos paquetes de funcionalidad. Además, ofrece algunos mecanismos especializados útiles que se tratan en este capítulo, como la herencia y los métodos especiales. El enfoque procedimental más sencillo, basado en módulos y funciones, puede ser más adecuado cuando no necesites las ventajas1 de la programación orientada a objetos. Con Python, puedes mezclar y combinar paradigmas.
Además de los conceptos básicos de OO, este capítulo abarca las clases base abstractas, los decoradores y las metaclases.
Clases e instancias
Si estás familiarizado con la programación orientada a objetos en otros lenguajes OO como C++ o Java, probablemente conozcas bien las clases y las instancias: una clase es un tipo definido por el usuario, que instancias para construir instancias, es decir, objetos de ese tipo. Python soporta esto a través de sus objetos clase e instancia.
Clases de Python
Una clase es un objeto de Python con las siguientes características:
-
Puedes llamar a un objeto de clase igual que llamarías a una función. La llamada, conocida en como instanciación, devuelve un objeto conocido como instancia de la clase; la clase también se conoce como tipo de la instancia.
-
Una clase tiene atributos de nombre arbitrario que puedes vincular y referenciar.
-
Los valores de los atributos de clase pueden ser descriptores (incluidas las funciones), tratados en "Descriptores", u objetos de datos ordinarios.
-
Los atributos de clase vinculados a funciones también se conocen como métodos de la clase.
-
Un método puede tener cualquiera de los muchos nombres definidos por Python con dos guiones bajos iniciales y dos finales (conocidos como dunder names, abreviatura de "double-underscore names"-el nombre __init__, por ejemplo, se pronuncia "dunder init"). Python llama implícitamente a esos métodos especiales, cuando una clase los proporciona, cuando se producen diversos tipos de operaciones sobre esa clase o sus instancias.
-
Una clase puede heredar de una o más clases, lo que significa que delega en otros objetos de la clase la búsqueda de algunos atributos (incluidos los métodos dunder regulares y ) que no están en la propia clase.
Una instancia de una clase es un objeto Python con atributos de nombre arbitrario que puedes vincular y referenciar. Todo objeto instancia delega en su clase la búsqueda de atributos que no se encuentren en la propia instancia. La clase, a su vez, puede delegar la búsqueda en las clases de las que hereda, si las hay.
En Python, las clases son objetos (valores), que se manejan igual que otros objetos. Puedes pasar una clase como argumento en una llamada a una función, y una función puede devolver una clase como resultado de una llamada. Puedes vincular una clase a una variable, a un elemento de un contenedor o a un atributo de un objeto. Las clases también pueden ser claves de un diccionario. Como las clases son objetos perfectamente ordinarios en Python, a menudo decimos que las clases son objetos de primera clase.
La declaración de clase
La sentencia class es la forma más habitual de crear un objeto clase. class es una sentencia compuesta de una sola cláusula con la siguiente sintaxis:
class
Classname
(
base
-
classes
,
*
,
*
*
kw
)
:
statement
(
s
)
Classname es un identificador: una variable que la sentencia class, al finalizar, vincula (o vuelve a vincular) al objeto class recién creado. Las convenciones de nomenclatura de Python aconsejan utilizar mayúsculas y minúsculas para los nombres de las clases, como Item, PrivilegedUser, MultiUseFacility, etc.
clases-base es una serie de expresiones delimitadas por comas cuyos valores son objetos de clase. Varios lenguajes de programación utilizan nombres diferentes para estos objetos de clase: puedes llamarlos bases, superclases o padres de la clase. Puedes decir que la clase creada hereda de, deriva de, extiende o subclasea sus clases base; en este libro, generalmente utilizamos extender. Esta clase es una subclase directa o descendiente de sus clases base. **kw puede incluir un argumento con nombre metaclass= para establecer la metaclase de la clase,2 como se explica en "Cómo determina Python la metaclase de una clase".
Sintácticamente, incluir clases-base es opcional: para indicar que estás creando una clase sin bases, basta con omitir clases-base (y, opcionalmente, omitir también los paréntesis que la rodean, colocando los dos puntos justo después del nombre de la clase). Toda clase hereda de object, especifiques o no bases explícitas.
La relación de subclase entre clases es transitiva: si C1 extiende a C2 y C2 extiende a C3, entonces C1 extiende a C3. La función incorporada issubclass(C1, C2) acepta dos objetos de clase: devuelve Verdadero cuando C1 extiende a C2, y en caso contrario devuelve Falso. Cualquier clase es una subclase de sí misma; por tanto, issubclass(C, C) devuelve Verdadero para cualquier clase C . En "Herencia" veremos cómo afectan las clases base a la funcionalidad de una clase .
La secuencia no vacía de sentencias con sangría que sigue a la sentencia de clase es el cuerpo de la clase. El cuerpo de una clase se ejecuta inmediatamente como parte de la ejecución de la sentencia class. Hasta que el cuerpo no termine de ejecutarse, el nuevo objeto class aún no existe, y el identificador Classname aún no está ligado (o rebotado). En "Cómo crea una clase una metaclase" encontrarás más detalles sobre lo que ocurre cuando se ejecuta una sentencia class. Ten en cuenta que la sentencia class no crea inmediatamente ninguna instancia de la nueva clase, sino que define el conjunto de atributos compartidos por todas las instancias cuando más tarde crees instancias llamando a la clase.
El cuerpo de la clase
El cuerpo de una clase es donde normalmente se especifican los atributos de la clase; estos atributos pueden ser objetos descriptores (incluidas las funciones) u objetos de datos ordinarios de cualquier tipo. Un atributo de una clase puede ser otra clase; así, por ejemplo, puedes tener una declaración de clase "anidada" dentro de otra declaración de clase.
Atributos de los objetos de clase
Normalmente especifica un atributo de un objeto de clase vinculando un valor a un identificador dentro del cuerpo de la clase. Por ejemplo:
class
C1
:
x
=
23
(
C1
.
x
)
# prints:
23
Aquí, el objeto de clase C1 tiene un atributo llamado x, ligado al valor 23, y C1.x hace referencia a ese atributo. También se puede acceder a estos atributos mediante instancias: c = C1(); print(c.x). Sin embargo, esto no siempre es fiable en la práctica. Por ejemplo, cuando la instancia de clase c tiene un atributo x, a eso es a lo que accede c.x, no al de nivel de clase. Por tanto, para acceder a un atributo de nivel de clase desde una instancia, puede ser mejor utilizar, digamos, print(c.__class__.x).
También puedes vincular o desvincular atributos de clase fuera del cuerpo de la clase. Por ejemplo:
class
C2
:
pass
C2
.
x
=
23
(
C2
.
x
)
# prints:
23
Tu programa suele ser más legible si vinculas los atributos de clase sólo con declaraciones dentro del cuerpo de la clase. Sin embargo, puede ser necesario volver a enlazarlos en otro lugar si quieres llevar información de estado a nivel de clase, en lugar de a nivel de instancia; Python te permite hacerlo, si lo deseas. No hay diferencia entre un atributo de clase ligado en el cuerpo de la clase y otro ligado o rebotado fuera del cuerpo mediante la asignación a un atributo.
Como veremos en breve, todas las instancias de una clase comparten todos sus atributos.
La declaración de clase establece implícitamente algunos atributos de clase. El atributo __name__ es la cadena identificadora del nombre de la clase utilizada en la sentencia class. El atributo __bases__ es la tupla de objetos de clase dados (o implícitos) como clases base en la sentencia class. Por ejemplo, utilizando la clase C1 que acabamos de crear:
(
C1
.
__name__
,
C1
.
__bases__
)
# prints:
C1 (<class 'object'>,)
Una clase también tiene un atributo llamado __dict__, que es la asignación de sólo lectura que la clase utiliza para contener otros atributos (también conocido, informalmente, como el espacio de nombres de la clase).
En las sentencias que se encuentran directamente en el cuerpo de una clase, las referencias a los atributos de la clase deben utilizar un nombre simple, no un nombre totalmente cualificado. Por ejemplo:
class
C3
:
x
=
23
y
=
x
+
22
# must use just x
,
not
C3.x
Sin embargo, en las sentencias dentro de los métodos definidos en el cuerpo de una clase, las referencias a los atributos de la clase deben utilizar un nombre totalmente cualificado, no un nombre simple. Por ejemplo:
class
C4
:
x
=
23
def
amethod
(
self
)
:
(
C4
.
x
)
# must use C4.x or self.x
,
not
just x!
Las referencias a atributos (es decir, expresiones como C.x) tienen una semántica más rica que los enlaces a atributos. En "Conceptos básicos de las referencias a atributos" tratamos en detalle este tipo de referencias .
Definiciones de funciones en el cuerpo de una clase
La mayoría de los cuerpos de clase de incluyen algunas sentencias def, ya que las funciones (conocidas como métodos en este contexto) son atributos importantes para la mayoría de las instancias de clase. Una sentencia def en el cuerpo de una clase obedece a las reglas tratadas en "Funciones". Además, un método definido en el cuerpo de una clase tiene un primer parámetro obligatorio, convencionalmente denominado siempre self, que se refiere a la instancia sobre la que se llama al método. El parámetro self desempeña un papel especial en las llamadas a métodos, como se explica en "Métodos vinculados y no vinculados".
Aquí tienes un ejemplo de clase que incluye una definición de método:
class
C5
:
def
hello
(
self
)
:
(
'
Hello
'
)
Una clase puede definir una serie de métodos dunder especiales relativos a operaciones específicas sobre sus instancias. Hablamos de estos métodos en detalle en "Métodos especiales".
Variables de clase privada
Cuando una sentencia en el cuerpo de una clase (o en un método del cuerpo) utiliza un identificador que empieza (pero no termina) por dos guiones bajos, como __ident, Python cambia implícitamente el identificador a _NombreClase__ident, donde NombreClase es el nombre de la clase. Este cambio implícito permite a una clase utilizar nombres "privados" para atributos, métodos, variables globales y otros fines, reduciendo el riesgo de duplicar accidentalmente nombres utilizados en otros lugares (sobre todo en subclases).
Por convención, los identificadores que empiezan por un guión bajo simple son privados para el ámbito que los vincula, tanto si ese ámbito es una clase como si no. El compilador de Python no aplica esta convención de privacidad: corresponde a los programadores respetarla.
Cadenas de documentación de clases
Si la primera declaración del cuerpo de la clase es un literal de cadena, el compilador vincula esa cadena como la cadena de documentación (o docstring) de la clase. La cadena de documentación de la clase está disponible en el atributo __doc__; si la primera sentencia del cuerpo de la clase no es un literal de cadena, su valor es Ninguno. Consulta "Cadenas de documentación " para obtener más información sobre las cadenas de documentación.
Descriptores
Un descriptor es un objeto cuya clase proporciona uno o más métodos especiales llamados __get__, __set__ o __delete__. Los descriptores que son atributos de clase controlan la semántica de acceso y establecimiento de atributos en instancias de esa clase. A grandes rasgos, cuando accedes a un atributo de instancia, Python obtiene el valor del atributo llamando a __get__ en el descriptor correspondiente, si lo hay. Por ejemplo:
class
Const
:
# class with an overriding descriptor, see later
def
__init__
(
self
,
value
)
:
self
.
__dict__
[
'
value
'
]
=
value
def
__set__
(
self
,
*
_
)
:
# silently ignore any attempt at setting
# (a better design choice might be to raise AttributeError)
pass
def
__get__
(
self
,
*
_
)
:
# always return the constant value
return
self
.
__dict__
[
'
value
'
]
def
__delete__
(
self
,
*
_
)
:
# silently ignore any attempt at deleting
# (a better design choice might be to raise AttributeError)
pass
class
X
:
c
=
Const
(
23
)
x
=
X
(
)
(
x
.
c
)
# prints:
23
x
.
c
=
42
# silently ignored (unless you raise AttributeError)
(
x
.
c
)
# prints:
23
del
x
.
c
# silently ignored again (ditto)
(
x
.
c
)
# prints:
23
Para más detalles, consulta "Conceptos básicos de la referencia de atributos".
Descriptores anulables y no anulables
Cuando la clase de un descriptor proporciona un método especial llamado __set__, el descriptor se conoce como un descriptor anulador (o, utilizando la terminología más antigua y confusa, un descriptor de datos ); cuando la clase del descriptor proporciona __get__ y no __set__, el descriptor se conoce como un descriptor no anulador.
Por ejemplo, la clase de objetos función proporciona __get__, pero no __set__; por tanto, los objetos función son descriptores no anulables. A grandes rasgos, cuando asignas un valor a un atributo de instancia con un descriptor correspondiente que es anulable, Python establece el valor del atributo llamando a __set__ en el descriptor. Para más detalles, consulta "Atributos de los objetos de instancia".
El tercer método dunder del protocolo del descriptor es __delete__, llamado cuando se utiliza la sentencia del en la instancia del descriptor. Si del no está soportado, sigue siendo una buena idea implementar __delete__, lanzando una excepción AttributeError adecuada; de lo contrario, quien llame obtendrá un misterioso AttributeError: __delete__.
Los documentos en línea incluyen muchos más ejemplos de descriptores y sus métodos relacionados.
Instancias
Para crear en una instancia de una clase, llama al objeto clase como si fuera una función. Cada llamada devuelve una nueva instancia cuyo tipo es esa clase:
an_instance
=
C5
()
La función integrada en isinstance(i, C), con una clase como argumento C, devuelve Verdadero cuando i es una instancia de la clase C o de cualquier subclase de C. En caso contrario, isinstance devuelve Falso. Si C es una tupla de tipos(3.10+ o varios tipos unidos mediante el operador | ), isinstance devuelve True si i es una instancia o subclase de cualquiera de los tipos dados, y False en caso contrario.
__init__
Cuando una clase define o hereda un método llamado __init__, la llamada al objeto de clase ejecuta __init__ en la nueva instancia para realizar la inicialización por instancia. Los argumentos pasados en la llamada deben corresponder a los parámetros de __init__, excepto el parámetro self. Por ejemplo, considera la siguiente definición de clase:
class
C6
:
def
__init__
(
self
,
n
)
:
self
.
x
=
n
Así es como puedes crear una instancia de la clase C6:
another_instance
=
C6
(
42
)
Como se muestra en la definición de la clase C6, el método __init__ suele contener sentencias que vinculan atributos de instancia. Un método __init__ no debe devolver un valor distinto de Ninguno; si lo hace, Python lanza una excepción TypeError.
El objetivo principal de __init__ es vincular, y por tanto crear, los atributos de una instancia recién creada. También puedes vincular, volver a vincular o desvincular atributos de instancia fuera de __init__. Sin embargo, tu código es más legible cuando vinculas inicialmente todos los atributos de instancia de la clase en el método __init__.
Cuando __init__ está ausente (y no se hereda de ninguna clase base), debes llamar a la clase sin argumentos, y la nueva instancia no tiene atributos específicos de instancia.
Atributos de los objetos instancia
Una vez que has creado una instancia, puedes acceder a sus atributos (datos y métodos) utilizando el operador punto (.). Por ejemplo:
an_instance
.
hello
(
)
# prints:
Hello
(
another_instance
.
x
)
# prints:
42
Las referencias a atributos como éstas tienen una semántica bastante rica en Python; las tratamos en detalle en "Conceptos básicos de las referencias a atributos".
Puedes dar un atributo a un objeto instancia vinculando un valor a una referencia de atributo. Por ejemplo:
class
C7
:
pass
z
=
C7
(
)
z
.
x
=
23
(
z
.
x
)
# prints:
23
El objeto instancia z tiene ahora un atributo llamado x, vinculado al valor 23, y z.x hace referencia a ese atributo. El método especial __setattr__, si está presente, intercepta cualquier intento de vincular un atributo. (Veremos __setattr__ en la Tabla 4-1.)
Cuando intentas enlazar con un atributo de instancia cuyo nombre corresponde a un descriptor de la clase, el método __set__ del descriptor intercepta el intento: si C7.x fuera un descriptor, z.x=23 ejecutaría type(z).x.__set__(z, 23).
Al crear una instancia se establecen dos atributos de instancia. Para cualquier instancia z, z.__class__ es el objeto de clase al que pertenece z, y z.__dict__ es el mapeo que utiliza z para mantener sus otros atributos. Por ejemplo, para la instancia z que acabamos de crear:
(
z
.
__class__
.
__name__
,
z
.
__dict__
)
# prints:
C7 {'x':23}
Puedes volver a vincular (pero no desvincular) uno o ambos atributos, pero rara vez es necesario.
Para cualquier instancia z, cualquier objeto x y cualquier identificador S (excepto __class__ y __dict__), z.S=x es equivalente a z.__dict__['S']=x (a menos que un método especial __setattr__, o un método especial __set__ de un descriptor anulado, intercepte el intento de enlace). Por ejemplo, refiriéndonos de nuevo a la z que acabamos de crear:
z
.
y
=
45
z
.
__dict__
[
'
z
'
]
=
67
(
z
.
x
,
z
.
y
,
z
.
z
)
# prints:
23 45 67
No hay diferencia entre los atributos de instancia creados asignando a atributos y los creados vinculando explícitamente una entrada en z.__dict__.
El lenguaje de las funciones de fábrica
A menudo es necesario crear instancias de diferentes clases en función de alguna condición, o evitar crear una nueva instancia si una existente está disponible para su reutilización. Un error común es pensar que tales necesidades pueden satisfacerse haciendo que __init__ devuelva un objeto concreto. Sin embargo, este enfoque es inviable: Python lanza una excepción si __init__ devuelve cualquier valor que no sea Ninguno. La mejor forma de implementar la creación flexible de objetos es utilizar una función en lugar de llamar directamente al objeto de la clase. Una función utilizada de este modo se conoce como función de fábrica.
Llamar a una función de fábrica es un enfoque flexible: una función puede devolver una instancia reutilizable existente o crear una nueva instancia llamando a cualquier clase que sea apropiada. Supongamos que tienes dos clases casi intercambiables, CasoEspecial y CasoNormal, y quieres generar de forma flexible instancias de cualquiera de ellas, en función de un argumento. La siguiente función de fábrica appropriate_case, como ejemplo "de juguete", te permite hacer precisamente eso (hablaremos más del parámetro self en "Métodos vinculados y no vinculados"):
class
SpecialCase
:
def
amethod
(
self
)
:
(
'
special
'
)
class
NormalCase
:
def
amethod
(
self
)
:
(
'
normal
'
)
def
appropriate_case
(
isnormal
=
True
)
:
if
isnormal
:
return
NormalCase
(
)
else
:
return
SpecialCase
(
)
aninstance
=
appropriate_case
(
isnormal
=
False
)
aninstance
.
amethod
(
)
# prints:
special
__nuevo
Cada clase tiene (o hereda) un método de clase llamado __new__ (tratamos los métodos de clase en "Métodos de clase"). Cuando llamas a C(*args, **kwds) para crear una nueva instancia de la clase C, Python llama primero a C.__new__(C, *args, **kwds), y utiliza el valor de retorno de __new__ , x, como la nueva instancia creada. A continuación, Python llama a C.__init__(x, *args, **kwds), pero sólo cuando x es efectivamente una instancia de C o de cualquiera de sus subclases (de lo contrario, el estado de xpermanece como lo había dejado __new__ ). Así, por ejemplo, la sentenciax=C(23) equivale a
x
=
C
.
__new__
(
C
,
23
)
if
isinstance
(
x
,
C
)
:
type
(
x
)
.
__init__
(
x
,
23
)
object.__new__ crea una nueva instancia no inicializada de la clase que recibe como primer argumento. Ignora otros argumentos cuando esa clase tiene un método __init__, pero lanza una excepción cuando recibe otros argumentos además del primero, y la clase que es el primer argumento no tiene un método __init__. Cuando anulas __new__ dentro del cuerpo de una clase, no necesitas añadir __new__=classmethod(__new__), ni utilizar un decorador @classmethod, como harías normalmente: Python reconoce el nombre __nuevo__ y lo trata como especial en este contexto. En los casos esporádicos en los que vuelvas a enlazar C.__new__ más adelante, fuera del cuerpo de la clase C, sí tendrás que utilizar C.__new__=classmethod(lo que sea).
__new__ tiene la mayor parte de la flexibilidad de una función de fábrica, como se ha explicado en la sección anterior. __new__ puede elegir entre devolver una instancia existente o crear una nueva, según convenga. Cuando __new__ crea una nueva instancia, suele delegar la creación en object.__new__ o en el método __new__ de otra superclase de C.
El siguiente ejemplo muestra cómo anular el método __new__ de la clase para implementar una versión del patrón de diseño Singleton:
class
Singleton
:
_singletons
=
{
}
def
__new__
(
cls
,
*
args
,
*
*
kwds
)
:
if
cls
not
in
cls
.
_singletons
:
cls
.
_singletons
[
cls
]
=
obj
=
super
(
)
.
__new__
(
cls
)
obj
.
_initialized
=
False
return
cls
.
_singletons
[
cls
]
(Trataremos el super incorporado en "Llamada a métodos de superclase cooperativa") .
Cualquier subclase de Singleton (que no anule __new__) tiene exactamente una instancia. Cuando la subclase define __init__, debe asegurarse de que __init__ es seguro para llamarlo repetidamente (en cada llamada de la subclase) en la única instancia de la subclase.3 En este ejemplo, insertamos el atributo __initialized, establecido en False, cuando __new__ crea realmente una nueva instancia. Los métodos __init__ de las subclases pueden comprobar si self._initialized es False y, en caso afirmativo, establecerlo en True y continuar con el resto del método __init__. Cuando los subsiguientes "creadores" de la instancia singleton vuelvan a llamar a __init__, self._initialized será True, indicando que la instancia ya está inicializada, y __init__ normalmente puede devolver, evitando trabajo repetitivo.
Atributos básicos de referencia
Una referencia de atributo es una expresión de la forma x.nombre, donde x es cualquier expresión y nombre es un identificador llamado nombre del atributo. Muchos objetos de Python tienen atributos, pero una referencia a un atributo tiene una semántica especial y rica cuando x se refiere a una clase o instancia. Los métodos también son atributos, por lo que todo lo que decimos sobre los atributos en general también se aplica a los atributos invocables (es decir, los métodos).
Digamos que x es una instancia de la clase C, que hereda de la clase base B. Ambas clases y la instancia tienen varios atributos (datos y métodos), como se indica a continuación:
class
B
:
a
=
23
b
=
45
def
f
(
self
)
:
(
'
method f in class B
'
)
def
g
(
self
)
:
(
'
method g in class B
'
)
class
C
(
B
)
:
b
=
67
c
=
89
d
=
123
def
g
(
self
)
:
(
'
method g in class C
'
)
def
h
(
self
)
:
(
'
method h in class C
'
)
x
=
C
(
)
x
.
d
=
77
x
.
e
=
88
A pocos nombres de atributos dunder son especiales. C.__name__ es la cadena 'C', el nombre de la clase. C.__bases__ es la tupla (B,), la tupla de las clases base de C. x.__class__ es la clase C a la que pertenece x. Cuando haces referencia a un atributo con uno de estos nombres especiales, la referencia al atributo busca directamente en una ranura dedicada de la clase o del objeto instancia y obtiene el valor que encuentra allí. No puedes desvincular estos atributos. Puedes volver a vincularlos sobre la marcha, cambiando el nombre o las clases base de una clase o la clase de una instancia, pero esta técnica avanzada rara vez es necesaria.
La clase C y la instancia x tienen cada una otro atributo especial: un mapeo llamado __dict__ (típicamente mutable para x, pero no para C). Todos los demás atributos de una clase o instancia4 excepto los pocos especiales, se mantienen como elementos en el atributo __dict__ de la clase o instancia.
Obtener un atributo de una clase
Cuando utilizas la sintaxis C.nombre para referirte a un atributo de un objeto de clase C, la búsqueda se realiza en dos pasos:
-
Cuando 'nombre' es una clave en C .__dict__, C.nombre obtiene el valor v de C.__dict__['nombre']. Entonces, cuando v es un descriptor (es decir, type(v) suministra un método llamado __get__), el valor de C.name es el resultado de llamar a type(v).__get__(v, Ninguno, C). Cuando v no es un descriptor, el valor de C.name es v.
-
Cuando "nombre" no es una clave en C.__dict__, C.nombre delega la búsqueda en las clases base de C, lo que significa que realiza un bucle en las clases antecesoras de Ce intenta la búsqueda del nombre en cada una de ellas (en orden de resolución de métodos, como se explica en "Herencia").
Obtener un atributo de una instancia
Cuando utilizas la sintaxis x.nombre para referirte a un atributo de la instancia x de la clase C, la búsqueda se realiza en tres pasos:
-
Cuando 'nombre' está en C (o en una de las clases antecesoras de C) como nombre de un descriptor anulado v (es decir, type(v) suministra los métodos __get__ y __set__), el valor de x.nombre es el resultado de type(v).__get__(v, x, C).
-
De lo contrario, cuando 'nombre' es una clave en x.__dict__, x.nombre obtiene y devuelve el valor en x.__dict__['nombre'].
-
En caso contrario, x.nombre delega la búsqueda en la clase de x(según el mismo proceso de búsqueda en dos pasos utilizado para C.nombre, como acabamos de detallar):
-
Cuando se encuentra un descriptor v, el resultado global de la búsqueda de atributos es, de nuevo, type(v).__get__(v, x, C).
-
Cuando se encuentra un valor no descriptor v, el resultado global de la búsqueda de atributos es sólo v.
-
Cuando estos pasos de búsqueda no encuentran un atributo, Python lanza una excepción AttributeError. Sin embargo, para las búsquedas de x.nombre, cuando C define o hereda el método especial __getattr__, Python llama a C.__getattr__(x, 'nombre') en lugar de lanzar la excepción. Depende entonces de __getattr__ devolver un valor adecuado o lanzar la excepción apropiada, normalmente AttributeError.
Considera las siguientes referencias de atributos, definidas anteriormente:
(
x
.
e
,
x
.
d
,
x
.
c
,
x
.
b
,
x
.
a
)
# prints:
88 77 89 67 23
x.e y x.d tienen éxito en el paso 2 del proceso de búsqueda de instancias, ya que no hay descriptores implicados y tanto "e" como "d" son claves en x.__dict__. Por lo tanto, las búsquedas no van más allá y devuelven 88 y 77. Las otras tres referencias deben pasar al paso 3 del proceso de búsqueda de instancias y buscar en x.__class__ (es decir, C). x.c y x.b superan el paso 1 del proceso de búsqueda de clases, ya que tanto "c" como "b" son claves de C.__dict__. Por lo tanto, las búsquedas no van más allá, sino que devuelven 89 y 67. x.a llega hasta el paso 2 del proceso de búsqueda de clases, buscando en C.__bases__[0] (es decir, B). a' es una clave en B.__dict__; por lo tanto, x.a finalmente tiene éxito y devuelve 23.
Establecer un atributo
Ten en cuenta que los pasos de búsqueda de atributos sólo se producen, como acabamos de describir, cuando haces referencia a un atributo, no cuando vinculas un atributo. Cuando vinculas un atributo de clase o de instancia cuyo nombre no es especial (a menos que un método __setattr__ o el método __set__ de un descriptor de invalidación intercepte la vinculación de un atributo de instancia), sólo afectas a la entrada __dict__ del atributo (en la clase o instancia, respectivamente). En otras palabras, para la vinculación de atributos, no hay ningún procedimiento de búsqueda, salvo la comprobación de los descriptores de anulación.
Métodos vinculados y no vinculados
El método __get__ de un objeto función puede devolver el propio objeto función o un objeto método vincul ado que envuelve a la función; un método vinculado está asociado a la instancia concreta de la que se obtiene.
En el código de la sección anterior, los atributos f, g y h son funciones; por tanto, una referencia de atributo a cualquiera de ellos devuelve un objeto método que envuelve la función respectiva. Considera lo siguiente:
(
x
.
h
,
x
.
g
,
x
.
f
,
C
.
h
,
C
.
g
,
C
.
f
)
Esta declaración produce tres métodos vinculados, representados por cadenas como:
<bound method C.h of <__main__.C object at 0x8156d5c>>
y luego tres objetos función, representados por cadenas como
<function C.h at 0x102cabae8>
Métodos vinculados frente a objetos de función
Obtenemos métodos vinculados cuando la referencia al atributo está en la instancia x, y objetos función cuando la referencia al atributo está en la clase C.
Como un método vinculado ya está asociado a una instancia concreta, puedes llamar al método de la siguiente manera:
x
.
h
(
)
# prints:
method h in class C
Lo más importante es que no pasas el primer argumento del método, self, mediante la sintaxis habitual de paso de argumentos. En cambio, un método vinculado a la instancia x vincula implícitamente el parámetro self al objeto x. Así, el cuerpo del método puede acceder a los atributos de la instancia como atributos de self, aunque no pasemos un argumento explícito al método.
Echemos un vistazo más de cerca a los métodos vinculados. Cuando una referencia de atributo de una instancia, en el curso de la búsqueda, encuentra un objeto función que es un atributo de la clase de la instancia, la búsqueda llama al método __get__ de la función para obtener el valor del atributo. La llamada, en este caso, crea y devuelve un método vinculado que envuelve a la función.
Ten en cuenta que cuando la búsqueda de la referencia de atributo encuentra un objeto función directamente en x.__dict__, la operación de referencia de atributo no crea un método vinculado. En estos casos, Python no trata la función como un descriptor y no llama al método __get__ de la función, sino que el propio objeto función es el valor del atributo. Del mismo modo, Python no crea métodos vinculados para llamadas que no sean funciones ordinarias, como las funciones incorporadas (en contraposición a las funciones codificadas en Python), ya que dichas llamadas no son descriptores.
Un método enlazado tiene tres atributos de sólo lectura, además de los del objeto función que envuelve: im_class es el objeto clase que suministra el método, im_func es la función envuelta, e im_self se refiere a x, la instancia de la que has obtenido el método.
Utilizas un método enlazado igual que su función im_func, pero las llamadas a un método enlazado no proporcionan explícitamente un argumento correspondiente al primer parámetro (convencionalmente llamado self). Cuando llamas a un método enlazado, éste pasa im_self como primer argumento a im_func antes que otros argumentos (si los hay) proporcionados en el punto de llamada.
Sigamos, con un detalle insoportablemente bajo, los pasos conceptuales implicados en una llamada a un método con la sintaxis normal x.nombre(arg). En el siguiente contexto
def
f
(
a
,
b
)
:
.
.
.
# a function f with two arguments
class
C
:
name
=
f
x
=
C
(
)
x es un objeto instancia de la clase C, nombre es un identificador que nombra un método de x(un atributo de C cuyo valor es una función, en este caso la función f), y arg es una expresión cualquiera. Python comprueba primero si 'nombre' es el nombre de atributo en C de un descriptor anulador, pero no lo es: las funciones son descriptores, porque su tipo define el método __get__, pero no los anuladores, porque su tipo no define el método __set__. A continuación, Python comprueba si "nombre" es una clave en x._dict__, pero no lo es. Así que Python encuentra nombre en C (todo funcionaría igual si nombre se encontrara, por herencia, en una de las __bases__ de C). Python se da cuenta de que el valor del atributo, el objeto función f, es un descriptor. Por tanto, Python llama a f.__get__(x, C), que devuelve un objeto método ligado con im_func igual a f, im_class igual a C e im_self igual a x. A continuación, Python llama a este objeto método ligado, con arg como único argumento. El método vinculado inserta im_self (es decir, x) como primer argumento, y arg se convierte en el segundo en una llamada a la im_func del método vinculado (es decir, la función f). El efecto global es el mismo que el de una llamada:
x
.
__class__
.
__dict__
[
'
name
'
]
(
x
,
arg
)
Cuando el cuerpo de la función de un método enlazado se ejecuta, no tiene ninguna relación especial de espacio de nombres ni con su objeto propio ni con ninguna clase. Las variables a las que se hace referencia son locales o globales, como cualquier otra función, tal y como se explica en "Espacios de nombres". Las variables no indican implícitamente atributos en self, ni atributos en ningún objeto de clase. Cuando el método necesita referirse, vincular o desvincular un atributo de su objeto self, lo hace mediante la sintaxis estándar de referencia a atributos (por ejemplo, self.nombre).5 Puede que cueste un poco acostumbrarse a la falta de alcance implícito (simplemente porque Python difiere en este aspecto de muchos, aunque ni mucho menos de todos, los demás lenguajes orientados a objetos), pero redunda en claridad, simplicidad y eliminación de posibles ambigüedades.
Los objetos método enlazado son objetos de primera clase: puedes utilizarlos siempre que puedas utilizar un objeto invocable. Puesto que un método enlazado contiene referencias tanto a la función que envuelve como al objeto yo sobre el que se ejecuta, es una alternativa potente y flexible a un cierre (tratado en "Funciones anidadas y ámbitos anidados"). Un objeto instancia cuya clase proporcione el método especial __call__ (tratado en la Tabla 4-1) ofrece otra alternativa viable. Estas construcciones te permiten agrupar parte del comportamiento (código) y parte del estado (datos) en un único objeto invocable. Los cierres son los más sencillos, pero su aplicabilidad es algo limitada. Aquí tienes el cierre de la sección sobre funciones anidadas y ámbitos anidados:
def
make_adder_as_closure
(
augend
)
:
def
add
(
addend
,
_augend
=
augend
)
:
return
addend
+
_augend
return
add
Los métodos vinculados y las instancias invocables son más ricos y flexibles que los cierres. A continuación te explicamos cómo implementar la misma funcionalidad con un método enlazado:
def
make_adder_as_bound_method
(
augend
)
:
class
Adder
:
def
__init__
(
self
,
augend
)
:
self
.
augend
=
augend
def
add
(
self
,
addend
)
:
return
addend
+
self
.
augend
return
Adder
(
augend
)
.
add
Y aquí tienes cómo implementarlo con una instancia invocable (una instancia cuya clase proporciona el método especial __call__):
def
make_adder_as_callable_instance
(
augend
)
:
class
Adder
:
def
__init__
(
self
,
augend
)
:
self
.
augend
=
augend
def
__call__
(
self
,
addend
)
:
return
addend
+
self
.
augend
return
Adder
(
augend
)
Desde el punto de vista del código que llama a las funciones, todas estas funciones de fábrica son intercambiables, ya que todas devuelven objetos invocables que son polimórficos (es decir, utilizables de las mismas formas). Desde el punto de vista de la implementación, el cierre es el más sencillo; los enfoques orientados a objetos -es decir, el método vinculado y la instancia invocable- utilizan mecanismos más flexibles, generales y potentes, pero no hay necesidad de esa potencia adicional en este sencillo ejemplo (ya que no se requiere ningún otro estado más allá del augendo, que se transporta con la misma facilidad en el cierre que en cualquiera de los enfoques orientados a objetos).
Herencia
Cuando utiliza una referencia de atributo C.name en un objeto de clase C, y 'name' no es una clave en C.__dict__, la búsqueda procede implícitamente en cada objeto de clase que está en C.__bases__ en un orden específico (que por razones históricas se conoce como orden de resolución de métodos, u ORM, pero en realidad se aplica a todos los atributos, no sólo a los métodos). Las clases base de Cpueden tener a su vez sus propias bases. La búsqueda comprueba los ancestros directos e indirectos, uno a uno, en MRO, deteniéndose cuando se encuentra 'nombre'.
Orden de resolución del método
La búsqueda de un nombre de atributo en una clase se realiza esencialmente visitando las clases antecesoras de izquierda a derecha, en orden de profundidad primero. Sin embargo, en presencia de herencia múltiple (que hace que el grafo de herencia sea un grafo acíclico dirigido general, o DAG, en lugar de específicamente un árbol), este enfoque simple puede hacer que algunas clases antecesoras se visiten dos veces. En tales casos, el orden de resolución sólo deja en la secuencia de búsqueda la aparición más a la derecha de una clase determinada.
Cada clase y tipo incorporado tiene un atributo de clase especial de sólo lectura llamado __mro__, que es la tupla de tipos utilizada para la resolución de métodos, en orden. Sólo puedes hacer referencia a __mro__ en las clases, no en las instancias, y, puesto que __mro__ es un atributo de sólo lectura, no puedes volver a vincularlo o desvincularlo. Para una explicación detallada y muy técnica de todos los aspectos del MRO de Python, puedes estudiar el ensayo de Michele Simionato "El orden de resolución de métodos de Python 2.3"6 y el artículo de Guido van Rossum sobre "La historia de Python". En particular, ten en cuenta que es muy posible que Python no pueda determinar ningún MRO inequívoco para una determinada clase: en este caso, Python lanza una excepción TypeError cuando ejecuta esa sentencia de clase.
Anulación de atributos
Como acabamos de ver, la búsqueda de un atributo procede a lo largo de la MRO (normalmente, hacia arriba en el árbol de herencia) y se detiene en cuanto se encuentra el atributo. Las clases descendientes siempre se examinan antes que sus ascendientes, de modo que cuando una subclase define un atributo con el mismo nombre que uno de una superclase, la búsqueda encuentra la definición en la subclase y se detiene ahí. Es lo que se conoce como que la subclase anula la definición de la superclase. Considera el siguiente código:
class
B
:
a
=
23
b
=
45
def
f
(
self
)
:
(
'
method f in class B
'
)
def
g
(
self
)
:
(
'
method g in class B
'
)
class
C
(
B
)
:
b
=
67
c
=
89
d
=
123
def
g
(
self
)
:
(
'
method g in class C
'
)
def
h
(
self
)
:
(
'
method h in class C
'
)
Aquí, la clase C anula los atributos b y g de su superclase B. Ten en cuenta que, a diferencia de otros lenguajes, en Python puedes anular los atributos de datos con la misma facilidad que los atributos invocables (métodos).
Delegar en métodos de la superclase
Cuando la subclase C sobrescribe un método f de su superclase B, el cuerpo de C.f suele querer delegar alguna parte de su operación en la implementación del método de la superclase. Esto puede hacerse a veces utilizando un objeto función, como se indica a continuación:
class
Base
:
def
greet
(
self
,
name
)
:
(
'
Welcome
'
,
name
)
class
Sub
(
Base
)
:
def
greet
(
self
,
name
)
:
(
'
Well Met and
'
,
end
=
'
'
)
Base
.
greet
(
self
,
name
)
x
=
Sub
(
)
x
.
greet
(
'
Alex
'
)
La delegación a la superclase, en el cuerpo de Sub.greet, utiliza un objeto función obtenido por referencia de atributo Base.greet en la superclase, y por tanto pasa todos los argumentos normalmente, incluido self. (Si te parece un poco feo utilizar explícitamente la clase base, ten paciencia; verás una forma mejor de hacerlo en breve, en esta misma sección). Delegar en la implementación de una superclase es un uso frecuente de este tipo de objetos función.
Un uso común de la delegación se produce con el método especial __init__. Cuando Python crea una instancia, no llama automáticamente a los métodos __init__ de ninguna clase base, a diferencia de otros lenguajes orientados a objetos. Corresponde a una subclase inicializar sus superclases, utilizando la delegación cuando sea necesario. Por ejemplo:
class
Base
:
def
__init__
(
self
)
:
self
.
anattribute
=
23
class
Derived
(
Base
)
:
def
__init__
(
self
)
:
Base
.
__init__
(
self
)
self
.
anotherattribute
=
45
Si el método __init__ de la clase Derivada no llamara explícitamente al de la clase Base, las instancias de Derivada se perderían esa parte de su inicialización. Por tanto, dichas instancias violarían el principio de sustitución de Liskov (LSP), ya que carecerían del atributo anatributo. Este problema no se plantea si una subclase no define __init__, ya que en ese caso lo hereda de la superclase. Por tanto, nunca hay motivo para codificar:
class
Derived
(
Base
)
:
def
__init__
(
self
)
:
Base
.
__init__
(
self
)
Nunca codifiques un método que sólo delegue en la superclase
Nunca debes definir un __init__ semánticamente vacío (es decir, que sólo delegue en la superclase). En su lugar, hereda __init__ de la superclase. Este consejo se aplica a todos los métodos, especiales o no, pero por alguna razón la mala costumbre de codificar esos métodos semánticamente vacíos parece aparecer con más frecuencia en __init__.
El código anterior ilustra el concepto de delegación a la superclase de un objeto, pero en realidad es una mala práctica, en el Python actual, codificar estas superclases explícitamente por su nombre. Si se cambia el nombre de la clase base, habrá que actualizar todos los sitios de llamada a ella. O, peor aún, si al refactorizar la jerarquía de clases se introduce una nueva capa entre la clase derivada y la clase base, el método de la clase recién insertada se omitirá silenciosamente.
Lo recomendable es llamar a los métodos definidos en una superclase utilizando el tipo incorporado super. Para invocar métodos en la cadena de herencia, basta con llamar a super(), sin argumentos:
class
Derived
(
Base
)
:
def
__init__
(
self
)
:
super
(
)
.
__init__
(
)
self
.
anotherattribute
=
45
Llamada al método cooperativo de la superclase
Llamar explícitamente a la versión de la superclase de un método utilizando el nombre de la superclase también es bastante problemático en casos de herencia múltiple con los llamados gráficos "en forma de diamante". Considera el siguiente código:
class
A
:
def
met
(
self
)
:
(
'
A.met
'
)
class
B
(
A
)
:
def
met
(
self
)
:
(
'
B.met
'
)
A
.
met
(
self
)
class
C
(
A
)
:
def
met
(
self
)
:
(
'
C.met
'
)
A
.
met
(
self
)
class
D
(
B
,
C
)
:
def
met
(
self
)
:
(
'
D.met
'
)
B
.
met
(
self
)
C
.
met
(
self
)
Cuando llamamos a D().met(), A.met acaba siendo llamado dos veces. ¿Cómo podemos asegurarnos de que la implementación del método de cada antepasado se llame una vez y sólo una vez? La solución es utilizar super:
class
A
:
def
met
(
self
)
:
(
'
A.met
'
)
class
B
(
A
)
:
def
met
(
self
)
:
(
'
B.met
'
)
super
(
)
.
met
(
)
class
C
(
A
)
:
def
met
(
self
)
:
(
'
C.met
'
)
super
(
)
.
met
(
)
class
D
(
B
,
C
)
:
def
met
(
self
)
:
(
'
D.met
'
)
super
(
)
.
met
(
)
Ahora, D().met() produce exactamente una llamada a la versión de met de cada clase. Si adquieres la buena costumbre de codificar siempre las llamadas a la superclase con super, tus clases encajarán sin problemas incluso en estructuras de herencia complicadas, y no habrá efectos nocivos si la estructura de herencia resulta ser simple.
La única situación en la que tal vez prefieras utilizar el enfoque más tosco de llamar a métodos de superclases mediante la sintaxis explícita es cuando varias clases tienen firmas diferentes e incompatibles para el mismo método. Se trata de una situación desagradable en muchos aspectos; si tienes que enfrentarte a ella, la sintaxis explícita puede ser a veces el menor de los males. El uso adecuado de la herencia múltiple se ve seriamente obstaculizado; pero entonces, incluso las propiedades más fundamentales de la programación orientada a objetos, como el polimorfismo entre instancias base y subclase, se ven perjudicadas cuando das a métodos del mismo nombre firmas diferentes en una superclase y su subclase.
Definición dinámica de clase utilizando la función incorporada de tipo
En además del uso de type(obj), también puedes llamar a type con tres argumentos para definir una nueva clase:
NewClass
=
type
(
name
,
bases
,
class_attributes
,
**
kwargs
)
donde nombre es el nombre de la nueva clase (que debe coincidir con la variable de destino), bases es una tupla de superclases inmediatas, atributos_clase es un dict de métodos y atributos a nivel de clase para definir en la nueva clase, y **kwargs son argumentos con nombre opcionales para pasar a la metaclase de una de las clases base.
Por ejemplo, con una jerarquía simple de clases Vehículo (como VehículoTierra, VehículoAgua, VehículoAire, VehículoEspacio, etc.), puedes crear dinámicamente clases híbridas en tiempo de ejecución, como por ejemplo
AmphibiousVehicle
=
type
(
'AmphibiousVehicle'
,
(
LandVehicle
,
WaterVehicle
),
{})
Esto equivaldría a definir una clase heredada múltiple:
class
AmphibiousVehicle
(
LandVehicle
,
WaterVehicle
)
:
pass
Cuando llamas a type para crear clases en tiempo de ejecución, no necesitas definir manualmente la expansión combinatoria de todas las combinaciones de subclases de Vehículos, y añadir nuevas subclases no requiere una extensión masiva de las clases mixtas definidas.7 Para más notas y ejemplos, consulta la documentación en línea.
"Borrar" atributos de clase
La herencia y la redefinición ofrecen una forma sencilla y eficaz de añadir o modificar (redefinir) atributos de clase (como métodos) de forma no invasiva -es decir, sin modificar la clase base que define los atributos- añadiendo o redefiniendo los atributos en subclases. Sin embargo, la herencia no ofrece una forma de eliminar (ocultar) los atributos de las clases base de forma no invasiva. Si la subclase simplemente no define (anula) un atributo, Python encuentra la definición de la clase base. Si necesitas realizar dicha eliminación, las posibilidades son las siguientes:
-
Anula el método y lanza una excepción en el cuerpo del método.
-
Evita la herencia, guarda los atributos en otro lugar que no sea __dict__ de la subclase y define __getattr__ para una delegación selectiva.
-
Anula __getattribute__ para un efecto similar.
La última de estas técnicas se demuestra en "__getattribute__".
Considera la posibilidad de utilizar la agregación en lugar de la herencia
Una alternativa a la herencia es utilizar la agregación: en lugar de heredar de una clase base, mantén una instancia de esa clase base como atributo privado. A continuación, obtienes un control total sobre el ciclo de vida y la interfaz pública del atributo proporcionando métodos públicos en la clase contenedora que deleguen en el atributo contenido (es decir, llamando a métodos equivalentes en el atributo). De esta forma, la clase contenedora tiene más control sobre la creación y eliminación del atributo; además, para cualquier método no deseado que proporcione la clase del atributo, simplemente no escribes métodos delegados en la clase contenedora.
El objeto incorporado Tipo
El tipo objeto incorporado es el antepasado de todos los tipos y clases incorporados. El tipo objeto define algunos métodos especiales (documentados en "Métodos especiales") que implementan la semántica por defecto de los objetos:
- __nuevo__, __inicio__
- Puedes crear una instancia directa de objeto llamando a object() sin ningún argumento. La llamada utiliza object.__new__ y object.__init__ para crear y devolver un objeto instancia sin atributos (y sin siquiera un __dict__ en el que guardar los atributos). Tales objetos instancia pueden ser útiles como "centinelas", garantizados para compararse de forma desigual con cualquier otro objeto distinto.
- __delattr__, __getattr__, __getattribute__, __setattr__
- Por defecto, cualquier objeto gestiona las referencias a atributos (como se explica en "Conceptos básicos de las referencias a atributos") utilizando estos métodos de objeto.
- __hash__, __repr__, __str__
- Al pasar un objeto a hash, repr o str, se llama al método dunder correspondiente del objeto.
Una subclase de objeto (es decir, cualquier clase) puede -¡y a menudo lo hará!- sobrescribir cualquiera de estos métodos y/o añadir otros.
Métodos a nivel de clase
Python proporciona dos tipos de descriptores incorporados no anulables, que dan a una clase dos tipos distintos de "métodos a nivel de clase": métodos estáticos y métodos de clase.
Métodos estáticos
Un método estático es un método que puedes invocar sobre una clase, o sobre cualquier instancia de la clase, sin el comportamiento especial y las restricciones de los métodos ordinarios en lo que respecta al primer parámetro. Un método estático puede tener cualquier firma; puede no tener parámetros, y el primer parámetro, si lo hay, no desempeña ningún papel especial. Puedes considerar un método estático como una función ordinaria a la que puedes llamar normalmente, aunque esté vinculada a un atributo de la clase.
Aunque nunca es necesario definir métodos estáticos (siempre puedes optar por definir en su lugar una función normal, fuera de la clase), algunos programadores los consideran una alternativa sintáctica elegante cuando el propósito de una función está estrechamente ligado a alguna clase concreta.
Para construir un método estático, llama al tipo incorporado staticmethod y vincula su resultado a un atributo de la clase. Como toda vinculación de atributos de clase, esto se hace normalmente en el cuerpo de la clase, pero también puedes optar por realizarlo en otro lugar. El único argumento de staticmethod es la función a la que hay que llamar cuando Python llame al método estático. El siguiente ejemplo muestra una forma de definir y llamar a un método estático:
class
AClass
:
def
astatic
(
)
:
(
'
a static method
'
)
astatic
=
staticmethod
(
astatic
)
an_instance
=
AClass
(
)
(
AClass
.
astatic
(
)
)
# prints:
a static method
(
an_instance
.
astatic
(
)
)
# prints:
a static method
Este ejemplo utiliza el mismo nombre para la función pasada a staticmethod y para el atributo ligado al resultado de staticmethod. Esta convención de nombres no es obligatoria, pero es una buena idea y te recomendamos que la utilices siempre. Python ofrece una sintaxis especial y simplificada para soportar este estilo, tratada en "Decoradores".
Métodos de clase
Un método de clase es un método que puedes invocar sobre una clase o sobre cualquier instancia de la clase. Python vincula el primer parámetro del método a la clase sobre la que llamas al método, o a la clase de la instancia sobre la que llamas al método; no lo vincula a la instancia, como en los métodos normales vinculados a. El primer parámetro de un método de clase se llama convencionalmente cls.
Al igual que ocurre con los métodos estáticos, aunque nunca es necesario definir métodos de clase (siempre puedes optar por definir una función normal, fuera de la clase, que tome el objeto de clase como primer parámetro), los métodos de clase son una alternativa elegante a dichas funciones (sobre todo porque pueden anularse útilmente en las subclases, cuando sea necesario).
Para construir un método de clase, llama al método de clase de tipo incorporado y vincula su resultado a un atributo de clase. Como toda vinculación de atributos de clase, esto se hace normalmente en el cuerpo de la clase, pero puedes optar por realizarlo en otro lugar. El único argumento de classmethod es la función a la que se llama cuando Python llama al método de la clase. Aquí tienes una forma de definir y llamar a un método de clase:
class
ABase
:
def
aclassmet
(
cls
)
:
(
'
a class method for
'
,
cls
.
__name__
)
aclassmet
=
classmethod
(
aclassmet
)
class
ADeriv
(
ABase
)
:
pass
b_instance
=
ABase
(
)
d_instance
=
ADeriv
(
)
(
ABase
.
aclassmet
(
)
)
# prints:
a class method for ABase
(
b_instance
.
aclassmet
(
)
)
# prints:
a class method for ABase
(
ADeriv
.
aclassmet
(
)
)
# prints:
a class method for ADeriv
(
d_instance
.
aclassmet
(
)
)
# prints:
a class method for ADeriv
Este ejemplo utiliza el mismo nombre para la función pasada a classmethod y para el atributo ligado al resultado de classmethod. De nuevo, esta convención de nombres no es obligatoria, pero es una buena idea y te recomendamos que la utilices siempre. La sintaxis simplificada de Python para soportar este estilo se trata en "Decoradores".
Propiedades
Python proporciona un tipo de descriptor de sobreescritura incorporado, utilizable para dar propiedades a las instancias de una clase. Una propiedad es un atributo de instancia con una funcionalidad especial. Puedes referenciar, vincular o desvincular el atributo con la sintaxis normal (por ejemplo, print(x.prop), x.prop=23, del x.prop). Sin embargo, en lugar de seguir la semántica habitual para la referencia, vinculación y desvinculación de atributos, estos accesos llaman en la instancia x a los métodos que especifiques como argumentos a la propiedad de tipo incorporada. He aquí una forma de definir una propiedad de sólo lectura:
class
Rectangle
:
def
__init__
(
self
,
width
,
height
)
:
self
.
width
=
width
self
.
height
=
height
def
area
(
self
)
:
return
self
.
width
*
self
.
height
area
=
property
(
area
,
doc
=
'
area of the rectangle
'
)
Cada instancia r de la clase Rectángulo tiene un atributo sintético de sólo lectura r.área, que el método r.área() calcula sobre la marcha multiplicando los lados. El docstring Rectángulo.área.__doc__ es 'área del rectángulo'. El atributo r.area es de sólo lectura (los intentos de reenlazarlo o desenlazarlo fallan) porque sólo especificamos un método get en la llamada a la propiedad, y ningún método set o del.
Las propiedades realizan tareas similares a las de los métodos especiales __getattr__, __setattr__ y __delattr__ (tratados en "Métodos especiales de propósito general"), pero las propiedades son más rápidas y sencillas. Para crear una propiedad, llama a la propiedad de tipo incorporada y vincula su resultado a un atributo de clase. Como toda vinculación de atributos de clase, esto se hace normalmente en el cuerpo de la clase, pero puedes elegir hacerlo en otro lugar. Dentro del cuerpo de una clase C, puedes utilizar la siguiente sintaxis:
attrib
=
property
(
fget
=
None
,
fset
=
None
,
fdel
=
None
,
doc
=
None
)
Cuando x es una instancia de C y haces referencia a x.attrib, Python llama sobre x al método que pasaste como argumento fget al constructor de la propiedad, sin argumentos. Cuando asignas x.attrib = valor, Python llama al método que pasaste como argumento fset, con valor como único argumento. Cuando ejecutas del x.attrib, Python llama al método que pasaste como argumento fdel, sin argumentos. Python utiliza el argumento que pasaste como doc como docstring del atributo. Todos los parámetros de la propiedad son opcionales. Cuando falta un argumento, Python lanza una excepción cuando algún código intenta esa operación. Por ejemplo, en el ejemplo Rectángulo, hicimos que el área de la propiedad fuera de sólo lectura porque pasamos un argumento sólo para el parámetro fget, y no para los parámetros fset y fdel.
Una sintaxis elegante para crear propiedades en una clase es utilizar la propiedad como decorador (ver "Decoradores"):
class
Rectangle
:
def
__init__
(
self
,
width
,
height
)
:
self
.
width
=
width
self
.
height
=
height
@property
def
area
(
self
)
:
"""area of the rectangle"""
return
self
.
width
*
self
.
height
Para utilizar esta sintaxis, debes dar al método getter el mismo nombre que quieras que tenga la propiedad; el docstring del método se convierte en el docstring de la propiedad. Si quieres añadir también un definidor y/o un eliminador, utiliza decoradores llamados (en este ejemplo) area.setter y area.deleter, y nombra a los métodos así decorados igual que a la propiedad. Por ejemplo
import
math
class
Rectangle
:
def
__init__
(
self
,
width
,
height
)
:
self
.
width
=
width
self
.
height
=
height
@property
def
area
(
self
)
:
"""area of the rectangle"""
return
self
.
width
*
self
.
height
@area
.
setter
def
area
(
self
,
value
)
:
scale
=
math
.
sqrt
(
value
/
self
.
area
)
self
.
width
*
=
scale
self
.
height
*
=
scale
Por qué son importantes las propiedades
La importancia crucial de las propiedades es que su existencia hace perfectamente seguro (y, de hecho, aconsejable) que expongas atributos de datos públicos como parte de la interfaz pública de tu clase. Si alguna vez fuera necesario, en futuras versiones de tu clase o de otras clases que necesiten ser polimórficas con ella, hacer que se ejecute algún código cuando se haga referencia al atributo, se rebote o se desboque, podrás cambiar el atributo plano por una propiedad y conseguir el efecto deseado sin ningún impacto en ningún código que utilice tu clase (también conocido como "código cliente"). Esto te permite evitar tonterías, como los métodos accessor y mutator, requeridos por los lenguajes OO que carecen de propiedades. Por ejemplo, el código cliente puede utilizar modismos naturales como éste:
some_instance
.
widget_count
+=
1
en lugar de vernos forzados a entrar en nidos retorcidos de accesores y mutadores como éste:
some_instance
.
set_widget_count
(
some_instance
.
get_widget_count
()
+
1
)
Si alguna vez tienes la tentación de codificar métodos cuyos nombres naturales son algo así como get_this o set_that, envuélvelos en propiedades, para mayor claridad.
Propiedades y herencia
La herencia de propiedades funciona como la de cualquier otro atributo. Sin embargo, hay una pequeña trampa para los incautos: los métodos a los que se recurre para acceder a una propiedad son los definidos en la clase en la que está definida la propia propiedad, sin utilizar intrínsecamente otras sobreescrituras que puedan darse en las subclases. Considera este ejemplo:
class
B
:
def
f
(
self
)
:
return
23
g
=
property
(
f
)
class
C
(
B
)
:
def
f
(
self
)
:
return
42
c
=
C
(
)
(
c
.
g
)
# prints:
23
,
not
42
Al acceder a la propiedad c.g se llama a B.f, no a C.f, como cabría esperar. La razón es muy sencilla: el constructor de la propiedad recibe (directamente o a través de la sintaxis del decorador) el objeto función f (y eso ocurre en el momento en que se ejecuta la sentencia de clase para B, por lo que el objeto función en cuestión es el también conocido como B.f). Por tanto, el hecho de que la subclase C redefina posteriormente el nombre f es irrelevante, ya que la propiedad no realiza ninguna búsqueda de ese nombre, sino que utiliza el objeto función que recibió en el momento de la creación. Si necesitas solucionar este problema, puedes hacerlo añadiendo tú mismo el nivel adicional de indirección de búsqueda:
class
B
:
def
f
(
self
)
:
return
23
def
_f_getter
(
self
)
:
return
self
.
f
(
)
g
=
property
(
_f_getter
)
class
C
(
B
)
:
def
f
(
self
)
:
return
42
c
=
C
(
)
(
c
.
g
)
# prints:
42
,
as expected
En este caso, el objeto función que contiene la propiedad es B._f_getter, que a su vez realiza una búsqueda del nombre f (ya que llama a self.f()); por tanto, la sustitución de f tiene el efecto esperado. Como dijo el famoso David Wheeler, "Todos los problemas de la informática pueden resolverse mediante otro nivel de indirección".8
__slots__
Normalmente, cada objeto de instancia x de cualquier clase C tiene un diccionario x.__dict__ que Python utiliza para permitirte vincular atributos arbitrarios a x. Para ahorrar un poco de memoria (a costa de dejar que x sólo tenga un conjunto predefinido de nombres de atributos), puedes definir en la clase C un atributo de clase llamado __slots__, una secuencia (normalmente una tupla) de cadenas (normalmente identificadores). Cuando la clase C tiene __slots__, la instancia x de la clase C no tiene __dict__: intentar vincular a x un atributo cuyo nombre no esté en C.__slots__ provoca una excepción.
Utilizar __slots__ te permite reducir el consumo de memoria de los objetos instancia pequeños que pueden prescindir de la potente y cómoda capacidad de tener atributos con nombres arbitrarios. Sólo merece la pena añadir __slots__ a las clases que puedan tener tantas instancias que ahorrar unas decenas de bytes por instancia sea importante, normalmente clases que podrían tener millones, no sólo miles, de instancias vivas al mismo tiempo. Sin embargo, a diferencia de la mayoría de los demás atributos de clase, __slots__ sólo funciona como acabamos de describir si una asignación en el cuerpo de la clase lo vincula como atributo de clase. Cualquier alteración posterior, revinculación o desvinculación de __slots__ no tiene ningún efecto, como tampoco lo tiene heredar __slots__ de una clase base. He aquí cómo añadir __slots__ a la clase Rectángulo definida anteriormente para obtener instancias más pequeñas (aunque menos flexibles):
class
OptimizedRectangle
(
Rectangle
)
:
__slots__
=
'
width
'
,
'
height
'
No es necesario definir una ranura para la propiedad área: __slots__ no restringe propiedades, sólo atributos ordinarios de la instancia, que residirían en el __dict__ de la instancia si no se definiera __slots__.
3.8+ Los atributos __slots__ también pueden definirse utilizando un dict con nombres de atributo para las claves y docstrings para los valores. OptimizedRectangle podría declararse de forma más completa como:
class
OptimizedRectangle
(
Rectangle
)
:
__slots__
=
{
'
width
'
:
'
rectangle width in pixels
'
,
'
height
'
:
'
rectangle height in pixels
'
}
__getatributo__
Todas las referencias a atributos de instancia pasan por el método especial __getattribute__. Este método procede de object, donde implementa la semántica de referencia a atributos (como se documenta en "Conceptos básicos de referencia a atributos"). Puedes anular __getattribute__ para fines como ocultar atributos de clase heredados para las instancias de una subclase. Por ejemplo, el siguiente ejemplo muestra una forma de implementar una lista sin append:
class
listNoAppend
(
list
)
:
def
__getattribute__
(
self
,
name
)
:
if
name
==
'
append
'
:
raise
AttributeError
(
name
)
return
list
.
__getattribute__
(
self
,
name
)
Una instancia x de la clase listaNoAppend es casi indistinguible de un objeto lista incorporado, salvo que su rendimiento en tiempo de ejecución es sustancialmente peor, y cualquier referencia a x.append lanza una excepción.
Implementar __getattribute__ puede ser complicado; a menudo es más fácil utilizar las funciones incorporadas getattr y setattr y el __dict__ de la instancia (si existe), o reimplementar __getattr__ y __setattr__. Por supuesto, en algunos casos (como el ejemplo anterior), no hay alternativa.
Métodos por instancia
Una instancia de puede tener vínculos específicos de instancia para todos los atributos, incluidos los atributos invocables (métodos). Para un método, como para cualquier otro atributo (excepto los vinculados a descriptores de anulación), una vinculación específica de instancia oculta una vinculación a nivel de clase: la búsqueda de atributos no tiene en cuenta la clase cuando encuentra una vinculación directamente en la instancia. Un enlace específico de instancia para un atributo invocable no realiza ninguna de las transformaciones detalladas en "Métodos enlazados y no enlazados": la referencia al atributo devuelve exactamente el mismo objeto invocable que antes estaba enlazado directamente al atributo de instancia.
Sin embargo, esto no funciona como cabría esperar para las vinculaciones por instancia de los métodos especiales que Python llama implícitamente como resultado de diversas operaciones, como se explica en "Métodos especiales". Estos usos implícitos de los métodos especiales siempre dependen de la vinculación a nivel de clase del método especial, si existe. Por ejemplo:
def
fake_get_item
(
idx
)
:
return
idx
class
MyClass
:
pass
n
=
MyClass
(
)
n
.
__getitem__
=
fake_get_item
(
n
[
23
]
)
# results in:
# Traceback (most recent call last):
# File "<stdin>", line 1, in ?
# TypeError: unindexable object
Herencia de tipos incorporados
Una clase puede heredar de un tipo incorporado. Sin embargo, una clase puede extender directa o indirectamente múltiples tipos incorporados sólo si esos tipos están específicamente diseñados para permitir este nivel de compatibilidad mutua. Python no admite la herencia sin restricciones de múltiples tipos incorporados arbitrarios. Normalmente, una clase de estilo nuevo sólo extiende como máximo un tipo incorporado sustancial. Por ejemplo, este:
class
noway
(
dict
,
list
)
:
pass
lanza una excepción TypeError, con una explicación detallada de "múltiples bases tienen conflicto de disposición de instancia". Cuando veas este tipo de mensajes de error, significa que estás intentando heredar, directa o indirectamente, de múltiples tipos incorporados que no están específicamente diseñados para cooperar a un nivel tan profundo.
Métodos especiales
Una clase puede definir o heredar métodos especiales, a menudo denominados métodos "dunder" porque, como se ha descrito antes, sus nombres tienen doble guión bajo inicial y final. Cada método especial se refiere a una operación concreta. Python llama implícitamente a un método especial cada vez que realizas la operación relacionada en un objeto instancia. En la mayoría de los casos, el valor de retorno del método es el resultado de la operación, e intentar realizar una operación cuando su método relacionado no está presente provoca una excepción.
A lo largo de esta sección, señalamos los casos en los que no se aplican estas reglas generales. En lo que sigue, x es la instancia de la clase C sobre la que realizas la operación, e y es el otro operando, si lo hay. El parámetro self de cada método también se refiere al objeto instancia x. Siempre que mencionemos llamadas a x.__whatever__(...), ten en cuenta que la llamada exacta que se produce es más bien, hablando pedantemente, x.__class__.__whatever__(x, ...).
Métodos especiales de uso general
Algunos métodos de dunder se refieren a operaciones de propósito general. Una clase que defina o herede estos métodos permite a sus instancias controlar dichas operaciones. Estas operaciones pueden dividirse en categorías:
- Inicialización y finalización
- Una clase puede controlar la inicialización de sus instancias (un requisito muy común) mediante los métodos especiales __new__ y __init__, y/o su finalización (un requisito poco común) mediante __del__.
- Representación en cadena
- Una clase puede controlar cómo Python representa sus instancias como cadenas mediante los métodos especiales __repr__, __str__, __format__ y __bytes__.
- Comparación, hashing y uso en un contexto booleano
- Una clase puede controlar cómo se comparan sus instancias con otros objetos (mediante métodos especiales __lt__, __le__, __gt__, __ge__, __eq__ y __ne__), cómo los utilizan los diccionarios como claves y los conjuntos como miembros (mediante __hash__), y si se evalúan como verdaderos o falsos en contextos booleanos ( mediante__bool__).
- Referencia, vinculación y desvinculación de atributos
- Una clase puede controlar el acceso a los atributos de sus instancias (referencia, vinculación, desvinculación) mediante métodos especiales __getattribute__, __getattr__, __setattr__ y __delattr__.
- Instancias invocables
- Una clase puede hacer que sus instancias sean invocables, igual que los objetos función, mediante el método especial __call__.
La Tabla 4-1 documenta los métodos especiales de uso general.
__bool__ | __bool__(auto) Cuando evalúa x como verdadero o falso (ver "Valores booleanos")-por ejemplo, en una llamada a bool(x)-Python llama a x.__bool__(), que debería devolver Verdadero o Falso. Cuando __bool__ no está presente, Python llama a __len__, y toma x como falso cuando x.__len__() devuelve 0 (para comprobar que un contenedor no está vacío, evita codificar if len(contenedor)>0:; utiliza en su lugar if contenedor:). Cuando no están presentes ni __bool__ ni __len__, Python considera que x es verdadero. |
__bytes__ | __bytes__(auto) Al llamar a bytes(x) se llama a x.__bytes__(), si existe. Si una clase proporciona ambos métodos especiales __bytes__ y __str__, deben devolver cadenas "equivalentes", respectivamente, de tipo bytes y str. |
__llama__ | __call__(self[, args...]) Cuando llama a x([args...]), Python traduce la operación en una llamada a x.__call__([args...]). Los argumentos de la operación de llamada corresponden a los parámetros del método __call__, menos el primero. El primer parámetro, convencionalmente llamado self, se refiere a x: Python lo proporciona implícitamente, como en cualquier otra llamada a un método vinculado a. |
__del__ | __del__(self) Justo antes de que x desaparezca mediante la recogida de basura, Python llama a x.__del__() para que x se finalice a sí mismo. Si __del__ está ausente, Python no hace ninguna finalización especial en la recogida de basura de x (éste es el caso más común: muy pocas clases necesitan definir __del__). Python ignora el valor de retorno de __del__ y no llama implícitamente a los métodos __del__ de las superclases de la clase C. C.__del__ debe realizar explícitamente cualquier finalización necesaria, incluso, si es necesario, por delegación. Cuando la clase C tiene clases base que finalizar, C.__del__ debe llamar a super().__del__(). El método __del__ no tiene ninguna relación específica con la sentencia del, tratada en "Sentencias del". Por lo general,__del__ no es el mejor método cuando necesitas una finalización puntual y garantizada. Para tales necesidades, utiliza la sentencia try/finally, tratada en "try/finally" (o, mejor aún, la sentencia with, tratada en "La sentencia with"). Las instancias de las clases que definen __del__ no participan en la recogida de basura cíclica, tratada en "Recogida de basura". Ten cuidado de evitar bucles de referencia en los que intervengan tales instancias: define __del__ sólo cuando no haya otra alternativa viable. |
__delattr__ | __delattr__(self, nombre) En cada vez que se solicita desvincular el atributo x.y (normalmente, del x.y), Python llama a x.__delattr__('y'). Todas las consideraciones expuestas más adelante para __setattr__ también se aplican a __delattr__. Python ignora el valor de retorno de __delattr__. En ausencia de __delattr__, Python convierte del x.y en del x.__dict__['y']. |
__dir__ | __dir__(auto) Cuando llama a dir(x), Python traduce la operación en una llamada a x.__dir__(), que debe devolver una lista ordenada de los atributos de x. Cuando la clase de xno tiene __dir__, dir(x) realiza una introspección para devolver una lista ordenada de los atributos de x, esforzándose por producir información relevante, en lugar de completa. |
__eq__, __ge__, __gt__, __le__, __lt__, __ne__ |
__eq__(yo, otro), __ge__(yo, otro), __gt__( yo, otro), __le__(yo, otro), __lt__(yo, otro), __ne__(yo, otro) Las comparaciones x == y, x >= y, x > y, x <= y, x < y, y x != y, respectivamente, llaman a los métodos especiales enumerados aquí, que deben devolver Falso o Verdadero. Cada método puede devolver NotImplemented para indicar a Python que maneje la comparación de forma alternativa (por ejemplo, Python puede intentar y > x en lugar de x < y). Las buenas prácticas consisten en definir sólo un método de comparación de desigualdades (normalmente __lt__) además de __eq__, y decorar la clase con functools.total_ordering (cubierto en la Tabla 8-7), para evitar la repetición y cualquier riesgo de contradicciones lógicas en tus comparaciones. |
__formato__ | __format__(self, cadena_de_formato='') Al llamar a format(x) se llama a x.__format__(''), y al llamar a format(x, cadena_formato) se llama a x.__format__(cadena_formato). La clase es responsable de interpretar la cadena de formato (cada clase puede definir su propio pequeño "lenguaje" de especificaciones de formato, inspirado en los implementados por los tipos incorporados, como se explica en "Formato de cadenas"). Cuando __formato__ se hereda de objeto, delega en __str__ y no acepta una cadena de formato no vacía. |
__getattr__ | __getattr__(yo, nombre) Cuando x.y no se puede encontrar por los pasos habituales (es decir, cuando normalmente se lanzaría un AttributeError ), Python llama a x.__getattr__('y'). Python no llama a __getattr__ para atributos encontrados por medios normales (como claves en x.__dict__, o a través de x.__class__). Si quieres que Python llame a __getattr__ para cada atributo, guarda los atributos en otro lugar (por ejemplo, en otro dict referenciado por un atributo con nombre privado), o anula __getattribute__ en su lugar. __getattr__ debería lanzar AttributeError si no encuentra y. |
__getatributo__ | __getatributo_(self, nombre) En cada solicitud de acceso al atributo x.y, Python llama a x.__getattribute__('y'), que debe obtener y devolver el valor del atributo o, de lo contrario, lanzar AttributeError. La semántica habitual del acceso a atributos(x .__dict__, C.__slots__, los atributos de clase de C, x.__getattr__) se debe a object.__getattribute__. Cuando la clase C anula __getattribute__, debe implementar toda la semántica de atributos que quiera ofrecer. La forma típica de implementar el acceso a atributos es delegando (por ejemplo, llamar a object.__getattribute__(self, ...) como parte de la operación de tu anulación de __getattribute__). Anular __getattribute__ ralentiza el acceso a los atributosCuando una clase sobrescribe __getattribute__, todos los accesos a atributos en instancias de la clase se vuelven lentos, ya que el código de sobrescritura se ejecuta en cada acceso a atributos. |
__hash__ | __hash__(yo) Al llamar a hash(x) se llama a x.__hash__() (y lo mismo ocurre en otros contextos que necesitan conocer el valor hash de x, es decir, al utilizar x como clave de diccionario, como D[x] cuando D es un diccionario, o al utilizar x como miembro de un conjunto). __hash__ debe devolver un int tal que x==y implique hash(x)==hash(y), y debe devolver siempre el mismo valor para un objeto dado. Cuando __hash__ está ausente, al llamar a hash(x) se llama a id(x) en su lugar, siempre que __eq__ también esté ausente. Otros contextos que necesiten conocer el valor hash de xse comportan de la misma manera. Cualquier x que devuelva un resultado con hash(x), en lugar de lanzar una excepción, se conoce como objeto hashable. Cuando __hash__ está ausente, pero __eq__ está presente, al llamar a hash(x) se produce una excepción (y lo mismo ocurre con otros contextos que necesitan conocer el valor hash de x). En este caso, x no es hashable y, por tanto, no puede ser una clave del diccionario o un miembro del conjunto. Normalmente sólo se define __hash__ para objetos inmutables que también definen __eq__. Ten en cuenta que si existe cualquier y tal que x==y, aunque y sea de otro tipo, y tanto x como y son hashables, debes asegurarte de que hash(x)==hash(y). (Hay pocos casos, entre los construidos en Python, en los que x==y pueda cumplirse entre objetos de tipos diferentes. Los más importantes son la igualdad entre distintos tipos de números: un int puede ser igual a un bool, un float, una instancia de fracciones.Fraction o una instancia de decimales.Decimal ). |
__init__ | __init__(self[, args...]) Cuando una llamada C([args...]) crea la instancia x de la clase C, Python llama a x.__init__([args...]) para que x se inicialice a sí misma. Si __init__ está ausente (es decir, se hereda de object), debes llamar a C sin argumentos, C(), y x no tiene atributos específicos de la instancia en el momento de la creación. Python no realiza ninguna llamada implícita a los métodos __init__ de las superclases de la clase C. C.__init__ debe realizar explícitamente cualquier inicialización, incluso, si es necesario, por delegación. Por ejemplo, cuando la clase C tiene una clase base B que inicializar sin argumentos, el código en C.__init__ debe llamar explícitamente a super().__init__(). La herencia de __init__funciona como la de cualquier otro método o atributo: si la propia C no anula __init__, lo hereda de la primera superclase de su __mro__ que anule __init__, como cualquier otro atributo. __init__ debe devolver Ninguno; de lo contrario, la llamada a la clase genera un error de tipo TypeError. |
__nuevo | __nuevo__(cls[, args...]) Cuando llamas a C([args...]), Python obtiene la nueva instancia x que estás creando invocando a C.__new__(C[, args...]). Toda clase tiene el método __new__ (normalmente, sólo lo hereda de object), que puede devolver cualquier valor x. En otras palabras, __new__ no tiene por qué devolver una nueva instancia de C, aunque se espera que lo haga. Si el valor x que devuelve __new__ es una instancia de C o de cualquier subclase de C (ya sea nueva o previamente existente), Python llama entonces a __init__ sobre x (con los mismos [args...] pasados originalmente a __new__). Inicializar inmutables en __new__, todos los demás en __init__En puedes realizar la mayoría de los tipos de inicialización de nuevas instancias tanto en __init__ como en __new__, así que quizá te preguntes dónde es mejor colocarlos. Las buenas prácticas son poner la inicialización sólo en __init__, a menos que tengas una razón específica para ponerla en __new__. (Cuando un tipo es inmutable, __init__ no puede cambiar sus instancias: en este caso, __new__ tiene que realizar toda la inicialización). |
__repr__ | __repr__(self) Al llamar a repr(x) (lo que ocurre implícitamente en el intérprete interactivo cuando x es el resultado de una sentencia de expresión) se llama a x.__repr__() para obtener y devolver una representación de cadena completa de x. Si __repr__ está ausente, Python utiliza una representación de cadena por defecto. __repr__ debe devolver una cadena con información inequívoca sobre x. Cuando sea factible, intenta que eval(repr(x))==x (pero, ¡no te vuelvas loco para conseguir este objetivo!). |
__setattr__ | __setattr__(self, nombre, valor) En cualquier solicitud para vincular el atributo x.y (normalmente, una sentencia de asignación x.y=valor, pero también, por ejemplo, setattr(x, 'y', valor)), Python llama a x.__setattr__('y', valor). Python siempre llama a __setattr__ para cualquier atributo vinculado a x,una diferencia importante respecto a __getattr__ (en este sentido, __setattr__ está más cerca de __getattribute__). Para evitar la recursión, cuando x.__setattr__ vincula los atributos de x, debe modificar x.__dict__ directamente (por ejemplo, mediante x.__dict__[nombre]=valor); o mejor, __setattr__ puede delegar en la superclase (llamar a super().__setattr__('y', valor)). Python ignora el valor de retorno de __setattr__. Si __setattr__ está ausente (es decir, heredado del objeto), y C.y no es un descriptor anulado, Python suele traducir x.y=z en x.__dict__['y']=z (sin embargo, __setattr__ también funciona bien con __slots__). |
__str__ | __str__(auto) Al igual que print(x), str(x) llama a x.__str__() para obtener una representación de cadena informal y concisa de x. Si no existe __str__, Python llama a x.__repr__. __str__ debe devolver una cadena cómoda y legible para el ser humano, aunque implique alguna aproximación. |
Métodos especiales para contenedores
Una instancia de puede ser un contenedor (una secuencia, un mapeo o un conjunto -conceptos mutuamente excluyentes9). Para obtener la máxima utilidad, los contenedores deben proporcionar los métodos especiales __getitem__, __contains__ y __iter__ (y, si son mutables, también __setitem__ y __delitem__), además de los métodos no especiales que se tratan en las secciones siguientes. En muchos casos, puedes obtener implementaciones adecuadas de los métodos no especiales ampliando la clase base abstracta apropiada del módulo collections.abc, como Sequence, MutableSequence, etc., como se explica en "Clases base abstractas".
Secuencias
En cada método especial de acceso a elementos, una secuencia que tenga L elementos debe aceptar cualquier clave entera tal que-L<=clave<L.10 Por compatibilidad con las secuencias incorporadas, una clave de índice negativo, 0>clave>=-L, debe ser equivalente a clave+L. Si la clave tiene un tipo no válido, la indexación debe lanzar una excepción TypeError. Cuando clave es un valor de un tipo válido pero fuera de rango, la indexación debe lanzar una excepción IndexError. Para las clases secuenciales que no definen __iter__, la sentencia for se basa en estos requisitos, al igual que las funciones incorporadas que toman argumentos iterables. Todo método especial de acceso a ítems de una secuencia debe aceptar también, si resulta práctico, como argumento índice una instancia del tipo incorporado slice cuyos atributos start, step y stop sean intso None; la sintaxis de troceado se basa en este requisito, como se explica en "Troceado de contenedores".
Una secuencia también debe permitir la concatenación (con otra secuencia del mismo tipo) mediante +, y la repetición mediante * (multiplicación por un entero). Por tanto, una secuencia debe tener los métodos especiales __add__, __mul__, __radd__ y __rmul__, tratados en "Métodos especiales para objetos numéricos"; además, las secuencias mutables deben tener los métodos equivalentes in situ __iadd__ y __imul__. Una secuencia debe ser significativamente comparable a otra secuencia del mismo tipo, implementando la comparaciónlexicográfica, como hacen las listas y las tuplas. (Heredar de la clase base abstracta Secuencia o SecuenciaMutable no basta para cumplir todos estos requisitos; heredar de SecuenciaMutable, como mucho, sólo proporciona __iadd__).
Toda secuencia debe tener los métodos no especiales contemplados en "Métodos de lista": contar e indexar en cualquier caso, y, si es mutable, también append, insert, extend, pop, remove, reverse y sort, con las mismas firmas y semántica que los métodos correspondientes de las listas. (Heredar de la clase base abstracta Secuencia o SecuenciaMutable basta para cumplir estos requisitos, excepto para ordenar).
Una secuencia inmutable debe ser hashable si, y sólo si, todos sus elementos lo son. Un tipo de secuencia puede restringir sus elementos de alguna manera (por ejemplo, aceptando sólo elementos de cadena), pero no es obligatorio.
Asignaciones
Los métodos especiales de acceso a ítems de un mapeo deben lanzar una excepción KeyError, en lugar de IndexError, cuando reciban un valor de argumento de clave no válido de un tipo válido. Cualquier mapeo debe definir los métodos no especiales tratados en "Métodos del diccionario": copiar, obtener, elementos, claves y valores. Una asignación mutable también debe definir los métodos clear, pop, popitem, setdefault y update. (Heredar de la clase base abstracta Mapping o MutableMapping cumple estos requisitos, excepto el de copiar).
Un mapeo inmutable debe ser hashable si todos sus elementos lo son. Un tipo de mapeo puede restringir sus claves de alguna manera -por ejemplo, aceptando sólo claves hashables, o (aún más específicamente) aceptando, digamos, sólo claves de cadena-, pero eso no es obligatorio. Toda correspondencia debe ser significativamente comparable a otra correspondencia del mismo tipo (al menos para igualdades y desigualdades, aunque no necesariamente para comparaciones de orden).
Establece
Conjuntos son un tipo peculiar de contenedor: no son ni secuencias ni mapeados y no pueden indexarse, pero tienen una longitud (número de elementos) y son iterables. Los conjuntos también admiten muchos operadores(&,|, ^ y -, así como pruebas de pertenencia y comparaciones) y métodos equivalentes no especiales(intersección, unión, etc.). Si implementas un contenedor similar a un conjunto, debe ser polimórfico respecto a los conjuntos incorporados en Python, tratados en "Conjuntos".(Heredar de la clase base abstracta Set o MutableSet cumple estos requisitos).
Un tipo conjunto inmutable debe ser hashable si todos sus elementos lo son. Un tipo tipo conjunto puede restringir sus elementos de alguna manera -por ejemplo, aceptando sólo elementos hashables, o (más específicamente) aceptando, digamos, sólo elementos enteros-, pero eso no es obligatorio.
Corte de envases
Cuando referencia, vincula o desvincula una rebanada como x[i:j] o x[i:j:k] en un contenedor x (en la práctica, esto sólo se utiliza con secuencias), Python llama al método especial de acceso a elementos aplicable de x, pasando como clave un objeto de un tipo incorporado llamado objeto rebanada. Un objeto trozo tiene los atributos inicio, parada y paso. Cada atributo es Ninguno si omites el valor correspondiente en la sintaxis de la rebanada. Por ejemplo, del x[:3] llama a x.__delitem__(y), donde y es un objeto slice tal que y.stop es 3, y.start es None, e y.step es None. Depende del objeto contenedor x interpretar adecuadamente los argumentos del objeto trozo pasados a los métodos especiales de x. El método índices de los objetos trozo puede ayudar: llámalo con la longitud de tu contenedor como único argumento, y te devolverá una tupla de tres índices no negativos adecuados como inicio, parada y paso para un bucle que indexe cada elemento del trozo. Por ejemplo, un lenguaje común en el método especial __getitem__ de una clase de secuencia para soportar completamente el troceado es:
def
__getitem__
(
self
,
index
)
:
# Recursively special-case slicing
if
isinstance
(
index
,
slice
)
:
return
self
.
__class__
(
self
[
x
]
for
x
in
range
(
*
index
.
indices
(
len
(
self
)
)
)
)
# Check index, and deal with a negative and/or out-of-bounds index
index
=
operator
.
index
(
index
)
if
index
<
0
:
index
+
=
len
(
self
)
if
not
(
0
<
=
index
<
len
(
self
)
)
:
raise
IndexError
# Index is now a correct int, within range(len(self))
# ...rest of __getitem__, dealing with single-item access...
Este modismo utiliza la sintaxis de expresión del generador (genexp) y supone que el método __init__ de tu clase puede llamarse con un argumento iterable para crear una nueva instancia adecuada de la clase.
Métodos de contenedor
Los métodos especiales __getitem__, __setitem__, __delitem__, __iter__, __len__ y __contains__ exponen la funcionalidad de los contenedores (ver Tabla 4-2).
Clases base abstractas
Clases base abstractas (ABC) son un patrón importante en el diseño orientado a objetos: son clases que no pueden instanciarse directamente, sino que existen para ser ampliadas por clases concretas (el tipo más habitual de clases, las que pueden instanciarse).
Un enfoque recomendado para el diseño OO (atribuido a Arthur J. Riel) es no extender nunca una clase concreta de.11 Si dos clases concretas tienen lo suficiente en común como para tentarte a que una de ellas herede de la otra, procede en su lugar creando una clase base abstracta que subsuma todo lo que tienen en común, y haz que cada clase concreta extienda ese ABC. Este enfoque evita muchas de las trampas y escollos sutiles de la herencia.
Python ofrece un amplio soporte para los ABCs, lo suficiente como para convertirlos en una parte de primera clase del modelo de objetos de Python.12
El módulo abc
El módulo abc de la biblioteca estándar proporciona la metaclase ABCMeta y la clase ABC (la subclase abc.ABC convierte a abc.ABCMeta en la metaclase, y no tiene ningún otro efecto).
Cuando utilizas abc.ABCMeta como metaclase para cualquier clase C, esto convierte a C en ABC y proporciona el método de clase C.register, invocable con un único argumento: ese único argumento puede ser cualquier clase existente (o tipo incorporado) X.
Llamar a C.register(X ) convierte a X en una subclase virtual de C, lo que significa que issubclass(X, C) devuelve True, pero C no aparece en X.__mro__, ni X hereda ninguno de los métodos u otros atributos de C.
Por supuesto, también es posible que una nueva clase Y herede de C de la forma normal, en cuyo caso C sí aparece en Y.__mro__, e Y hereda todos los métodos de C, como es habitual en las subclases.
Un ABC C también puede anular opcionalmente el método de clase __subclasshook__, que issubclass(X, C) llama con el único argumento X( siendoX cualquier clase o tipo). Cuando C.__subclasshook__(X) devuelve True, también lo hace issubclass(X, C); cuando C.__subclasshook__(X) devuelve False, también lo hace issubclass(X, C). Si C.__subclasshook__(X) devuelve NotImplemented, issubclass(X, C) sigue el procedimiento habitual.
El módulo abc también proporciona el decorador abstractmethod para designar los métodos que deben implementarse en las clases herederas. Puedes definir una propiedad como abstracta utilizando los decoradores property y abstractmethod, en ese orden.13 Los métodos y propiedades abstractos pueden tener implementaciones (disponibles para las subclases a través del superintegrado ), pero el objetivo de que los métodos y propiedades sean abstractos es que puedas instanciar una subclase no virtual X de un ABC C sólo si X anula cada propiedad y método abstractos de C.
ABC en el módulo de cobros
collections proporciona muchos ABC, en collections.abc.14 Algunos de estos ABC aceptan como subclase virtual cualquier clase que defina o herede un método abstracto específico, como que aparece en la Tabla 4-3.
ABC | Métodos abstractos |
---|---|
Llamable | __llama__ |
Contenedor | __contiene__ |
Hashable | __hash__ |
Iterable | __iter__ |
Tamaño | __len__ |
Los demás ABC de collections.abc amplían uno o varios de ellos, añadiendo más métodos abstractos y/o métodos mixin implementados en términos de los métodos abstractos. (Cuando extiendes cualquier ABC en una clase concreta, debes anular los métodos abstractos; también puedes anular algunos o todos los métodos mixin, cuando eso ayude a mejorar el rendimiento, pero no tienes por qué hacerlo; puedes simplemente heredarlos, cuando esto resulte en un rendimiento suficiente para tus propósitos).
La Tabla 4-4 detalla los ABC de colecciones.abc que amplían directamente los anteriores.
ABC | Amplía | Métodos abstractos | Métodos mixtos |
---|---|---|---|
Iterador | Iterable | __siguiente | __iter__ |
Cartografía | Contenedor Iterable Tamaño |
__getitem__ __iter__ __len |
contiene __eq __ne getitems claves valores |
MappingView | Tamaño | __len__ | |
Secuencia | Contenedor Iterable Tamaño |
__getitem__ __len__ |
contiene __iter__ invertido cuenta índice |
Configura | Contenedor Iterable Tamaño |
__contiene__ Íter __len |
ya igual __geb __gt __le__ __lt__ __ne o __sub__ __xor isdisjoint |
a Para los conjuntos y conjuntos mutables, muchos métodos dunder son equivalentes a métodos no especiales de la clase concreta conjunto; por ejemplo, __add__ es como intersección y __iadd__ es como intersección_actualización. b Para los conjuntos, los métodos de ordenación reflejan el concepto de subconjunto: s1 <= s2 significa "s1 es un subconjunto de o igual a s2". |
La Tabla 4-5 detalla los ABC de este módulo que amplían los anteriores.
Consulta la documentación en línea para más detalles y ejemplos de uso.
ABC en el módulo de números
Suministros denúmeros una jerarquía (también conocida como torre) de ABCs que representan varios tipos de números. La Tabla 4-6 enumera los ABC del módulo de números.
ABC | Descripción |
---|---|
Número | La raíz de la jerarquía. Incluye números de cualquier tipo; no es necesario que admita ninguna operación determinada. |
Complejo | Extiende Número. Debe admitir (mediante métodos especiales) conversiones a complejo y bool, +, -, *, /, ==, !=, y abs, y, directamente, el método conjugar y las propiedades real e imag. |
Real | Amplía Complejo.a Además, debe admitir (mediante métodos especiales) la conversión a float, math.trunc, round, math.floor, math.ceil, divmod, //, %, <, <=, > y >=. |
Racional | Extiende Real. Además, debe admitir las propiedades numerador y denominador. |
Integral | Amplía Rational.b Además, debe admitir (mediante métodos especiales) la conversión a int, ** y las operaciones bit a bit <<, >>, &,^, | y ~. |
a Así, todo int o float tiene una propiedad real igual a su valor, y una propiedad imag igual a 0. b Así, cada int tiene una propiedad numerador igual a su valor, y una propiedad denominador igual a 1. |
Consulta la documentación en línea para obtener notas sobre cómo implementar tus propios tipos numéricos.
Métodos especiales para objetos numéricos
Una instancia de puede admitir operaciones numéricas mediante muchos métodos especiales. Algunas clases que no son numéricas también admiten algunos de los métodos especiales de la Tabla 4-7 para sobrecargar operadores como + y *. En concreto, las secuencias deben tener los métodos especiales __add__, __mul__, __radd__ y __rmul__, como se menciona en "Secuencias". Cuando se llama a uno de los métodos binarios (como __add__, __sub__, etc.) con un operando de un tipo no admitido para ese método, el método debe devolver el singleton incorporado en NotImplemented.
Decoradores
En Python, a menudo utilizas funciones de orden superior: llamadas que aceptan una función como argumento y devuelven una función como resultado. Por ejemplo, los tipos de descriptores como staticmethod y classmethod, tratados en "Métodos a nivel de clase", pueden utilizarse, dentro de los cuerpos de las clases, de la siguiente forma:
def
f
(
cls
,
.
.
.
)
:
# ...definition of f snipped...
f
=
classmethod
(
f
)
Sin embargo, tener la llamada a classmethod textualmente después de la sentencia def perjudica la legibilidad del código: mientras lee la definición de f, el lector del código aún no es consciente de que f va a convertirse en un método de clase y no en un método de instancia. El código es más legible si la mención a classmethod va antes de la def. Para ello, utiliza la forma sintáctica conocida como decoración:
@classmethod
def
f
(
cls
,
.
.
.
)
:
# ...definition of f snipped...
El decorador, aquí @classmethod, debe ir inmediatamente seguido de una sentencia def y significa que f = classmethod(f) se ejecuta justo después de la sentencia def (para cualquier nombre f que def defina ). En términos más generales, @expresión evalúa la expresión (que debe ser un nombre, posiblemente cualificado, o una llamada) y vincula el resultado a un nombre temporal interno (digamos, __aux); cualquier decorador debe ir inmediatamente seguido de una sentencia def (o class), y significa que f =__aux(f) se ejecuta justo después de la sentencia def o class (para cualquier nombre f que def o class defina ). El objeto vinculado a __aux se conoce como decorador, y se dice que decora la función o clase f.
Los decoradores son una forma práctica de abreviar algunas funciones de orden superior. Puedes aplicar decoradores a cualquier sentencia def o class, no sólo en los cuerpos de las clases. Puedes codificar decoradores personalizados, que no son más que funciones de orden superior que aceptan una función o un objeto de clase como argumento y devuelven una función o un objeto de clase como resultado. Por ejemplo, aquí tienes un sencillo decorador de ejemplo que no modifica la función que decora, sino que imprime el docstring de la función en la salida estándar en el momento de definir la función:
def
showdoc
(
f
)
:
if
f
.
__doc__
:
(
f
'
{
f
.
__name__
}
:
{
f
.
__doc__
}
'
)
else
:
(
f
'
{
f
.
__name__
}
: No docstring!
'
)
return
f
@showdoc
def
f1
(
)
:
"""a docstring"""
# prints:
f1: a docstring
@showdoc
def
f2
(
)
:
pass
# prints:
f2: No docstring!
El módulo functools de la biblioteca estándar ofrece un práctico decorador, wraps, para mejorar los decoradores construidos mediante el lenguaje común "wrapping":
import
functools
def
announce
(
f
)
:
@functools
.
wraps
(
f
)
def
wrap
(
*
a
,
*
*
k
)
:
(
f
'
Calling
{
f
.
__name__
}
'
)
return
f
(
*
a
,
*
*
k
)
return
wrap
Decorar una función f con @anunciar hace que se imprima una línea anunciando la llamada antes de cada llamada a f. Gracias al decorador functools.wraps(f), el envoltorio adopta el nombre y el docstring del envoltorio: esto es útil, por ejemplo, cuando se llama a la ayuda incorporada en una función decorada de este tipo.
Metaclases
Cualquier objeto de , incluso un objeto de clase, tiene un tipo. En Python, los tipos y las clases también son objetos de primera clase. El tipo de un objeto de clase también se conoce como metaclase de la clase.15 El comportamiento de un objeto viene determinado principalmente por su tipo. Lo mismo ocurre con las clases: el comportamiento de una clase viene determinado principalmente por su metaclase. Las metaclases son un tema avanzado, y puede que quieras saltarte el resto de esta sección. Sin embargo, comprender a fondo las metaclases puede conducirte a una comprensión más profunda de Python; muy ocasionalmente, puede ser útil definir tus propias metaclases personalizadas.
Alternativas a las metaclases personalizadas para la personalización sencilla de clases
Aunque una metaclase personalizada te permite modificar los comportamientos de las clases prácticamente como quieras, a menudo es posible conseguir algunas personalizaciones de forma más sencilla que codificando una metaclase personalizada.
Cuando una clase C tiene o hereda un método de clase __init_subclass__, Python llama a ese método siempre que subclase C, pasando la subclase recién construida como único argumento posicional. __init_subclass__ también puede tener parámetros con nombre, en cuyo caso Python pasa los argumentos con nombre correspondientes que se encuentran en la declaración de la clase que realiza la subclase. Como ejemplo puramente ilustrativo:
>>
>
class
C
:
.
.
.
def
__init_subclass__
(
cls
,
foo
=
None
,
*
*
kw
)
:
.
.
.
(
cls
,
kw
)
.
.
.
cls
.
say_foo
=
staticmethod
(
lambda
:
f
'
*
{
foo
}
*
'
)
.
.
.
super
(
)
.
__init_subclass__
(
*
*
kw
)
.
.
.
>>
>
class
D
(
C
,
foo
=
'
bar
'
)
:
.
.
.
pass
.
.
.
<class '__main__.D'> {}
>>>
D
.
say_foo
()
'*bar*'
El código en __init_subclass__ puede alterar cls de cualquier forma aplicable, posterior a la creación de la clase; esencialmente, funciona como un decorador de clase que Python aplica automáticamente a cualquier subclase de C.
Otro método especial utilizado para la personalización es __set_name__, que te permite asegurarte de que las instancias de los descriptores añadidos como atributos de clase sepan a qué clase los estás añadiendo, y con qué nombres. Al final de la sentencia class que añade ca a la clase C con nombre n, cuando el tipo de ca tiene el método __set_name__, Python llama a ca.__set_name__(C, n). Por ejemplo:
>>
>
class
Attrib
:
.
.
.
def
__set_name__
(
self
,
cls
,
name
)
:
.
.
.
(
f
'
Attribute
{
name
!r}
added to
{
cls
}
'
)
.
.
.
>>
>
class
AClass
:
.
.
.
some_name
=
Attrib
(
)
.
.
.
Attribute 'some_name' added to <class '__main__.AClass'>
>>>
Cómo determina Python la metaclase de una clase
La sentencia class acepta argumentos con nombre opcionales (después de las bases, si las hay). El argumento con nombre más importante es metaclase, que, si está presente, identifica la metaclase de la nueva clase. Sólo se permiten otros argumentos con nombre si está presente una metaclase que no sea tipo, en cuyo caso se pasan al método opcional __prepare__ de la metaclase (depende totalmente del método __prepare__ hacer uso de tales argumentos con nombre).16 Cuando no hay metaclase de argumentos con nombre, Python determina la metaclase por herencia; para las clases sin bases especificadas explícitamente, la metaclase es por defecto type.
Python llama al método __prepare__, si está presente, en cuanto determina la metaclase, como se indica a continuación:
class
M
:
def
__prepare__
(
classname
,
*
classbases
,
*
*
kwargs
)
:
return
{
}
# ...rest of M snipped...
class
X
(
onebase
,
another
,
metaclass
=
M
,
foo
=
'
bar
'
)
:
# ...body of X snipped...
Aquí, la llamada equivale a M.__prepare__('X', onebase, another, foo='bar'). __prepare__, si está presente, debe devolver una asignación (normalmente sólo un diccionario), que Python utiliza como la asignación d en la que ejecuta el cuerpo de la clase. Si __prepare__ está ausente, Python utiliza como d un nuevo dict, inicialmente vacío.
Cómo una metaclase crea una clase
Habiendo determinado la metaclase M, Python llama a M con tres argumentos: el nombre de la clase (una str), la tupla de clases base t, y el diccionario (u otro mapeo resultante de __prepare__) d en el que acaba de ejecutarse el cuerpo de la clase.17 La llamada devuelve el objeto de clase C, que Python vincula al nombre de la clase, completando la ejecución de la sentencia class. Observa que se trata en realidad de una instanciación del tipo M, por lo que la llamada a M ejecuta M.__init__(C, namestring,t,d), donde C es el valor de retorno de M.__new__(M, namestring, t, d), igual que en cualquier otra instanciación.
Después de que Python cree el objeto de clase C, la relación entre la clase C y su tipo(type(C), normalmente M) es la misma que entre cualquier objeto y su tipo. Por ejemplo, cuando llamas al objeto de clase C (para crear una instancia de C), se ejecuta M.__call__, con el objeto de clase C como primer argumento.
Observa la ventaja, en este contexto, del enfoque descrito en "Métodos por instancia", según el cual los métodos especiales sólo se buscan en la clase, no en la instancia. Llamar a C para instanciarla debe ejecutar el M.__call__ de la metaclase, tenga o no C un atributo (método) __call__ por instancia (es decir, independientemente de si las instancias de C son o no llamables). De este modo, el modelo de objetos de Python evita tener que hacer de la relación entre una clase y su metaclase un caso especial ad hoc. Evitar los casos especiales ad hoc es una de las claves de la potencia de Python: Python tiene pocas reglas generales, sencillas, y las aplica de forma coherente.
Definir y utilizar tus propias metaclases
Es fácil definir metaclases personalizadas: hereda de type y anula algunos de sus métodos. También puedes realizar la mayoría de las tareas para las que te plantearías crear una metaclase con __new__, __init__, __getattribute__, etc., sin implicar metaclases. Sin embargo, una metaclase personalizada puede ser más rápida, ya que el procesamiento especial sólo se realiza en el momento de la creación de la clase, que es una operación poco frecuente. Una metaclase personalizada te permite definir toda una categoría de clases en un framework que adquieren mágicamente cualquier comportamiento interesante que hayas codificado, con total independencia de los métodos especiales que las propias clases decidan definir.
Para alterar una clase concreta de forma explícita, una buena alternativa suele ser utilizar un decorador de clase, como se menciona en "Decoradores". Sin embargo, los decoradores no se heredan, por lo que el decorador debe aplicarse explícitamente a cada clase de interés.18 Las metaclases, en cambio , se heredan; de hecho, cuando defines una metaclase personalizada M, es habitual definir también una clase vacía C con la metaclase M, para que otras clases que necesiten M puedan simplemente heredar de C.
Algunos comportamientos de los objetos de clase sólo pueden personalizarse en metaclases. El siguiente ejemplo muestra cómo utilizar una metaclase para cambiar el formato de cadena de los objetos de clase:
class
MyMeta
(
type
)
:
def
__str__
(
cls
)
:
return
f
'
Beautiful class
{
cls
.
__name__
!r}
'
class
MyClass
(
metaclass
=
MyMeta
)
:
pass
x
=
MyClass
(
)
(
type
(
x
)
)
# prints:
Beautiful class 'MyClass'
Un ejemplo sustancial de metaclase personalizada
Supongamos que, programando en Python, echamos de menos el tipo struct de C: un objeto que no es más que un montón de atributos de datos, en orden, con nombres fijos (las clases de datos, tratadas en la sección siguiente, abordan plenamente este requisito, lo que hace que este ejemplo sea puramente ilustrativo). Python nos permite definir fácilmente una clase genérica Bunch que es similar, aparte del orden y los nombres fijos:
class
Bunch
:
def
__init__
(
self
,
*
*
fields
)
:
self
.
__dict__
=
fields
p
=
Bunch
(
x
=
2.3
,
y
=
4.5
)
(
p
)
# prints:
<_main__.Bunch object at 0x00AE8B10>
Una metaclase personalizada puede aprovechar el hecho de que los nombres de los atributos se fijan en el momento de crear la clase. El código que se muestra en el Ejemplo 4-1 define una metaclase, MetaBunch, y una clase, Bunch, para permitirnos escribir código como
class
Point
(
Bunch
)
:
"""A Point has x and y coordinates, defaulting to 0.0,
and a color, defaulting to 'gray'-and nothing more,
except what Python and the metaclass conspire to add,
such as __init__ and __repr__.
"""
x
=
0.0
y
=
0.0
color
=
'
gray
'
# example uses of class Point
q
=
Point
(
)
(
q
)
# prints:
Point()
p
=
Point
(
x
=
1.2
,
y
=
3.4
)
(
p
)
# prints:
Point(x=1.2, y=3.4)
En este código, las llamadas a print emiten representaciones de cadena legibles de nuestras instancias de Punto. Las instancias de Punto tienen poca memoria, y su rendimiento es básicamente el mismo que el de las instancias de la clase simple Manojo del ejemplo anterior (no hay sobrecarga adicional debida a llamadas implícitas a métodos especiales). El Ejemplo 4-1 es bastante sustancial, y seguir todos sus detalles requiere conocer aspectos de Python tratados más adelante en este libro, como las cadenas (tratadas en el Capítulo 9) y las advertencias de los módulos (tratadas en "El módulo de advertencias"). El identificador mcl utilizado en el Ejemplo 4-1 significa "metaclase", más claro en este caso especial avanzado que el caso habitual de cls que significa "clase".
Ejemplo 4-1. La metaclase MetaBunch
import
warnings
class
MetaBunch
(
type
)
:
"""
Metaclass for new and improved "Bunch": implicitly defines
__slots__, __init__, and __repr__ from variables bound in
class scope.
A class statement for an instance of MetaBunch (i.e., for a
class whose metaclass is MetaBunch) must define only
class-scope data attributes (and possibly special methods, but
NOT __init__ and __repr__). MetaBunch removes the data
attributes from class scope, snuggles them instead as items in
a class-scope dict named __dflts__, and puts in the class a
__slots__ with those attributes' names, an __init__ that takes
as optional named arguments each of them (using the values in
__dflts__ as defaults for missing ones), and a __repr__ that
shows the repr of each attribute that differs from its default
value (the output of __repr__ can be passed to __eval__ to make
an equal instance, as per usual convention in the matter, if
each non-default-valued attribute respects that convention too).
The order of data attributes remains the same as in the
class body.
"""
def
__new__
(
mcl
,
classname
,
bases
,
classdict
)
:
"""Everything needs to be done in __new__, since
type.__new__ is where __slots__ are taken into account.
"""
# Define as local functions the __init__ and __repr__ that
# we'll use in the new class
def
__init__
(
self
,
*
*
kw
)
:
"""__init__ is simple: first, set attributes without
explicit values to their defaults; then, set
those
explicitly
passed in kw.
"""
for
k
in
self
.
__dflts__
:
if
not
k
in
kw
:
setattr
(
self
,
k
,
self
.
__dflts__
[
k
]
)
for
k
in
kw
:
setattr
(
self
,
k
,
kw
[
k
]
)
def
__repr__
(
self
)
:
"""__repr__ is minimal: shows only attributes that
differ
from default values, for compactness.
"""
rep
=
[
f
'
{
k
}
=
{
getattr
(
self
,
k
)
!r}
'
for
k
in
self
.
__dflts__
if
getattr
(
self
,
k
)
!=
self
.
__dflts__
[
k
]
]
return
f
'
{
classname
}
(
{
'
,
'
.
join
(
rep
)
}
)
'
# Build the newdict that we'll use as class dict for the
# new class
newdict
=
{
'
__slots__
'
:
[
]
,
'
__dflts__
'
:
{
}
,
'
__init__
'
:
__init__
,
'
__repr__
'
:
__repr__
,
}
for
k
in
classdict
:
if
k
.
startswith
(
'
__
'
)
and
k
.
endswith
(
'
__
'
)
:
# Dunder methods: copy to newdict, or warn
# about conflicts
if
k
in
newdict
:
warnings
.
warn
(
f
'
Cannot set attr
{
k
!r}
'
f
'
in bunch-class
{
classname
!r}
'
)
else
:
newdict
[
k
]
=
classdict
[
k
]
else
:
# Class variables: store name in __slots__, and
# name and value as an item in __dflts__
newdict
[
'
__slots__
'
]
.
append
(
k
)
newdict
[
'
__dflts__
'
]
[
k
]
=
classdict
[
k
]
# Finally, delegate the rest of the work to type.__new__
return
super
(
)
.
__new__
(
mcl
,
classname
,
bases
,
newdict
)
class
Bunch
(
metaclass
=
MetaBunch
)
:
"""For convenience: inheriting from Bunch can be used to get
the new metaclass (same as defining metaclass= yourself).
"""
pass
Clases de datos
Como ejemplificaba la anterior clase Manojo, una clase cuyas instancias son sólo un montón de elementos de datos con nombre es una gran comodidad. La biblioteca estándar de Python lo cubre con el módulo dataclasses.
La característica principal del módulo dataclasses que vas a utilizar es la función dataclass: un decorador que se aplica a cualquier clase cuyas instancias quieras que sean un montón de elementos de datos con nombre. Como ejemplo típico, considera el siguiente código:
import
dataclasses
@
dataclasses
.
dataclass
class
Point
:
x
:
float
y
:
float
Ahora puedes llamar, por ejemplo, a pt = Punto(0,5, 0,5) y obtener una variable con los atributos pt.x y pt.y, cada uno igual a 0,5. Por defecto, el decorador de clases de datos ha imbuido a la clase Punto con un método __init__ que acepta valores iniciales en coma flotante para los atributos x y y un método __repr__ preparado para mostrar adecuadamente cualquier instancia de la clase:
>>>
pt
Point(x=0.5, y=0.5)
La función dataclass toma muchos parámetros opcionales con nombre para permitirte ajustar detalles de la clase que decora. Los parámetros que puedes utilizar explícitamente con más frecuencia se enumeran en la Tabla 4-8.
Nombre del parámetro | Valor por defecto y comportamiento resultante |
---|---|
eq | Verdadero Cuando es True, genera un método __eq__ (a menos que la clase defina uno) |
congelado | Falso Si es Verdadero, hace que cada instancia de la clase sea de sólo lectura (no permite volver a vincular o eliminar atributos) |
init | Verdadero Cuando es True, genera un método __init__ (a menos que la clase defina uno) |
kw_sólo | Falso 3.10+ Cuando es Verdadero, obliga a que los argumentos de __init__ sean nominativos, no posicionales |
pide | Falso Cuando es Verdadero, genera métodos especiales de comparación de órdenes(__le__, __lt__, etc.) a menos que la clase los defina |
repr | Verdadero Cuando es True, genera un método __repr__ (a menos que la clase defina uno) |
ranuras | Falso 3.10+ Cuando es Verdadero, añade el atributo __slots__ apropiado a la clase (ahorrando cierta cantidad de memoria para cada instancia, pero sin permitir la adición de otros atributos arbitrarios a las instancias de la clase) |
El decorador también añade a la clase un método __hash__ (que permite que las instancias sean claves de un diccionario y miembros de un conjunto) cuando eso es seguro (normalmente, cuando estableces congelado en True). Puedes forzar la adición de __hash__ incluso cuando no sea necesariamente seguro, pero te recomendamos encarecidamente que no lo hagas; si insistes, consulta la documentación en línea para saber cómo hacerlo.
Si necesitas retocar cada instancia de una clase de datos después de que el método __init__ generado automáticamente haya hecho el trabajo central de asignar cada atributo de instancia, define un método llamado __post_init__, y el decorador se asegurará de que se llame justo después de que __init__ haya terminado.
Digamos que deseas añadir un atributo a Punto para capturar la hora en que se creó el punto. Esto podría añadirse como un atributo asignado en __post_init__. Añade el atributo create_time a los miembros definidos para Punto, como tipo float con un valor por defecto de 0, y luego añade una implementación para __post_init__:
def
__post_init__
(
self
)
:
self
.
create_time
=
time
.
time
(
)
Ahora, si creas la variable pt = Punto(0,5, 0,5), al imprimirla aparecerá la marca de tiempo de creación, de forma similar a la siguiente:
>>>
pt
Point(x=0.5, y=0.5, create_time=1645122864.3553088)
Al igual que las clases normales, las clases de datostambién pueden admitir métodos y propiedades adicionales, como este método que calcula la distancia entre dos Puntosy esta propiedad que devuelve la distancia de un Punto al origen:
def
distance_from
(
self
,
other
)
:
dx
,
dy
=
self
.
x
-
other
.
x
,
self
.
y
-
other
.
y
return
math
.
hypot
(
dx
,
dy
)
@property
def
distance_from_origin
(
self
)
:
return
self
.
distance_from
(
Point
(
0
,
0
)
)
Por ejemplo:
>>>
pt
.
distance_from
(
Point
(
-
1
,
-
1
))
2.1213203435596424
>>>
pt
.
distance_from_origin
0.7071067811865476
El módulo dataclasses también proporciona las funciones asdict y astuple, cada una de las cuales toma una instancia de clase de datos como primer argumento y devuelve, respectivamente, un dict y una tupla con los campos de la clase. Además, el módulo proporciona una función de campo que puedes utilizar para personalizar el tratamiento de algunos de los campos de una clase de datos(es decir, los atributos de instancia), y otras funciones y clases especializadas que sólo se necesitan para fines muy avanzados y esotéricos; para conocerlas todas, consulta la documentación en línea.
Tipos enumerados (Enums)
Cuando programes en, a menudo querrás crear un conjunto de valores relacionados que cataloguen o enumeren los posibles valores de una determinada propiedad o ajuste del programa,19 sean cuales sean: colores de terminal, niveles de registro, estados de proceso, palos de naipes, tallas de ropa o cualquier otra cosa que se te ocurra. Un tipo enumerado(enum) es un tipo que define un grupo de valores de este tipo, con nombres simbólicos que puedes utilizar como constantes globales tipificadas. Python proporciona la clase Enum y subclases relacionadas en el módulo enum para definir enumeraciones.
Definir una enumeración proporciona a tu código un conjunto de constantes simbólicas que representan los valores de la enumeración. En ausencia de enum, las constantes pueden definirse como ints, como en este código:
# colors
RED
=
1
GREEN
=
2
BLUE
=
3
# sizes
XS
=
1
S
=
2
M
=
3
L
=
4
XL
=
5
Sin embargo, en este diseño, no hay ningún mecanismo que advierta de expresiones sin sentido como ROJO > XL o L * AZUL, ya que todas son sólo ints. Tampoco existe una agrupación lógica de los colores o tamaños.
En su lugar, puedes utilizar una subclase Enum para definir estos valores:
from
enum
import
Enum
,
auto
class
Color
(
Enum
)
:
RED
=
1
GREEN
=
2
BLUE
=
3
class
Size
(
Enum
)
:
XS
=
auto
(
)
S
=
auto
(
)
M
=
auto
(
)
L
=
auto
(
)
XL
=
auto
(
)
Ahora, código como Color.ROJO > Tamaño.S destaca visualmente como incorrecto, y en tiempo de ejecución genera un error de tipo Python TypeError. El uso de auto() asigna automáticamente valores int incrementales que empiezan por 1 (en la mayoría de los casos, los valores reales asignados a los miembros de la enumeración no son significativos).
Llamar a Enum crea una clase, no una instancia
Sorprendentemente, cuando llamas a enum.Enum(), no devuelve una instancia recién construida, sino una subclase recién construida. Por tanto, el fragmento anterior equivale a
from
enum
import
Enum
Color
=
Enum
(
'
Color
'
,
(
'
RED
'
,
'
GREEN
'
,
'
BLUE
'
)
)
Size
=
Enum
(
'
Size
'
,
'
XS S M L XL
'
)
Cuando llamas a Enum (en lugar de subclasificarla explícitamente en una declaración de clase), el primer argumento es el nombre de la subclase que estás construyendo; el segundo argumento proporciona todos los nombres de los miembros de esa subclase, ya sea como una secuencia de cadenas o como una única cadena separada por espacios en blanco (o por comas).
Te recomendamos que definas las subclases de Enum utilizando la sintaxis de herencia de clase, en lugar de esta forma abreviada. La forma de clase es más explícita visualmente, por lo que es más fácil ver si falta un miembro, si está mal escrito o si se ha añadido más tarde.
Los valores de una enum se denominan miembros. Es habitual utilizar mayúsculas para nombrar a los miembros de una enum, tratándolos como si fueran constantes manifiestas. Los usos típicos de los miembros de una enumeración son la asignación y la comprobación de identidad:
while
process_state
is
ProcessState
.
RUNNING
:
# running process code goes here
if
processing_completed
(
)
:
process_state
=
ProcessState
.
IDLE
Puedes obtener todos los miembros de un Enum iterando sobre la propia clase Enum, o a partir del atributo __members__ de la clase. Los miembros de un Enum son todos individuales globales, por lo que la comparación con is y is not es preferible a == o !=.
El módulo de enum contiene varias clases20 para dar soporte a diferentes formas de enumeraciones, enumeradas en la Tabla 4-9.
Clase | Descripción |
---|---|
Enum | Clase básica de enumeración; los valores de los miembros pueden ser cualquier objeto Python, normalmente intso strs, pero no admite métodos int o str. Útil para definir tipos enumerados cuyos miembros son un grupo desordenado. |
Bandera | Se utiliza para definir enumeraciones que puedes combinar con los operadores |, &,^ y ~; los valores de los miembros deben definirse como intspara admitir estas operaciones a nivel de bits (Python, sin embargo, no asume ningún orden entre ellos). Los miembros bandera con valor 0 son falsos; los demás miembros son verdaderos. Es útil cuando creas o compruebas valores con operaciones bit a bit (por ejemplo, permisos de archivos). Para soportar operaciones bit a bit, generalmente se utilizan potencias de 2 (1, 2, 4, 8, etc.) como valores de los miembros. |
IntEnum | Equivale a la clase IntEnum(int, Enum); los valores de los miembros son intsy admiten todas las operaciones con int, incluido el orden. Útil cuando el orden entre valores es significativo, como al definir niveles de registro. |
IntFlag | Equivale a la clase IntFlag(int, Bandera); los valores de los miembros son ints(normalmente, potencias de 2) que admiten todas las operaciones con int, incluidas las comparaciones. |
StrEnum | 3.11+ Equivalente a la clase StrEnum(str, Enum); los valores de los miembros son strsy admiten todas las operaciones str. |
El módulo enum también define algunas funciones de apoyo, enumeradas en la Tabla 4-10.
Función de apoyo | Descripción |
---|---|
auto | Autoincrementa los valores de los miembros a medida que los defines. Los valores suelen empezar en 1 e incrementarse de 1 en 1; para Flag, los incrementos son en potencias de 2. |
único | Decorador de clase para garantizar que los valores de los miembros difieran entre sí. |
El siguiente ejemplo muestra cómo definir una subclase Bandera para trabajar con los permisos de archivo en el atributo st_mode devuelto al llamar a os.stat o Path.stat (para una descripción de las funciones stat, consulta el Capítulo 11):
import
enum
import
stat
class
Permission
(
enum
.
Flag
)
:
EXEC_OTH
=
stat
.
S_IXOTH
WRITE_OTH
=
stat
.
S_IWOTH
READ_OTH
=
stat
.
S_IROTH
EXEC_GRP
=
stat
.
S_IXGRP
WRITE_GRP
=
stat
.
S_IWGRP
READ_GRP
=
stat
.
S_IRGRP
EXEC_USR
=
stat
.
S_IXUSR
WRITE_USR
=
stat
.
S_IWUSR
READ_USR
=
stat
.
S_IRUSR
@classmethod
def
from_stat
(
cls
,
stat_result
)
:
return
cls
(
stat_result
.
st_mode
&
0o777
)
from
pathlib
import
Path
cur_dir
=
Path
.
cwd
(
)
dir_perm
=
Permission
.
from_stat
(
cur_dir
.
stat
(
)
)
if
dir_perm
&
Permission
.
READ_OTH
:
(
f
'
{
cur_dir
}
is readable by users outside the owner group
'
)
# the following raises TypeError: Flag enums do not support order
# comparisons
(
Permission
.
READ_USR
>
Permission
.
READ_OTH
)
Utilizar enums en lugar de intso strsarbitrarios puede añadir legibilidad e integridad tipográfica a tu código. Puedes encontrar más detalles sobre las clases y métodos del módulo enum en la documentación de Python.
1 O "inconvenientes", según un crítico. La carne de un desarrollador es el veneno de otro.
2 En ese caso, también está bien tener otros argumentos con nombre después de metaclase=. Esos argumentos, si los hay, se pasan a la metaclase.
3 Esa necesidad surge porque __init__, en cualquier subclase de Singleton que defina este método especial, se ejecuta repetidamente, cada vez que instancias la subclase, en la única instancia que existe para cada subclase de Singleton.
4 Excepto para las instancias de una clase que defina __slots__, tratadas en "__slots__".
5 Algunos otros lenguajes OO, como Modula-3, requieren igualmente el uso explícito de self.
6 Muchas ediciones de Python después, ¡el ensayo de Michele sigue siendo válido!
7 Uno de los autores ha utilizado esta técnica para combinar dinámicamente pequeñas clases de prueba mixin para crear complejas clases de casos de prueba para probar múltiples características independientes del producto.
8 Para completar la cita célebre habitualmente truncada: "salvo, por supuesto, el problema de demasiadas indirecciones".
9 Las extensiones de terceros también pueden definir tipos de contenedores que no sean secuencias, ni mapeados, ni conjuntos.
10 Límite inferior incluido, límite superior excluido -como siempre, la norma para Python.
11 Véase, por ejemplo, "Evitar extender clases", de Bill Harlan.
12 Para un concepto relacionado centrado en la comprobación de tipos, véase tipado.Protocolos, tratado en "Protocolos".
13 El módulo abc incluye el decorador abstractproperty, que combina estos dos, pero abstractproperty está obsoleto, y el nuevo código debe utilizar los dos decoradores tal y como se describen.
14 Por compatibilidad con versiones anteriores, estos ABCs también eran accesibles en el módulo collections hasta Python 3.9, pero las importaciones de compatibilidad se eliminaron en Python 3.10. El nuevo código debe importar estos ABC de collections.abc.
15 En sentido estricto, podría decirse que el tipo de una clase C es la metaclase sólo de las instancias de C y no de la propia C, pero esta sutil distinción semántica rara vez, o nunca, se observa en la práctica.
16 O cuando una clase base tiene __init_subclass__, en cuyo caso los argumentos con nombre se pasan a ese método, como se explica en "Alternativas a las metaclases personalizadas para la personalización sencilla de clases".
17 Esto es similar a llamar a type con tres argumentos, como se describe en "Definición dinámica de clases utilizando la función incorporada type".
18 __init_subclass__, tratada en "Alternativas a las metaclases personalizadas para la personalización sencilla de clases", funciona de forma muy parecida a un "decorador heredado", por lo que suele ser una alternativa a una metaclase personalizada.
19 No confundas este concepto con la función incorporada no relacionada enumerar, tratada en el Capítulo 8, que genera pares (número, elemento) a partir de un iterable.
20 La metaclase especializada de enumse comporta de forma tan distinta a la metaclase de tipo habitual que merece la pena señalar todas las diferencias entre enum.Enum y las clases ordinarias. Puedes leer sobre esto en la sección "¿En qué se diferencian las Enum?" de la documentación online de Python.
Get Python en una cáscara de nuez, 4ª edición now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.