Saltar a contenido

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.