Making Code Classy
The DreamCake Class
En esta sección, hablaremos sobre las clases. Y sobre pasteles. Comencemos con el pastel. Cuando tenemos un pedazo de brownie frente a nosotros, digamos, por el bien del argumento, que este pedazo de brownie es un object
del tipo de comida Cake
. Nuestro pedazo de pastel tiene algunas propiedades que otros pedazos de pastel podrían no tener. Una de esas propiedades podría ser la cobertura, que en nuestro caso podría ser glaseado de chocolate y una cereza. Necesitamos preguntarnos cómo se produjo este pedazo de pastel y de qué está compuesto.
Las recetas de cocina y las clases son muy similares porque definen cómo se produce un plato - o un objeto. Un pastel podría tener una cantidad fija de harina y agua, pero dejar que el chef añada un glaseado de chocolate o fresa. Una class
es una especificación de cómo se produce un objeto de algún tipo. El resultado de instanciar
dicha class
es un objeto de la clase. Veamos un ejemplo:
class DreamCake:
# Measurements are defined in grams or units
eggs = 4
sugar = 300
milk = 200
butter = 50
flour = 250
baking_soda = 20
vanilla = 10
topping = None
garnish = None
is_baked = False
def __init__(self, topping='No topping', garnish='No garnish'):
self.topping = topping
self.garnish = garnish
def bake(self):
self.is_baked = True
def is_cake_ready(self):
return self.is_baked
Al igual que las funciones se definieron usando la palabra clave def
, las clases se definen utilizando la palabra clave class
, seguida del nombre de la clase, en la convención de nombres CapWords. CapWords significa que todas las palabras utilizadas en el nombre están en mayúscula y juntas, como CapWordsArePrettyCool
.
Luego vienen los ingredientes que producen un pastel básico (y delicioso, por cierto), que en este ejemplo nunca cambiarán. Las variables topping
y garnish
se establecen en None
justo después del espacio. Esto sugiere que estas variables tendrán valores concretos asignados más adelante, en este caso, dentro de la función __init__
de la clase. Esta función se llama automáticamente en Python una vez que se solicita una nueva instancia de una clase. La función __init__
es un llamado "Magic Method". No cubriremos los Magic Methods en detalle, pero se ha incluido una nota sobre ellos en la parte opcional y avanzada al final.
Volviendo a la clase, observa sobre la función __init__
, el parámetro self
. Este parámetro es un parámetro obligatorio y primero
de todas
las funciones de la clase. En pocas palabras, las clases necesitan una forma de referirse a sus propias variables y funciones. Python está diseñado para requerir un parámetro "self
" en la primera posición de la firma de la función. Luego podemos referirnos a otras funciones dentro de las funciones de la clase llamando a self.other_func()
o self.topping
. Nota que no necesitamos proporcionar un valor para esto cuando llamamos a funciones en objetos de clase a pesar de este primer parámetro self
. Lo veremos más adelante.
Otro pequeño truco a notar son los valores predeterminados para los parámetros de función. Estos nos permiten omitir por completo especificar un valor para uno o más de los parámetros. Los parámetros se establecerán - en ese caso - en sus valores predeterminados según lo especificado, y topping
se establece en 'No topping'
a menos que se sobrescriba cuando creamos un objeto.
Por último, en este ejemplo, hemos definido una función dentro del alcance de la clase
como lo dicta el nivel de sangría. Esto significa que la función bake
es accesible solo
desde el código dentro de la clase misma (por ejemplo, código dentro de una función que llama a otra función) y objetos instanciados de la clase. Creamos algunos objetos de ejemplo para ilustrar mejor este comportamiento.
A Plain Cake
El topping y el garnish por defecto son "No topping" y "No garnish" para un pastel simple, respectivamente.
plain_cake = DreamCake()
A Chocolate Cake
Necesitamos agregar glaseado de chocolate en la parte superior para un pastel de chocolate, pero sin garnish (por defecto es "No garnish"
).
chocolate_cake = DreamCake(topping='Chocolate frosting')
A Luxury Cake
Nuestros pasteles de lujo tienen el topping y el garnish establecidos explícitamente.
luxury_strawberry_cake = DreamCake(topping='Strawberry frosting', garnish='Chocolate bits')
Esto también se puede especificar sin
usar parámetros nombrados para abreviar:
luxury_strawberry_cake = DreamCake('Strawberry frosting', 'Chocolate bits')
Como se mostró anteriormente, las clases se instancian en objetos de manera similar a cómo llamamos a funciones: escribimos el nombre seguido de paréntesis con posibles parámetros especificados. Ahora que tenemos objetos de la clase DreamCake
almacenados en variables, podemos llamar a las funciones de la clase en
las variables de objeto agregando un .
y la función.
Baking the Cake
chocolate_cake = DreamCake(topping='Chocolate frosting')
chocolate_cake.bake() # Call the function "bake" on the object.
is_cake_done = chocolate_cake.is_cake_ready()
print(is_cake_done) # Prints "True" because we called "bake" earlier
Observa la llamada a la función bake()
en el chocolate_cake
. Aunque la función bake
dentro de la clase tiene un parámetro self
, no necesitamos especificar su valor. No profundizaremos en las decisiones sobre por qué es así, es solo algo para recordar. Para finalizar, vale la pena mencionar que este estilo de código es una pequeña parte de la Programación Orientada a Objetos (OOP). Hay mucho más en OOP que simplemente usar clases - suficiente para un módulo completo por separado - pero en sus formas más simples, definimos clases, creamos objetos (o "instancias") de estas clases y las usamos para contener datos o llamar funciones.
Advanced Notes on Classes
Si eres nuevo en programación, no te desanimes si esto parece un poco complicado. Lo es. Las siguientes son notas muy breves sobre algunos usos más avanzados de las clases, que en su mayoría no necesitamos preocuparnos en nuestra programación diaria, pero que son bastante interesantes de conocer.
Una cosa que prometí explicar brevemente son los Magic Methods. Los Magic Methods son funciones - o métodos, como también se les llama en muchos lenguajes de programación - que existen por defecto y tienen una implementación predeterminada en todas las clases. Esto se debe a la class hierarchy
en Python, donde todas las clases heredan de una clase base llamada object
("object" es el nombre de la clase base - quizás un poco confuso).
Esta declaración abre una gran caja sobre OOP que no exploraremos aquí, pero siéntete libre de investigar "Python class inheritance" y frases similares por tu cuenta. En resumen, "class inheritance" significa que una clase puede heredar el tipo, sus funciones y variables internas. Esta clase base da a los objetos algunas funcionalidades básicas, por ejemplo, la capacidad de compararse entre sí (¿es un pastel igual a otro?) o obtener una string representation
del objeto.
Supongamos que tenemos una clase Circle
, el objeto en sí es un dato crudo almacenado en memoria que solo Python sabe leer, pero la string representation
de un objeto Circle
podría ser "Circle(r=5)"
, describiendo un círculo con un radio de 5. El Magic Method responsable de devolver una representación en cadena de un objeto es __str__
. Llamar a esta función en un objeto es similar a llamar a str(...)
con el objeto como parámetro. Por ejemplo, considera el siguiente fragmento de código desde mi Python IDLE:
Overriding Magic Methods (IDLE)
>>> class Circle:
... def __init__(self, radius):
... self.radius = radius
...
... def __str__(self):
... return f'Circle(r={self.radius})'
...
>>> my_circle = Circle(5)
>>> str(my_circle)
'Circle(r=5)'
Si no sobrescribiéramos la función __str__
, el código seguiría funcionando, pero la salida sería menos significativa:
'<__main__.Circle object at 0x022FFB98>'
Este string representa un objeto Circle
dentro de __main__
(aquí, el IDLE), ubicado en la dirección de memoria 0x022FFB98
.
Otros dos Magic Methods que vale la pena mencionar son las funciones __enter__
y __exit__
, que nos permiten crear clases que admiten el uso de la palabra clave with
. La palabra clave with
nos permite especificar la funcionalidad predeterminada de una clase para procedimientos de configuración (build-up) y desmontaje (teardown). Por ejemplo, la clase C2TcpConnection
, que representa una conexión TCP a un servidor C2. El paso de configuración podría incluir iniciar un socket e intentar autenticar datos de entrada desde fuentes externas. El paso de desmontaje podría incluir el manejo adecuado de errores y garantizar el cierre correcto del socket después de su uso. Esto es avanzado pero divertido y un estilo de codificación "Pythonic" que te recomiendo investigar.
Consideremos brevemente un ejemplo antes de pasar a la siguiente sección del módulo.
Class Supporting WITH Context Manager
class Foo():
def __enter__(self):
print("Enter...")
def __exit__(self, type, value, traceback):
print("...and exit.")
Aquí se define una clase Foo
con una función __enter__
y una función __exit__
simples, que no hacen más que imprimir un mensaje. Esto nos permite usar la cláusula with
para "envolver" este supuesto código repetitivo reutilizable alrededor de código concreto, por ejemplo:
with Foo():
print("Hello world!")
Esto imprime lo siguiente en la consola:
Enter...
Hello world!
...and exit.
Además, podemos cambiar la cláusula with a algo como with Foo() as foo
, lo que nos permite hacer referencia al objeto instanciado de Foo
usado para envolver nuestro código. Esto es útil si, por ejemplo, la clase Foo
tiene funciones que queremos llamar dentro de la cláusula with, como get_connection_status
en el ejemplo de creación de una clase C2TcpConnection
. Un uso más frecuente de la cláusula with es with open('/path/to/file.txt', 'w') as wr
, que abre un archivo para escritura. Luego podemos usar wr.write('something')
para escribir "something" en el archivo. Al final de la cláusula with, no necesitamos cerrar los flujos de salida al archivo - la funcionalidad de desmontaje en la clase open
se encarga de eso.