Objetos y Clases en Python: POO Essentials
Objetos y Clases en Python: POO Essentials
Objetos y Clases
Hasta ahora hemos estado usando objetos de forma totalmente transparente, casi sin ser conscientes
de ello. Pero, en realidad, todo en Python es un objeto, desde números a funciones. El lenguaje provee
ciertos mecanismos para no tener que usar explícitamente técnicas de orientación a objetos.
Llegados a este punto, investigaremos en profundidad la creación y manipulación de clases y objetos,
así como todas las técnicas y procedimientos que engloban este paradigma. 1
Encapsulamiento
Permite empaquetar el código dentro de una unidad (objeto) donde se puede determinar el
ámbito de actuación.
Abstracción
Permite generalizar los tipos de objetos a través de las clases y simplificar el programa.
Herencia
Permite reutilizar código al poder heredar atributos y comportamientos de una clase a otra.
Polimorfismo
Permite crear múltiples objetos a partir de una misma pieza flexible de código.
https://aprendepython.es/core/modularity/oop/ 1/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
¿Qué es un objeto?
Un objeto es una estructura de datos personalizada que contiene datos y código:
Un objeto representa una instancia única de alguna entidad (a través de los valores de sus atributos) e
interactúa con otros objetos (o consigo mismo) a través de sus métodos.
En el proceso de diseño de una clase hay que tener en cuenta – entre otros – el principio de
responsabilidad única 7, intentando que los atributos y los métodos que contenga esa clase estén
enfocados a un objetivo único y bien definido.
Creando objetos
Empecemos por crear nuestra primera clase. En este caso vamos a modelar algunos de los droides de la
saga StarWars:
Para ello usaremos la palabra reservada class seguida del nombre de la clase:
>>>
>>> class StarWarsDroid:
... pass
https://aprendepython.es/core/modularity/oop/ 3/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
...
Consejo
Existen multitud de droides en el universo StarWars. Una vez que hemos definido la clase genérica
podemos crear instancias/objetos (droides) concretos:
>>>
>>> c3po = StarWarsDroid()
>>> r2d2 = StarWarsDroid()
>>> bb8 = StarWarsDroid()
>>> type(c3po)
__main__.StarWarsDroid
>>> type(r2d2)
__main__.StarWarsDroid
>>> type(bb8)
__main__.StarWarsDroid
Añadiendo métodos
Un método es una función que forma parte de una clase o de un objeto. En su ámbito tiene acceso a
otros métodos y atributos de la clase o del objeto al que pertenece.
La definición de un método (de instancia) es análoga a la de una función ordinaria, pero incorporando
un primer parámetro self que hace referencia a la instancia actual del objeto.
Una de las acciones más sencillas que se pueden hacer sobre un droide es encenderlo o apagarlo.
Vamos a implementar estos dos métodos en nuestra clase:
>>>
>>> class Droid:
... def switch_on(self):
... print("Hi! I'm a droid. Can I help you?")
...
... def switch_off(self):
... print("Bye! I'm going to sleep")
...
>>> k2so.switch_on()
Hi! I'm a droid. Can I help you?
https://aprendepython.es/core/modularity/oop/ 4/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
>>> k2so.switch_off()
Bye! I'm going to sleep
Consejo
El nombre self es sólo una convención. Este parámetro puede llamarse de otra manera, pero seguir el
estándar ayuda a la legibilidad.
Añadiendo atributos
Un atributo no es más que una variable, un nombre al que asignamos un valor, con la particularidad de
vivir dentro de una clase o de un objeto.
Supongamos que, siguiendo con el ejemplo anterior, queremos guardar en un atributo el estado del
droide (encendido/apagado):
>>>
>>> class Droid:
... def switch_on(self):
... self.power_on = True
... print("Hi! I'm a droid. Can I help you?")
...
... def switch_off(self):
... self.power_on = False
... print("Bye! I'm going to sleep")
>>> k2so.switch_on()
Hi! I'm a droid. Can I help you?
>>> k2so.power_on
True
>>> k2so.switch_off()
Bye! I'm going to sleep
>>> k2so.power_on
False
Importante
Siempre que queramos acceder a cualquier método o atributo del objeto habrá que utilizar la
palabra self .
https://aprendepython.es/core/modularity/oop/ 5/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
Inicialización
Existe un método especial que se ejecuta cuando creamos una instancia de un objeto. Este método
es __init__ y nos permite asignar atributos y realizar operaciones con el objeto en el momento de su
creación. También es ampliamente conocido como el constructor.
Veamos un ejemplo de este método con nuestros droides en el que únicamente guardaremos el
nombre del droide como un atributo del objeto:
>>>
1 >>> class Droid:
2 ... def __init__(self, name: str):
3 ... self.name = name
4 ...
5
6 >>> droid = Droid('BB-8')
7
8 >>> droid.name
9 'BB-8'
Línea 2
Definición del constructor.
Línea 7
Creación del objeto (y llamada implícita al constructor)
Línea 9
Acceso al atributo name creado previamente en el constructor.
Es importante tener en cuenta que si no usamos self estaremos creando una variable local en vez de un
atributo del objeto:
>>>
>>> class Droid:
... def __init__(self, name: str):
... name = name # No lo hagas!
...
>>> droid.name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Droid' object has no attribute 'name'
https://aprendepython.es/core/modularity/oop/ 6/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
Ejercicio
Métodos:
¿Serías capaz de extender el método install_app() para instalar varias aplicaciones a la vez?
Plantilla: mobile.py
Tests: test_mobile.py
Lanzar tests: pytest -xq test_mobile.py
Atributos
Acceso directo
En el siguiente ejemplo vemos que, aunque el atributo name se ha creado en el constructor de la clase,
también podemos modificarlo desde «fuera» con un acceso directo:
>>>
>>> class Droid:
... def __init__(self, name: str):
... self.name = name
...
>>> droid.name
'C-3PO'
https://aprendepython.es/core/modularity/oop/ 7/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
Python nos permite añadir atributos dinámicamente a un objeto incluso después de su creación:
>>>
>>> droid.manufacturer = 'Cybot Galactica'
>>> droid.height = 1.77
Nota
Nótese el acceso a los atributos con obj.attribute en vez de lo que veníamos usando
en diccionarios donde hay que escribir «un poco más» obj['attribute'] .
Propiedades
Como hemos visto previamente, los atributos definidos en un objeto son accesibles públicamente. Esto
puede parecer extraño a personas que vengan de otros lenguajes de programación (véase Java). En
Python existe un cierto «sentido de la responsabilidad» a la hora de programar y manejar este tipo de
situaciones: Casi todo es posible a priori pero se debe controlar explícitamente.
Una primera solución «pitónica» para la privacidad de los atributos es el uso de propiedades. La forma
más común de aplicar propiedades es mediante el uso de decoradores:
Veamos un ejemplo en el que estamos ofuscando el nombre del droide a través de propiedades:
>>>
>>> class Droid:
... def __init__(self, name: str):
... self.hidden_name = name
...
... @property
... def name(self) -> str:
... print('inside the getter')
... return self.hidden_name
...
... @name.setter
... def name(self, name: str) -> None:
... print('inside the setter')
... self.hidden_name = name
...
>>> droid.name
inside the getter
https://aprendepython.es/core/modularity/oop/ 8/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
'N1-G3L'
>>> droid.name
inside the getter
'Nigel'
>>> droid.name
inside the getter
'waka-waka'
VALORES CALCULADOS
Una propiedad también se puede usar para devolver un valor calculado (o computado).
A modo de ejemplo, supongamos que la altura del periscopio de los droides astromecánicos se calcula
siempre como un porcentaje de su altura. Veamos cómo implementarlo:
>>>
>>> class AstromechDroid:
... def __init__(self, name: str, height: float):
... self.name = name
... self.height = height
...
... @property
... def periscope_height(self) -> float:
... return 0.3 * self.height
...
>>> droid.periscope_height
0.315
>>> droid.periscope_height(from_ground=True)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'float' object is not callable
En este caso tendríamos que implementar un método para resolver el escenario planteado.
Consejo
La ventaja de usar valores calculados sobre simples atributos es que el cambio de valor en un atributo
no asegura que actualicemos otro atributo, y además siempre podremos modificar directamente el
valor del atributo, con lo que podríamos obtener efectos colaterales indeseados.
CACHEANDO PROPIEDADES
En los ejemplos anteriores hemos creado una propiedad que calcula el alto del periscopio de un droide
astromecánico a partir de su altura. El «coste» de este cálculo es bajo, pero imaginemos por un
momento que fuera muy alto.
https://aprendepython.es/core/modularity/oop/ 10/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
Si cada vez que accedemos a dicha propiedad tenemos que realizar ese cálculo, estaríamos siendo muy
ineficientes (en el caso de que la altura del droide no cambiara). Veamos una aproximación a este
escenario usando el cacheado de propiedades:
>>>
>>> class AstromechDroid:
... def __init__(self, name: str, height: float):
... self.name = name
... self.height = height # llamada al setter
...
... @property
... def height(self) -> float:
... return self._height
...
... @height.setter
... def height(self, height: float) -> None:
... self._height = height
... self._periscope_height = None # invalidar caché
...
... @property
... def periscope_height(self) -> float:
... if self._periscope_height is None:
... print('Calculating periscope height...')
... self._periscope_height = 0.3 * self.height
... return self._periscope_height
>>> droid.periscope_height
Calculating periscope height...
0.315
>>> droid.periscope_height # Cacheado!
0.315
>>> droid.periscope_height
Calculating periscope height...
0.345
https://aprendepython.es/core/modularity/oop/ 11/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
Ocultando atributos
Python tiene una convención sobre aquellos atributos que queremos hacer «privados» (u ocultos):
comenzar el nombre con doble subguión __
>>>
>>> class Droid:
... def __init__(self, name: str):
... self.__name = name
...
Lo que realmente ocurre tras el telón se conoce como «name mangling» y consiste en modificar el
nombre del atributo incorporado la clase como un prefijo. Sabiendo esto podemos acceder al valor del
atributo supuestamente privado:
>>>
>>> droid._Droid__name
'BC-44'
Nota
La filosofía de Python permite hacer casi cualquier cosa con los objetos que se manejan, eso sí, el
sentido de la responsabilidad se traslada a la persona que desarrolla e incluso a la persona que hace
uso del objeto.
Atributos de clase
Podemos asignar atributos a una clase y serán asumidos por todos los objetos instanciados de esa
clase.
A modo de ejemplo, en un principio, todos los droides están diseñados para que obedezcan a su
dueño. Esto lo conseguiremos a nivel de clase, salvo que ese comportamiento se sobreescriba:
>>>
>>> class Droid:
... obeys_owner = True # obedece a su dueño
https://aprendepython.es/core/modularity/oop/ 12/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
...
Truco
Los atributos de clase son accesibles tanto desde la clase como desde las instancias creadas.
>>> droid1.obeys_owner
False
https://aprendepython.es/core/modularity/oop/ 13/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
>>> droid2.obeys_owner
False
Métodos
Métodos de instancia
Un método de instancia es un método que modifica o accede al estado del objeto al que hace
referencia. Recibe self como primer parámetro, el cual se convierte en el propio objeto sobre el que
estamos trabajando. Python envía este argumento de forma transparente: no hay que pasarlo como
argumento.
Veamos un ejemplo en el que, además del constructor, creamos un método de instancia para hacer que
un droide se mueva:
>>>
>>> class Droid:
... def __init__(self, name: str): # método de instancia -> constructor
... self.name = name
... self.covered_distance = 0
...
... def move_up(self, steps: int) -> None: # método de instancia
... self.covered_distance += steps
... print(f'Moving {steps} steps')
...
>>> droid.move_up(10)
Moving 10 steps
PROPIEDADES VS MÉTODOS
https://aprendepython.es/core/modularity/oop/ 14/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
Métodos de clase
Un método de clase es un método que modifica o accede al estado de la clase a la que hace referencia.
Recibe cls como primer parámetro, el cual se convierte en la propia clase sobre la que estamos
trabajando. Python envía este argumento de forma transparente. La identificación de estos métodos se
completa aplicando el decorador @classmethod a la función.
Veamos un ejemplo en el que implementamos un método de clase que muestra el número de droides
creados:
>>>
>>> class Droid:
... count = 0
...
... def __init__(self):
... Droid.count += 1
...
... @classmethod
... def total_droids(cls) -> None:
... print(f'{cls.count} droids built so far!')
...
https://aprendepython.es/core/modularity/oop/ 15/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
>>> Droid.total_droids()
3 droids built so far!
Consejo
El nombre cls es sólo una convención. Este parámetro puede llamarse de otra manera, pero seguir el
estándar ayuda a la legibilidad.
Métodos estáticos
Un método estático es un método que no «debería» modificar el estado del objeto ni de la clase. No
recibe ningún parámetro especial. La identificación de estos métodos se completa aplicando el
decorador @staticmethod a la función.
Veamos un ejemplo en el que creamos un método estático para devolver las categorías de droides que
existen en StarWars:
>>>
>>> class Droid:
... def __init__(self):
... pass
...
... @staticmethod
... def get_droids_categories() -> tuple[str]:
... return ('Messeger', 'Astromech', 'Power', 'Protocol')
...
>>> Droid.get_droids_categories()
('Messeger', 'Astromech', 'Power', 'Protocol')
MÉTODOS DECORADOS
Es posible que, según el escenario, queramos decorar ciertos métodos de nuestra clase. Esto lo
conseguiremos siguiendo la misma estructura de decoradores que ya hemos visto, pero con ciertos
matices.
A continuación veremos un ejemplo en el que creamos un decorador para auditar las acciones de un
droide y saber quién ha hecho qué:
>>>
>>> class Droid:
... @staticmethod
... def audit(method):
... def wrapper(self, *args, **kwargs):
... print(f'Droid {self.name} running {method.__name__}')
https://aprendepython.es/core/modularity/oop/ 16/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
>>> droid.move(1, 1)
Droid B1 running move
>>> droid.reset()
Droid B1 running reset
El decorador se puede poner dentro o fuera de la clase. Por una cuestión de encapsulamiento podría
tener sentido dejarlo dentro de la clase como método estático.
Ver también
También es posible aplicar esta misma técnica usando decoradores con parámetros.
Métodos mágicos
Nivel avanzado
Cuando escribimos 'hello world' * 3 ¿cómo sabe el objeto 'hello world' lo que debe hacer para
multiplicarse con el objeto entero 3 ? O dicho de otra forma, ¿cuál es la implementación del
operador * para «strings» e «int»? En valores numéricos puede parecer evidente (siguiendo los
operadores matemáticos), pero no es así para otros objetos. La solución que proporciona Python para
estas (y otras) situaciones son los métodos mágicos.
https://aprendepython.es/core/modularity/oop/ 17/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
Los métodos mágicos empiezan y terminan por doble subguión __ (es por ello que también se les
conoce como «dunder-methods»). Uno de los «dunder-methods» más famosos es el constructor de
una clase: __init__() .
Importante
Digamos que los métodos mágicos se «disparan» de manera transparente cuando utilizamos ciertas
estructuras y expresiones del lenguaje.
Para el caso de los operadores, existe un método mágico asociado (que podemos personalizar). Por
ejemplo la comparación de dos objetos se realiza con el método __eq__() :
Extrapolando esta idea a nuestro universo StarWars, podríamos establecer que dos droides son iguales
si su nombre es igual, independientemente de que tengan distintos números de serie:
>>>
>>> class Droid:
... def __init__(self, name: str, serial_number: int):
... self.name = name
... self.serial_number = serial_number
...
... def __eq__(self, droid: Droid) -> bool:
... return self.name == droid.name
...
>>> droid1.__eq__(droid2)
True
https://aprendepython.es/core/modularity/oop/ 18/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
Truco
Para poder utilizar la anotación de tipo Droid necesitamos añadir la siguiente línea al principio de
nuestro código:
from __future__ import annotations
Nota
Veamos un ejemplo en el que «sumamos» dos droides (esto se podría ver como una fusión).
Supongamos que la suma de dos droides implica: a) que el nombre del droide resultante es la
concatenación de los nombres de los droides de entrada; b) que la energía del droide resultante es la
suma de la energía de los droides de entrada:
https://aprendepython.es/core/modularity/oop/ 19/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
>>>
>>> class Droid:
... def __init__(self, name: str, power: int):
... self.name = name
... self.power = power
...
... def __add__(self, other: Droid) -> Droid:
... new_name = self.name + '-' + other.name
... new_power = self.power + other.power
... return Droid(new_name, new_power) # Hay que devolver un objeto de tipo Droid
...
Importante
Este tipo de operaciones debe devolver un objeto de la clase con la que estamos trabajando.
Truco
En este tipo de métodos mágicos el parámetro suele llamarse other haciendo referencia al «otro»
objeto que entra en la operación. Es una convención.
SOBRECARGA DE OPERADORES
¿Qué ocurriría si sumamos un número entero a un droide? De primeras nada, porque no lo tenemos
contemplado, pero podríamos establecer un significado: Si sumamos un número entero a un droide
éste aumenta su energía en el valor indicado. Vamos a intentar añadir también este comportamiento al
operador suma ya implementado.
Aunque en Python no existe técnicamente la «sobrecarga de funciones», sí que podemos simularla
identificando el tipo del objeto que nos pasan y realizando acciones en base a ello:
>>>
>>> class Droid:
... def __init__(self, name: str, power: int):
... self.name = name
... self.power = power
https://aprendepython.es/core/modularity/oop/ 20/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
...
... def __add__(self, other: Droid | int) -> Droid:
... if isinstance(other, Droid):
... new_name = self.name + '-' + other.name
... new_power = self.power + other.power
... elif isinstance(other, int):
... new_name = self.name
... new_power = self.power + other
... return Droid(new_name, new_power)
...
>>> powerful_droid.power
100
Esta misma estrategia se puede aplicar al operador de igualdad ya que es muy habitual encontrar
comparaciones de objetos en nuestro código. Por ello, deberíamos tener en cuenta si se van a comparar
dos objetos de distinta naturaleza.
Retomando el caso ya visto… ¿qué pasaría si comparamos un droide con una cadena de texto?
>>>
>>> class Droid:
... def __init__(self, name: str, serial_number: int):
... self.name = name
... self.serial_number = serial_number
...
... def __eq__(self, droid: Droid) -> bool:
... return self.name == droid.name
...
https://aprendepython.es/core/modularity/oop/ 21/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
__STR__
Uno de los métodos mágicos más utilizados es __str__ y permite establecer la forma en la que un
objeto es representado como cadena de texto:
>>>
>>> class Droid:
... def __init__(self, name: str, serial_number: int):
... self.serial_number = serial_number
... self.name = name
...
... def __str__(self) -> str:
... return f'🤖 Droid "{self.name}" serial-no {self.serial_number}'
...
>>> str(droid)
'🤖 Droid "K-2SO" serial-no 8403898409432'
https://aprendepython.es/core/modularity/oop/ 22/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
Ejercicio
Defina una clase Fraction que represente una fracción con numerador y denominador enteros y
utilice los métodos mágicos para poder sumar, restar, multiplicar y dividir estas fracciones.
Además de esto, necesitaremos:
gcd(a, b) como método estático siguiendo el algoritmo de Euclides para calcular el máximo común
divisor entre a y b .
__init__(self, num, den) para construir una fracción (incluyendo simplificación de sus términos
mediante el método gcd() .
__str__(self) para representar una fracción.
Algoritmo de Euclides:
25 40 31 25 40 −1 25 40 20 25 40 15
[ + = ] [ − = ] [ ∗ = ] [ / = ]
30 45 18 30 45 18 30 45 27 30 45 16
25 40 31 25 40 −1 25 40 20 25 40 15
+ = − = ∗ = / =
[ 30 45 18 ] [ 30 45 18 ] [ 30 45 27 ] [ 30 45 16 ]
Plantilla: fraction.py
Tests: test_fraction.py
Lanzar tests: pytest -xq test_fraction.py
__REPR__
En ausencia del método __str__() se usará por defecto el método __repr__() . La diferencia entre ambos
métodos es que el primero está más pensado para una representación del objeto de cara al usuario
mientras que el segundo está más orientado al desarrollador.
El método __repr()__ se invoca automáticamente en los dos siguientes escenarios:
https://aprendepython.es/core/modularity/oop/ 23/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
Veamos un ejemplo. En primer lugar un droide que sólo implementa el método __str__() :
>>>
>>> class Droid:
... def __init__(self, name: str):
... self.name = name
...
... def __str__(self):
... return f"Hi there! I'm {self.name}"
...
>>> print(c14)
Hi there! I'm C-14
https://aprendepython.es/core/modularity/oop/ 24/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
Atención
GESTORES DE CONTEXTO
__enter__()
Acciones que se llevan a cabo al entrar al contexto.
__exit__()
Acciones que se llevan a cabo al salir del contexto.
Veamos un ejemplo en el que implementamos un gestor de contexto que mide tiempos de ejecución:
>>>
>>> from time import time
Aunque en este caso no estamos haciendo uso de los parámetros en la función __exit__() , hacen
referencia a una posible excepción (error) que se produzca en la ejecución del bloque de código que
engloba el contexto. Los tres parámetros son:
Ahora podemos probar nuestro gestor de contexto con un ejemplo concreto. La forma de «activar» el
contexto es usar la sentencia with seguida del símbolo que lo gestiona:
https://aprendepython.es/core/modularity/oop/ 25/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
>>>
>>> with Timer():
... for _ in range(1_000_000):
... x = 2 ** 20
...
Execution time (seconds): 0.05283
Volviendo a nuestro ejemplo de los droides de StarWars, vamos a crear un gestor de contexto que
«congele» un droide para resetear su distancia recorrida:
>>>
>>> class Droid:
... def __init__(self, name: str):
... self.name = name
... self.covered_distance = 0
...
... def move_up(self, steps: int) -> None:
... self.covered_distance += steps
... print(f'Moving {steps} steps')
...
... print(droid.covered_distance)
...
Moving 10 steps
Moving 20 steps
Moving 30 steps
60
Herencia
Nivel intermedio
La herencia consiste en construir una nueva clase partiendo de una clase existente, pero que añade o
modifica ciertos aspectos. La herencia se considera una buena práctica de programación tanto
para reutilizar código como para realizar generalizaciones.
Nota
Cuando se utiliza herencia, la clase derivada, de forma automática, puede usar todo el código de la
clase base sin necesidad de copiar nada explícitamente.
https://aprendepython.es/core/modularity/oop/ 27/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
Sigamos con el ejemplo galáctico: Una de las grandes categorías de droides en StarWars es la
de droides de protocolo. Vamos a crear una herencia sobre esta idea:
>>>
>>> class Droid:
... """ Clase Base """
... pass
...
>>> r2d2.switch_on()
Hi! I'm a droid. Can I help you?
https://aprendepython.es/core/modularity/oop/ 28/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
>>> r2d2.switch_off()
Bye! I'm going to sleep
Sobreescribir un método
Como hemos visto, una clase derivada hereda todo lo que tiene su clase base. Pero en muchas
ocasiones nos interesa modificar el comportamiento de esta herencia.
En el ejemplo anterior vamos a modificar el comportamiento del método switch_on() para la clase
derivada:
>>>
>>> class Droid:
... def switch_on(self):
... print("Hi! I'm a droid. Can I help you?")
...
... def switch_off(self):
... print("Bye! I'm going to sleep")
...
>>> r2d2.switch_on()
Hi! I'm a droid. Can I help you?
Añadir un método
La clase derivada puede, como cualquier otra clase «normal», añadir métodos que no estaban presentes
en su clase base. En el siguiente ejemplo vamos a añadir un método translate() que permita a
los droides de protocolo traducir cualquier mensaje:
>>>
>>> class Droid:
... def switch_on(self):
... print("Hi! I'm a droid. Can I help you?")
https://aprendepython.es/core/modularity/oop/ 29/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
...
... def switch_off(self):
... print("Bye! I'm going to sleep")
...
Con esto ya hemos aportado una personalidad diferente a los droides de protocolo, a pesar de que
heredan de la clase genérica de droides de StarWars.
https://aprendepython.es/core/modularity/oop/ 30/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
Herencia múltiple
Nivel avanzado
Aunque no está disponible en todos los lenguajes de programación, Python sí permite heredar
de múltiples clases base.
Supongamos que queremos modelar la siguiente estructura de clases con herencia múltiple:
https://aprendepython.es/core/modularity/oop/ 31/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
>>>
>>> class Droid:
... def greet(self):
... return 'Here a droid'
...
Prudencia
>>> super_droid.greet()
'Here a protocol droid'
https://aprendepython.es/core/modularity/oop/ 32/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
>>> hyper_droid.greet()
'Here an astromech droid'
Si en una clase se hace referencia a un método o atributo que no existe, Python lo buscará en todas sus
clases base. Pero es posible que exista una colisión en caso de que el método o el atributo buscado
esté, a la vez, en varias clases base. En este caso, Python resuelve el conflicto a través del orden de
resolución de métodos 4.
Todas las clases en Python disponen de un método especial llamado mro() «method resolution order»
que devuelve una lista de las clases que se visitarían en caso de acceder a un método o a un atributo:
>>>
>>> SuperDroid.mro()
[__main__.SuperDroid,
__main__.ProtocolDroid,
__main__.AstromechDroid,
__main__.Droid,
object]
Ver también
Todos los objetos en Python heredan, en primera instancia, de object . Esto se puede comprobar con el
correspondiente mro() de cada objeto:
>>>
>>> int.mro()
[int, object]
>>> str.mro()
[str, object]
>>> float.mro()
[float, object]
>>> tuple.mro()
[tuple, object]
>>> list.mro()
[list, object]
https://aprendepython.es/core/modularity/oop/ 33/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
Mixins
Hay situaciones en las que nos interesa incorporar una clase base «independiente» de la jerarquía
establecida, y sólo a efectos de tareas auxiliares o transversales. Esta aproximación podría ayudar a
evitar colisiones en métodos o atributos reduciendo la ambigüedad que añade la herencia múltiple. A
estas clases auxiliares se las conoce como «mixins».
Veamos un ejemplo de un «mixin» para mostrar las variables de un objeto:
>>>
>>> class Instrospection:
... def dig(self):
... print(vars(self)) # vars devuelve las variables del argumento
...
... class Droid(Instrospection):
... pass
...
>>> droid.dig()
{'code': 'DN-LD', 'num_feet': 2, 'type': 'Power Droid'}
https://aprendepython.es/core/modularity/oop/ 34/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
Ejercicio
Cree el siguiente escenario de herencia de clases en Python que representa distintos tipos de ficheros
en un sistema:
Notas:
El atributo size debe devolver el número total de caracteres sumando las longitudes de los
elementos del atributo contents .
El atributo info de cada clase debe hacer uso del atributo info de su clase base para conformar las
salida final.
Plantilla: file_inheritance.py
Tests: test_file_inheritance.py
Lanzar tests: pytest -xq test_file_inheritance.py
Agregación y composición
Aunque la herencia de clases nos permite modelar una gran cantidad de casos de uso en términos de
«is-a» (es un), existen muchas otras situaciones en las que la agregación o la composición son una
mejor opción. En este caso una clase se compone de otras clases: hablamos de una relación «has-a»
(tiene un).
Hay una sutil diferencia entre agregación y composición:
https://aprendepython.es/core/modularity/oop/ 35/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
>>> print(bb8)
Droid BB-8 armed with a LIGHTER
Estructuras mágicas
https://aprendepython.es/core/modularity/oop/ 36/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
Obviamente no existen estructuras mágicas, pero sí que hay estructuras de datos que deben
implementar ciertos métodos mágicos (o especiales) para desarrollar su comportamiento.
En este apartado veremos algunos de ellos.
Secuencias
Una secuencia en Python es un objeto en el que podemos acceder a cada uno de sus elementos a
través de un índice, así como calcular su longitud total.
Como ejemplo, podemos asumir que los droides de StarWars están ensamblados con distintas
partes/componentes. Veamos una implementación de este escenario:
>>>
>>> class Droid:
... def __init__(self, name: str, parts: list[str]):
... self.name = name
... self.parts = parts
...
... def __setitem__(self, index: int, part: str) -> None:
... self.parts[index] = part
...
... def __getitem__(self, index: int) -> str:
... return self.parts[index]
...
... def __len__(self):
... return len(self.parts)
...
>>> droid.parts
['Radar Eye', 'Pocket Vent', 'Battery Box']
'Radar Eye'
>>> droid[1] # __getitem__(1)
'Pocket Vent'
>>> droid[2] # __getitem__(2)
'Battery Box'
>>> droid.parts
['Radar Eye', 'Holographic Projector', 'Battery Box']
Ejercicio
Cree una clase InfiniteList que permita utilizar una lista sin tener límites, es decir, evitando
un IndexError . Por ejemplo, si la lista tiene 10 elementos, y asignamos un valor al elemento en el
índice 20, esto no daría un error, sino que haría ampliar la lista hasta el valor 20, rellenando los valores
en blanco con un valor de relleno que por defecto es None .
Plantilla: infinite_list.py
Tests: test_infinite_list.py
Lanzar tests: pytest -xq test_infinite_list.py
Diccionarios
Los métodos __getitem__() y __setitem()__ también se pueden aplicar para obtener o fijar valores en un
estructura tipo diccionario. La diferencia es que en vez de manejar un índice manejamos una clave.
Retomando el ejemplo anterior de las partes de un droide vamos a plantear que cada componente
tiene asociado una versión, lo que nos proporciona una estructura de tipo diccionario con clave
(nombre de la parte) y valor (versión de la parte):
>>>
>>> class Droid:
... def __init__(self, name: str, parts: dict[str, float]):
... self.name = name
... self.parts = parts
...
... def __setitem__(self, part: str, version: float) -> None:
... self.parts[part] = version
...
... def __getitem__(self, part: str) -> float:
... return self.parts.get(part)
https://aprendepython.es/core/modularity/oop/ 38/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
...
... def __len__(self):
... return len(self.parts)
>>> droid.parts
{'Radar Eye': 1.1, 'Pocket Vent': 3.0, 'Battery Box': 2.8}
>>> droid.parts
{'Radar Eye': 1.1, 'Pocket Vent': 3.1, 'Battery Box': 2.8}
>>> len(droid)
3
Iterables
Nivel avanzado
Un objeto en Python se dice iterable si implementa el protocolo de iteración. Este protocolo permite
«entregar» un valor de cada vez en forma de secuencia.
Hay muchos tipos de datos iterables en Python que ya hemos visto: cadenas de texto, listas, tuplas,
conjuntos, diccionarios, etc.
https://aprendepython.es/core/modularity/oop/ 39/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
Para ser un objeto iterable sólo es necesario implementar el método mágico __iter__() . Este método
debe proporcionar una referencia al objeto iterador que es quien se encargará de desarrollar el
protocolo de iteración a través del método mágico __next__() .
Protocolo de iteración
Truco
Veamos un ejemplo del universo StarWars. Vamos a partir de un modelo muy sencillo de droide:
>>>
>>> class Droid:
... def __init__(self, serial: str):
... self.serial = serial * 5 # just for fun!
...
... def __str__(self):
... return f'Droid: SN={self.serial}'
https://aprendepython.es/core/modularity/oop/ 40/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
Ahora podemos recorrer el iterable y obtener los droides que genera la factoría:
>>>
>>> for droid in Geonosis(10):
... print(droid)
...
Droid: SN=00000
Droid: SN=11111
Droid: SN=22222
Droid: SN=33333
Droid: SN=44444
Droid: SN=55555
Droid: SN=66666
Droid: SN=77777
Droid: SN=88888
Droid: SN=99999
Cuando utilizamos un bucle for para recorrer los elementos de un iterable, ocurren varias cosas:
Ahora que conocemos las interiodades de los iterables, podemos ver qué ocurre si los usamos desde un
enfoque más funcional.
En primer lugar hay que controlar el uso de los métodos mágicos en el protocolo de iteración:
https://aprendepython.es/core/modularity/oop/ 41/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
>>> next(factory_iterator)
Droid: SN=00000
>>> next(factory_iterator)
Droid: SN=11111
>>> next(factory_iterator)
Droid: SN=22222
>>> next(factory_iterator)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Se da la circunstancia de que, en este caso, no tenemos que crear el iterador para poder obtener
nuevos elementos:
>>>
>>> next(Geonosis(3))
Droid: SN=00000
Otra característica importante es que los iterables se agotan. Lo podemos comprobar con el siguiente
código:
>>>
>>> geon = Geonosis(3)
https://aprendepython.es/core/modularity/oop/ 42/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
... print(droid)
... # Salida vacía!
Ejercicio
pycheck: fibonacci_iterable
Hasta ahora hemos analizado el escenario en el que el objeto iterable coincide con el objeto iterador,
pero esto no tiene por qué ser siempre así.
Supongamos, en este caso, que queremos implementar un mercado de droides de protocolo que debe
ser un iterable y devolver cada vez un droide de protocolo. Veamos esta aproximación usando un
iterador externo:
>>>
>>> class Droid:
... def __init__(self, name: str):
... self.name = name
...
... def __repr__(self):
... return f'Droid: {self.name}'
...
https://aprendepython.es/core/modularity/oop/ 43/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
Probamos ahora el código anterior recorriendo todos los droides que están disponibles en el mercado:
>>>
>>> market = ProtocolDroidMarket()
Consejo
Esta aproximación puede ser interesante cuando no queremos mezclar el código del iterador con la
lógica del objeto principal.
Si utilizamos un generador (ya sea como función o expresión) estaremos, casi sin saberlo,
implementando el protocolo de iteración:
Veamos una reimplementación del ejemplo anterior del mercado de droides utilizando una función
generadora:
>>>
>>> class Droid:
... def __init__(self, name: str):
... self.name = name
...
... def __repr__(self):
... return f'Droid: {self.name}'
...
https://aprendepython.es/core/modularity/oop/ 44/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
EJEMPLOS DE ITERABLES
>>> next(tool)
https://aprendepython.es/core/modularity/oop/ 45/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
(0, 1)
>>> next(tool)
(1, 2)
>>> next(tool)
(2, 3)
Rangos:
>>>
>>> tool = range(1, 4)
>>> tool_iterator
<range_iterator at 0x1100e6d60>
>>> next(tool_iterator)
1
>>> next(tool_iterator)
2
>>> next(tool_iterator)
3
https://aprendepython.es/core/modularity/oop/ 46/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
Nota
Los objetos de tipo range representan una secuencia inmutable de números. La ventaja de usar este
tipo de objetos es que siempre se usa una cantidad fija (y pequeña) de memoria, independientemente
del rango que represente (ya que solamente necesita almacenar los valores para start , stop y step ,
y calcula los valores intermedios a medida que los va necesitando).
Invertido:
>>>
>>> tool = reversed([1, 2, 3])
>>> next(tool)
3
>>> next(tool)
2
>>> next(tool)
1
Comprimir:
>>>
>>> tool = zip([1, 2], [3, 4])
>>> next(tool)
(1, 3)
>>> next(tool)
https://aprendepython.es/core/modularity/oop/ 47/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
(2, 4)
Generadores:
>>>
>>> def seq(n):
... for i in range(1, n+1):
... yield i
...
>>> next(tool)
1
>>> next(tool)
2
>>> next(tool)
3
Ver también
Listas:
https://aprendepython.es/core/modularity/oop/ 48/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
>>>
>>> tool = [1, 2, 3]
>>> tool_iterator
<list_iterator at 0x1102492d0>
>>> next(tool_iterator)
1
>>> next(tool_iterator)
2
>>> next(tool_iterator)
3
Tuplas:
>>>
>>> tool = tuple([1, 2, 3])
>>> tool_iterator
<tuple_iterator at 0x107255a50>
https://aprendepython.es/core/modularity/oop/ 49/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
>>> next(tool_iterator)
1
>>> next(tool_iterator)
2
>>> next(tool_iterator)
3
Cadenas de texto:
>>>
>>> tool = 'abc'
>>> tool_iterator
<str_ascii_iterator at 0x1078da7d0>
>>> next(tool_iterator)
'a'
>>> next(tool_iterator)
'b'
>>> next(tool_iterator)
'c'
Diccionarios:
https://aprendepython.es/core/modularity/oop/ 50/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
>>>
>>> tool = dict(a=1, b=1)
>>> tool_iterator
<dict_keyiterator at 0x1070200e0>
>>> next(tool_iterator)
'a'
>>> next(tool_iterator)
'b'
>>> iter(tool.values())
<dict_valueiterator at 0x1102aab10>
>>> iter(tool.items())
<dict_itemiterator at 0x107df6ac0>
Conjuntos:
>>>
>>> tool = set([1, 2, 3])
https://aprendepython.es/core/modularity/oop/ 51/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
>>> tool_iterator
<set_iterator at 0x10700e900>
>>> next(tool_iterator)
1
>>> next(tool_iterator)
2
>>> next(tool_iterator)
3
Ficheros:
>>>
>>> f = open('data.txt')
>>> next(f)
'1\n'
>>> next(f)
'2\n'
>>> next(f)
'3\n'
https://aprendepython.es/core/modularity/oop/ 52/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
EJERCICIOS DE REPASO
Plantilla: date.py
Tests: test_date.py
Lanzar tests: pytest -xq test_date.py
Plantilla: dna.py
Tests: test_dna.py
Lanzar tests: pytest -xq test_dna.py
3. Escriba una clase IntegerStack que represente una pila de valores enteros.
Plantilla: istack.py
Tests: test_istack.py
Lanzar tests: pytest -xq test_istack.py
3. Escriba una clase IntegerQueue que represente una cola de valores enteros.
Plantilla: iqueue.py
Tests: test_iqueue.py
Lanzar tests: pytest -xq test_iqueue.py
AMPLIAR CONOCIMIENTOS
https://aprendepython.es/core/modularity/oop/ 54/55
31/3/24, 22:39 Objetos y Clases - Aprende Python
7 Principios SOLID
https://aprendepython.es/core/modularity/oop/ 55/55