Clase 8: Más sobre objetos
Hemos visto que si bien en Python todo es un objeto, esto es, una instancia de alguna clase con sus atributos y métodos, los mismos son públicos. Esto otorga gran versatilidad a la programación, pero puede ser un inconveniente a la hora de mantener código, debido a que el pretendido encapsulamiento de datos y funciones en una clase es pura responsabilidad del programador. Aquí veremos algunas facilidades que otorga Python para ayudar a crear clases robustas.
El decorador @classmethod
En nuestra versión de la clase Punto
, teníamos la capacidad de ir
registrando el número de puntos que se van inicializando,
class Punto:
"Clase para describir un punto en el espacio"
num_puntos = 0
def __init__(self, x=0, y=0, z=0):
"Inicializa un punto en el espacio"
self.x = x
self.y = y
self.z = z
Punto.num_puntos += 1
return None
def __del__(self):
"Borra el punto y actualiza el contador"
Punto.num_puntos -= 1
p1 = Punto(1,1,1)
p2 = Punto()
print('Número de puntos:', Punto.num_puntos)
del p2
print('Número de puntos:', Punto.num_puntos)
Número de puntos: 2
Número de puntos: 1
Claramente la variable num_puntos
es un dato de la clase Punto
,
y no de una instancia particular de la misma. Para mejorar la
organización de este tipo de datos o métodos asociados a una clase,
Python provee el decorador @classmethod
:
class Punto:
"Clase para describir un punto en el espacio"
num_puntos = 0
def __init__(self, x=0, y=0, z=0):
"Inicializa un punto en el espacio"
self.x = x
self.y = y
self.z = z
Punto.num_puntos += 1
return None
def __del__(self):
"Borra el punto y actualiza el contador"
Punto.num_puntos -= 1
@classmethod
def total(cls):
"Imprime el número total de puntos"
print(f"En total hay {cls.num_puntos} puntos definidos")
Así como self
debe ser el primer argumento de los métodos de
instancia, cls
se refiere a la propia clase y debe ser el primer
argumento del método de clase decorado por @classmethod
. De la misma
forma, se utiliza la palabra cls
por convención, podría ser
cualquier otra siempre que se mantenga la consistencia interna.
p1 = Punto(1,1,1)
p2 = Punto()
Punto.total()
del p2
Punto.total()
En total hay 1 puntos definidos
En total hay 0 puntos definidos
Otro ejemplo interesante es crear constructores alternativos:
class Persona:
def __init__(self, nombre, edad):
self.nombre = nombre
self.edad = edad
@classmethod
def desde_cadena(cls, cadena):
nombre, edad = cadena.split(", ")
return cls(nombre, int(edad)) # Devuelve una nueva instancia
# Crear una instancia desde una cadena
p3 = Persona.desde_cadena("Lionel, 37")
print(p3.nombre)
print(p3.edad)
Lionel 37 En el ejercicio dePolinomio
, puede transformar la funciónfrom_string
requerida a un método de clase, de forma tal que se pueda crear un polinomio como: ```python p1 = Polinomio.from_string(“4 x^3 + 3 x^2 + 2.1 x + 1”)
Getters y Setters
Función property
Volvamos a nuestra clase Punto
y veamos cómo podemos mejorarla para
no incurrir en posibles errores.
class Punto:
"Clase para describir un punto en el espacio"
num_puntos = 0
def __init__(self, x=0, y=0, z=0):
"Inicializa un punto en el espacio"
self.x = x
self.y = y
self.z = z
Punto.num_puntos += 1
return None
def __del__(self):
"Borra el punto y actualiza el contador"
Punto.num_puntos -= 1
def __str__(self):
return f"Punto en el espacio con coordenadas: x = {self.x}, y = {self.y}, z = {self.z}"
def __repr__(self):
return f"Punto(x = {self.x}, y = {self.y}, z = {self.z})"
def __call__(self):
return "Ejecuté el objeto: {}".format(self)
# return str(self)
# return "{}".format(self)
@classmethod
def total(cls):
"Imprime el número total de puntos"
print(f"En total hay {cls.num_puntos} puntos definidos")
P1 = Punto('a',1,2.)
print(P1)
P1
Punto en el espacio con coordenadas: x = a, y = 1, z = 2.0
Punto(x = a, y = 1, z = 2.0)
Esto ocurrió porque nos olvidamos de verificar que los argumentos son del tipo correcto. Una manera de solucionarlo es chequear que los valores son del tipo correcto al crear el objeto, como hicimos anteriormente:
class Punto:
"Clase para describir un punto en el espacio"
num_puntos = 0
def __init__(self, x=0, y=0, z=0):
"Inicializa un punto en el espacio"
if not (isinstance(x, (int, float)) and isinstance(y, (int, float)) and isinstance(z, (int, float))):
raise TypeError("x, y, z deben ser números enteros o flotantes")
self.x = x
self.y = y
self.z = z
Punto.num_puntos += 1
def __del__(self):
"Borra el punto y actualiza el contador"
Punto.num_puntos -= 1
def __str__(self):
return f"Punto en el espacio con coordenadas: x = {self.x}, y = {self.y}, z = {self.z}"
def __repr__(self):
return f"Punto(x = {self.x}, y = {self.y}, z = {self.z})"
@classmethod
def total(cls):
"Imprime el número total de puntos"
print(f"En total hay {cls.num_puntos} puntos definidos")
Punto('a',1,2.)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[10], line 1
----> 1 Punto('a',1,2.)
Cell In[9], line 9, in Punto.__init__(self, x, y, z)
7 "Inicializa un punto en el espacio"
8 if not (isinstance(x, (int, float)) and isinstance(y, (int, float)) and isinstance(z, (int, float))):
----> 9 raise TypeError("x, y, z deben ser números enteros o flotantes")
10 self.x = x
11 self.y = y
TypeError: x, y, z deben ser números enteros o flotantes
Sin embargo aún tendremos problemas si los usuarios lo modifican luego de crearlo:
P1 = Punto(1,2,3)
P1.x = 'b'
P1
Punto(x = b, y = 2, z = 3)
Una solución a esto es hacer las componentes “privadas” (por convención) para que los usuarios no la modifiquen directamente. El problema es que los usuarios tienen que poder acceder y modificarla. Una solución es crear métodos para darle valores (setter) y tomarlos (getter)
class Punto:
"Clase para describir un punto en el espacio"
num_puntos = 0
def __init__(self, x=0, y=0, z=0):
"Inicializa un punto en el espacio"
self.set_coordenadas(x,y,z)
Punto.num_puntos += 1
return None
def get_coordenadas(self):
return self._x, self._y, self._z
def set_coordenadas(self, x=0, y=0, z=0):
if not (isinstance(x, (int, float)) and isinstance(y, (int, float)) and isinstance(z, (int, float))):
raise TypeError("x, y, z deben ser números enteros o flotantes")
self._x = x
self._y = y
self._z = z
def __del__(self):
"Borra el punto y actualiza el contador"
Punto.num_puntos -= 1
def __str__(self):
return f"Punto en el espacio con coordenadas: x = {self._x}, y = {self._y}, z = {self._z}"
def __repr__(self):
return f"Punto(x = {self._x}, y = {self._y}, z = {self._z})"
@classmethod
def total(cls):
"Imprime el número total de puntos"
print(f"En total hay {cls.num_puntos} puntos definidos")
*Por convención* se denota a las variables privadas con un guión bajo
antes del nombre, ej, ``_x``.
P1 = Punto(3,2,4.5)
P2 = Punto(3,2,"hola")
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[15], line 1
----> 1 P2 = Punto(3,2,"hola")
Cell In[13], line 9, in Punto.__init__(self, x, y, z)
7 "Inicializa un punto en el espacio"
8 Punto.num_puntos += 1
----> 9 self.set_coordenadas(x,y,z)
10 return None
Cell In[13], line 17, in Punto.set_coordenadas(self, x, y, z)
15 def set_coordenadas(self, x=0, y=0, z=0):
16 if not (isinstance(x, (int, float)) and isinstance(y, (int, float)) and isinstance(z, (int, float))):
---> 17 raise TypeError("x, y, z deben ser números enteros o flotantes")
18 self._x = x
19 self._y = y
TypeError: x, y, z deben ser números enteros o flotantes
print(P1.get_coordenadas())
(3, 2, 4.5)
# Tomemos el valor de la componente x
a = P1.x
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[17], line 2
1 # Tomemos el valor de la componente x
----> 2 a = P1.x
AttributeError: 'Punto' object has no attribute 'x'
P1.__dict__
{'_x': 3, '_y': 2, '_z': 4.5}
# Se puede hacer, pero no queremos que se haga!
P1._x
3
Esto es un problema! Cambiamos la clase haciendo las variables x,y,z
‘privadas’, pero eso implicó cambiarles el nombre a _x,_y,_z
, si ya
hay una versión de esta clase utilizada en otros programas y acceden a
x,y,z
( es un comportamiento muy razonable querer modificar las
coordenadas del punto…). Hicimos un cambio necesario, pero que puede
afectar uno o más programas existentes y habría que rastrear y modificar
todos (asumiendo que son nuestros). Para ello existe la función
property()
y el decorador correspondiente @property
class Punto:
"Clase para describir un punto en el espacio"
num_puntos = 0
def __init__(self, x=0, y=0, z=0):
"Inicializa un punto en el espacio"
self.set_coordenadas(x,y,z)
Punto.num_puntos += 1
return None
def get_coordenadas(self):
return self._x, self._y, self._z
def get_x(self):
return self._x
def get_y(self):
return self._y
def get_z(self):
return self._z
def set_x(self, x):
if not isinstance(x, (int, float)):
raise TypeError("x debe ser número entero o flotante")
self._x = x
def set_y(self, y):
if not isinstance(y, (int, float)):
raise TypeError("y debe ser número entero o flotante")
self._y = y
def set_z(self, z):
if not isinstance(z, (int, float)):
raise TypeError("z debe ser número entero o flotante")
self._z = z
def set_coordenadas(self, x=0, y=0, z=0):
#if not (isinstance(x, (int, float)) and isinstance(y, (int, float)) and isinstance(z, (int, float))):
# raise TypeError("x, y, z deben ser números enteros o flotantes")
self.set_x(x)
self.set_y(y)
self.set_z(z)
def __del__(self):
"Borra el punto y actualiza el contador"
Punto.num_puntos -= 1
def __str__(self):
return f"Punto en el espacio con coordenadas: x = {self._x}, y = {self._y}, z = {self._z}"
def __repr__(self):
return f"Punto(x = {self._x}, y = {self._y}, z = {self._z})"
def __call__(self):
return "Ejecuté el objeto: {}".format(self)
# return str(self)
# return "{}".format(self)
@classmethod
def total(cls):
"Imprime el número total de puntos"
print(f"En total hay {cls.num_puntos} puntos definidos")
x = property(get_x, set_x)
y = property(get_y, set_y)
z = property(get_z, set_z)
P1 = Punto(2,4,6)
a = P1.x
print(a, P1.y)
2 4
P1.x = 3
P1
Punto(x = 3, y = 4, z = 6)
P1.y = '5'
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[47], line 1
----> 1 P1.y = '5'
Cell In[41], line 31, in Punto.set_y(self, y)
29 def set_y(self, y):
30 if not isinstance(y, (int, float)):
---> 31 raise TypeError("y debe ser número entero o flotante")
32 self._y = y
TypeError: y debe ser número entero o flotante
P2 = Punto('a',1,32)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[48], line 1
----> 1 P2 = Punto('a',1,32)
Cell In[41], line 8, in Punto.__init__(self, x, y, z)
6 def __init__(self, x=0, y=0, z=0):
7 "Inicializa un punto en el espacio"
----> 8 self.set_coordenadas(x,y,z)
9 Punto.num_puntos += 1
10 return None
Cell In[41], line 42, in Punto.set_coordenadas(self, x, y, z)
39 def set_coordenadas(self, x=0, y=0, z=0):
40 #if not (isinstance(x, (int, float)) and isinstance(y, (int, float)) and isinstance(z, (int, float))):
41 # raise TypeError("x, y, z deben ser números enteros o flotantes")
---> 42 self.set_x(x)
43 self.set_y(y)
44 self.set_z(z)
Cell In[41], line 26, in Punto.set_x(self, x)
24 def set_x(self, x):
25 if not isinstance(x, (int, float)):
---> 26 raise TypeError("x debe ser número entero o flotante")
27 self._x = x
TypeError: x debe ser número entero o flotante
P1.__dict__
{'_x': 3, '_y': 4, '_z': 6}
Más sobre herencia
Vimos cómo usar herencia para que una clase pueda derivarse desde otra,
en nuestro caso, Vector
era derivado de la clase Punto
. Otro
ejemplo sencillo podría ser el siguiente:
class Persona:
def __init__(self, nombre, edad):
self.nombre = nombre
self.edad = edad
def presentarse(self):
return f"Hola, soy {self.nombre} y tengo {self.edad} años."
# Clase derivada: Estudiante
class Estudiante(Persona):
def __init__(self, nombre, edad, carrera):
# Se llama al constructor de Persona directamente
Persona.__init__(self, nombre, edad)
self.carrera = carrera
def presentarse(self):
return f"Hola, soy {self.nombre}, tengo {self.edad} años y estudio {self.carrera}."
Aquí la clase Estudiante
deriva de la clase Persona
, y en el
constructor (__init__
) de la clase estudiante, utilizamos el
constructor de la clase base Persona
:
pedro = Estudiante('Pedro', 25, 'Física')
pedro.presentarse()
'Hola, soy Pedro, tengo 25 años y estudio Física.'
Otra posibilidad que brinda Python para referirse a los constructores de
la clase base es utilizar la función super()
:
class Persona:
def __init__(self, nombre, edad):
self.nombre = nombre
self.edad = edad
def presentarse(self):
return f"Hola, soy {self.nombre} y tengo {self.edad} años."
# Clase derivada: Estudiante
class Estudiante(Persona):
def __init__(self, nombre, edad, carrera):
# Llamamos al constructor de la clase base con super()
super().__init__(nombre, edad)
self.carrera = carrera # Nuevo atributo para Estudiante
def presentarse(self):
# Reutilizamos el método de la clase base y agregamos más información
return f"{super().presentarse()} Estoy estudiando {self.carrera}."
# Uso de las clases
carlos = Persona("Carlos", 20)
maria = Estudiante("María", 20, "Ingeniería")
print(carlos.presentarse())
print(maria.presentarse())
Hola, soy Carlos y tengo 20 años.
Hola, soy María y tengo 20 años. Estoy estudiando Ingeniería.
Ejercicios 08 (a)
Cree una nueva clase
Materia
que describa una materia que se dicta en el IB. La clase debe contener información sobre el nombre de la materia, los alumnos que la cursan, y los docentes que la dictan. Utilice las clasesPersona
yEstudiante
y, si es necesario, cree nuevas clases. Además debe proveer los siguientes métodos:agrega_estudiante
que agrega un estudiante al cursoagrega_docente
que agrega un docente al cursoimprime_estudiantes
que lista los estudiantes del curso
Enum
y dataclass
es
Vamos a ver ahora dos tipos de datos que pueden ser útiles más allá de los objetos que uno pueda definir en Python mediante clases. Ambos tipos de datos se relacionan con la inmutabilidad, propiedad que tiene muchos casos de uso relevantes y es de mucha ayuda para crear código robusto.
Enum
s
Los enum
s (enumeraciones) son una forma de asociar simbólicamente
un conjunto de etiquetas a un conjunto de valores constantes, y se
introducen en Python con la versión 3.4. Los enum
modelan un
conjunto limitado de valores que una variable puede tomar, y donde
cada valor tiene un nombre descriptivo.
Para definir un enum
, es necesario importar la clase Enum
del
módulo correspondiente
from enum import Enum
class ColorCMYK(Enum):
YELLOW = 1
CYAN = 2
MAGENTA = 3
BLACK = 4
En este caso hemos definido un enum
con tres elementos
correspondientes a cuatro colores.
def print_color(color: ColorCMYK) -> None:
print(f"Color : {color}")
print(f"Nombre : {color.name}" )
print(f"Valor : {color.value}" )
print_color(ColorCMYK.YELLOW)
Color : ColorCMYK.YELLOW Nombre : YELLOW Valor : 1 Atención: Por convención se usan MAYÚSCULAS para las opciones que puede tener un Enum, al igual que en otros lenguajes de programación donde también se estila usarlas para las constantes.
Efectivamente los valores del Enum son constantes y no es posible reasignarlos:
ColorCMYK.YELLOW = 42
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[5], line 1
----> 1 ColorCMYK.YELLOW = 42
File /usr/lib64/python3.13/enum.py:835, in EnumType.__setattr__(cls, name, value)
833 member_map = cls.__dict__.get('_member_map_', {})
834 if name in member_map:
--> 835 raise AttributeError('cannot reassign member %r' % (name, ))
836 super().__setattr__(name, value)
AttributeError: cannot reassign member 'YELLOW'
ColorCMYK.YELLOW.value = 4
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[6], line 1
----> 1 ColorCMYK.YELLOW.value = 4
File /usr/lib64/python3.13/enum.py:227, in property.__set__(self, instance, value)
225 if self.fset is not None:
226 return self.fset(instance, value)
--> 227 raise AttributeError(
228 "<enum %r> cannot set attribute %r" % (self.clsname, self.name)
229 )
AttributeError: <enum 'Enum'> cannot set attribute 'value'
class ColorRGB(Enum):
RED = 1
GREEN = 2
BLUE = 3
def __repr__(self):
return f"Color : {self}\nNombre : {self.name}\nValor : {self.value}\n"
ColorRGB.RED
Color : ColorRGB.RED
Nombre : RED
Valor : 1
Se pueden comparar distintos enums:
ColorRGB.RED == ColorCMYK.YELLOW
False
ColorRGB.RED.value == ColorCMYK.YELLOW.value
True
print(ColorRGB.RED == ColorCMYK.YELLOW)
print(ColorRGB.RED is ColorCMYK.YELLOW)
False
False
Enums y match
Una estructura de control introducida en Python 3.10 es match-case
,
y puede ser interesante de usar junto con Enums. El match-case
fue
un pedido recurrente de la comunidad para poseer una estructura de
control de flujo múltiple más clara que el if-elif-else
. Se comporta
en forma similar a los switch
que usan otros lenguajes de
programación. La estructura que tiene es la siguiente:
match variable:
case patrón1:
# Código para patrón1
case patrón2:
# Código para patrón2
...
case _:
# Código para el caso por defecto
Por ejemplo:
def describe_color(color):
match color:
case ColorCMYK.YELLOW:
return "Amarillo"
case ColorCMYK.CYAN:
return "Cian"
case ColorCMYK.MAGENTA:
return "Magenta"
case ColorRGB.RED:
return "Rojo"
case ColorRGB.GREEN:
return "Verde"
case ColorRGB.BLUE:
return "Azul"
case _:
return "Color no reconocido"
print(describe_color(ColorCMYK.YELLOW))
print(describe_color(ColorRGB.RED))
print(describe_color(ColorRGB.GREEN))
print(describe_color("Negro"))
Amarillo
Rojo
Verde
Color no reconocido
La estructura match-case
acepta patrones avanzados, comparando
estructuras más complejas:
def detecta_coordenadas(coord):
match coord:
case (0, 0):
return "Origen"
case (x, 0):
return f"En el eje X, en {x}"
case (0, y):
return f"En el eje Y, en {y}"
case (x, y):
return f"En el plano: ({x}, {y})"
case _:
return "Coordenada no válida"
print(detecta_coordenadas((0, 5))) # "En el eje Y, en 5"
print(detecta_coordenadas("cero, cero"))
En el eje Y, en 5
Coordenada no válida
def clasifica_lista(lista):
match lista:
case []:
print("Lista vacía")
return None
case [x]: # Coincide con una lista de un solo elemento
print (f"Lista con un solo elemento: {x}")
return x
case [x, y]: # Coincide con una lista de dos elementos
print (f"Lista con dos elementos: {x} y {y}")
return (x,y)
case [x, y, *resto]: # Coincide con una lista de tres o más elementos
print (f"Lista con tres o más elementos: {x}, {y}, y otros {len(resto)} elementos")
return resto
case _: # Coincide con cualquier otro caso
print ("Lista vacía o no reconocida")
return
# Probar con diferentes listas
clasifica_lista([10])
clasifica_lista([10, 20])
clasifica_lista([10, 20, 30])
clasifica_lista([10, 20, 30, 40])
clasifica_lista([])
clasifica_lista("Hola")
Lista con un solo elemento: 10
Lista con dos elementos: 10 y 20
Lista con tres o más elementos: 10, 20, y otros 1 elementos
Lista con tres o más elementos: 10, 20, y otros 2 elementos
Lista vacía
Lista vacía o no reconocida
v = clasifica_lista([10, 20, 30, 40])
print(v,type(v))
v = clasifica_lista([ColorCMYK.BLACK, ColorRGB.RED])
print(v,type(v))
Lista con tres o más elementos: 10, 20, y otros 2 elementos
[30, 40] <class 'list'>
Lista con dos elementos: ColorCMYK.BLACK y ColorRGB.RED
(<ColorCMYK.BLACK: 4>, Color : ColorRGB.RED
Nombre : RED
Valor : 1
) <class 'tuple'>
Los distintos casos posibles aceptan el operador |
que se usa para
agruparlos:
from enum import Enum
# Definimos un Enum para los días de la semana
class Dia(Enum):
LUNES = 1
MARTES = 2
MIERCOLES = 3
JUEVES = 4
VIERNES = 5
SABADO = 6
DOMINGO = 7
# Función para determinar si es día laboral o fin de semana
def es_dia_laboral(self):
match self:
case Dia.LUNES | Dia.MIERCOLES:
return "Tengo clases de Python 🥳"
case Dia.MARTES | Dia.JUEVES | Dia.VIERNES :
return "Es un día laboral 🧐"
case Dia.SABADO | Dia.DOMINGO:
return "Es fin de semana 😆"
case _:
return "Día no válido"
# Probar con diferentes días
print(Dia.LUNES.es_dia_laboral())
print(Dia.SABADO.es_dia_laboral())
print(Dia.MIERCOLES.es_dia_laboral())
Tengo clases de Python 🥳
Es fin de semana 😆
Tengo clases de Python 🥳
O comparar tipos de datos
def printer_color(color: ColorRGB | ColorCMYK) -> None:
match color:
case ColorRGB():
print(f"Usando RGB: {color}\n")
case ColorCMYK():
print(f"Usando YMgCy: {color}\n")
case _:
print("Color no reconocido\n")
printer_color(ColorRGB.RED)
printer_color(ColorCMYK.YELLOW)
printer_color("Negro")
Usando RGB: ColorRGB.RED
Usando YMgCy: ColorCMYK.YELLOW
Color no reconocido
Es posible hacer comparaciones más complejas todavía, por ejemplo, usando clases:
class Persona:
def __init__(self, nombre, edad):
self.nombre = nombre
self.edad = edad
def saluda_a(persona):
match persona:
case Persona() if persona.edad >= 18:
return f"Hola {persona.nombre}, eres mayor de edad."
case Persona() if 16 <= persona.edad < 18:
return f"Hola {persona.nombre}, podés manejar pero no comprar alcohol"
case Persona():
return f"Hola {persona.nombre}, eres menor de edad."
case _:
return "Eres un alien"
print(saluda_a(Persona("Juan",12)))
print(saluda_a(Persona("Ana",19)))
print(saluda_a(Persona("Mabel",17)))
print(saluda_a("Chewbacca"))
Hola Juan, eres menor de edad.
Hola Ana, eres mayor de edad.
Hola Mabel, podés manejar pero no comprar alcohol
Eres un alien
Otra forma de hacerlo es a través del denominado match posicional.
Para ello se agrega el atributo match_args a la clase, que contiene
una tupla que representa los argumentos de creación de la clase tal como
figuran en el __init__
.
Atención: consultar la ayuda para comprender en profundidad cómo funciona el
match-case
cuando se comparan estructuras de datos complejas como las clases.
class Persona:
__match_args__ = ("nombre","edad")
def __init__(self, nombre, edad):
self.nombre = nombre
self.edad = edad
def saluda_a(persona):
match persona:
case Persona(nombre, edad) if edad >= 18:
return f"Hola {nombre}, eres mayor de edad."
case Persona(nombre, edad) if 16 <= edad < 18:
return f"Hola {nombre}, podés manejar pero no comprar alcohol"
case Persona(nombre, edad):
return f"Hola {nombre}, eres menor de edad."
case _:
return "Eres un alien"
print(saluda_a(Persona("Juan",12)))
print(saluda_a(Persona("Ana",19)))
print(saluda_a(Persona("Mabel",17)))
print(saluda_a("Chewbacca"))
Hola Juan, eres menor de edad.
Hola Ana, eres mayor de edad.
Hola Mabel, podés manejar pero no comprar alcohol
Eres un alien
Dataclasses
En muchísimas situaciones uno necesita utilizar una clase con ciertos
métodos habituales, como un constructor default. Para ello Python provee
un módulo que define un decorador @dataclass
que los genera.
from dataclasses import dataclass
@dataclass
class Atomo:
nombre: str
simbolo: str
N: int # número atómico
A: int # número de masa
hidrogeno = Atomo("Hidrógeno", "H", 1, 1)
helio = Atomo("Helio", "He", 2, 4)
print(hidrogeno)
hidrogeno
Atomo(nombre='Hidrógeno', simbolo='H', N=1, A=1)
Atomo(nombre='Hidrógeno', simbolo='H', N=1, A=1)
Entre los métodos que el decorador genera automáticamente están el
constructor __init__
, los métodos __repr__
y __str__
y el
método __eq__
que provee igualdad estructural:
h = Atomo("Hidrógeno", "H", 1, 1)
print(h==hidrogeno)
print(h is hidrogeno)
True
False
Además de la sintaxis sencilla, se pueden crear dataclasses con argumentos default:
class StockStatus(Enum):
DISPONIBLE = "En stock"
AGOTADO = "Sin stock"
QUEDAN_POCOS = "Stock bajo!"
@dataclass
class Producto:
nombre: str
precio: float
stock: StockStatus = StockStatus.AGOTADO
p = Producto("Laptop", 1000.0)
print(p)
b = Producto("Cerveza",2.5, StockStatus.DISPONIBLE)
print(b)
Producto(nombre='Laptop', precio=1000.0, stock=<StockStatus.AGOTADO: 'Sin stock'>)
Producto(nombre='Cerveza', precio=2.5, stock=<StockStatus.DISPONIBLE: 'En stock'>)
Para finalizar, es posible poblar una dataclass
a partir de un
diccionario en forma sencilla, siempre y cuando las claves del
diccionario se correspondan unívocamente con los campos de la estructura
de la dataclass
cerveza = { "nombre": "Cerveza", "precio": 2.5, "stock": StockStatus.DISPONIBLE }
b = Producto(**cerveza)
print(b)
Producto(nombre='Cerveza', precio=2.5, stock=<StockStatus.DISPONIBLE: 'En stock'>)
b.nombre = "Cerveza artesanal"
print(b)
Producto(nombre='Cerveza artesanal', precio=2.5, stock=<StockStatus.DISPONIBLE: 'En stock'>)
b.nombre = 4
Otra propiedad interesante que poseen las dataclass
es consiste en
utilizar el argumento frozen
para evitar que los objetos sean
modificados una vez creados. Si intentamos modificar un atributo de un
objeto frozen
, se lanzará una excepción FrozenInstanceError
.
@dataclass(frozen=True)
class Atomo():
nombre: str
simbolo: str
N: int # número atómico
A: int # número de masa
Ca = Atomo("Calcio", "Ca", 20, 40)
print(Ca)
Ca.A = 14
Atomo(nombre='Calcio', simbolo='Ca', N=20, A=40)
---------------------------------------------------------------------------
FrozenInstanceError Traceback (most recent call last)
Cell In[28], line 3
1 Ca = Atomo("Calcio", "Ca", 20, 40)
2 print(Ca)
----> 3 Ca.A = 14
File <string>:18, in __setattr__(self, name, value)
FrozenInstanceError: cannot assign to field 'A'
Ejercicios 08 (b)
El archivo
atomos_t.json
contiene datos atómicos y físicos de los primeros átomos de la tabla periódica. Se puede usar el módulojson
para leer este archivo de la siguiente maneraimport json with open('atomos_t.json', 'r') as file: # Verifique que el path al archivo sea el correcto en su caso atomos = json.load(file)
De esta manera se crea un diccionario
atomos
con la información del archivo.Cree una
dataclass
para manejar los datos atómicos, que incluya el nombre del elemento, el símbolo, el número atómico y la masa atómica.Extienda la clase anterior para poder manejar el estado del material a temperatura ambiente (‘State at Room Temp’). Para ello cree un
enum
adecuado para representarlo y construya una nuevadataclass
adecuada.Modifique
__repr__
y__str__
para que se imprima la información de cada átomo en forma clara y bella.¿Qué estrategia/s usaría para incorporar las temperaturas de fusión (‘Melting Point’) y de ebullición (‘Boiling Point’) de los átomos de la lista?