Emulando un Struct en Python

Introducción

Cuando vimos TDA en C, utilizábamos una combinación de typdef a modo de definir un nuevo tipo, con structs. El struct era una construcción que nos permitía componer, es decir, definir un tipo de dato que era compuesto por miembros. Por ejemplo, la Persona tenía un nombre, apellido, documento, que a su vez podía ser otro tipo compuesto con tipo y numero, etc.

Para nosotros estos tipos compuestos eran estructuras de datos. Es decir que solo contenían datos. Y cualquier operación que yo quería hacer sobre ellos, debería ser una función que los reciba por parámetro.

Ahora, Python es un Lenguaje Orientado a Objeos. En estos lenguajes aparece una idea muy poderosa de unificar estas dos cosas: el estado/datos, con el comportamiento. Eso es un objeto, y (en general) se definen a través de una clase.

Nosotros no vamos a ver objetos en esta materia, ya que tendrán luego otra materia exclusivamente para este paradigma. Pero sucede que Python no tiene una forma específica para definir "structs", ya que los objetos son más poderosos, no es necesario.

Entonces, toda esa introducción es para decir que nosotros vamos a buscar una forma de "emular" la idea de hacer structs, pero en el medio van a aparecer cositas del paradigma de objetos, como las clases.

No se asusten y por ahora traten de verlo como si definiéramos un struct.

Implementación

Tenemos (al menos) dos formas de emular un struct:

Clase con init

Vamos a definir una nueva clase y dentro de ella vamos a definir un método (función) con un nombre especial __init__(self) que actuará de "constructor". Es decir, cada vez que alguien cree una nueva instancia de nuestro struct, por ejemplo un Persona, se va a invocar esta función, y el primer parámetro, con el nombre especial self va a ser ese "objeto" persona nuevo.

Veamos un ejemplo. Para definir el struct Producto en C hacíamos

struct Producto {
    char[20] nombre;
    int precio;
}

Y luego teníamos una función constructora:

struct Producto* crearProducto(char* nombre, int precio) {
    struct Producto* producto = malloc(sizeof(struct Producto));
    strcpy(producto.nombre, nombre);
    producto.precio = precio;
    return producto;
}

En Python podemos escribir algo que emule esto así:

class Producto:
    def __init__(self, nombre, precio):
        self.nombre = nombre
        self.precio = precio

Donde estamos en realidad definiendo una Clase, y fíjense que estamos definiendo una función dentro de la clase, que se tiene que llamar __init__, y cuyo primer parámetro tiene que ser self, luego, puede tener cuantos parámetros nosotros querramos.

Esta función cumple dos funciones:

    • por un lado, cumple la función de la función crearProducto que vimos antes en el struct de C. Es decir que es la que va a recibir los valores para los atributos del Producto.
    • por otro lado, cumple también el rol de definir qué atributos tiene el tipo Producto. Ya que mirando esta función vemos que un producto va a tener un "nombre" y un "precio". Que no se definen en ningún otro lugar más que aquí.

Técnicamente lo que sucede es que esta función le está agregando miembros al objeto nuevo que referenciamos como self. Porque Python es un lenguaje dinámico, y me modificar un objeto agregándole miembros.

Luego esto se usaría así, para crear un nuevo Producto:

aceite = Producto("Aceite", 10.25)
harina = Producto("Harina", 5.30)

Ahora podemos hacer cualquier función que opere con nuestro struct como calcular el costo total de una lista de productos:

def calcularCosto(productos):
    total = 0
    for p in productos:
        total += p.precio
    return total
print calcularCosto([aceite, harina])

Pueden ver el código de este ejemplo acá

Clase vacía con función constructora

La forma de emular structs explicada recién, no escondía demasiado los detalles de la programación con objetos. Ya que teníamos que definir la función constructora dentro de la clase.

Otra variante que se nos ocurrió (podría haber muchas) es tratar de evitar eso, para no marearlos tanto. Entonces hacerlo más parecido a lo que hacíamos en C

    • Definir el tipo
    • Definir una función que construye una nueva instacia.

Quedaría así:

class Producto:
    pass
   
def crearProducto(nombre, precio):
    p = Producto()
    p.nombre = nombre
    p.precio = precio
    return p
print crearProducto("Aceite", 9.5)

Primero estamos definiendo una clase vacía Producto. La palabra reservada pass simplemente no hace nada. Se utiliza para definir un método vacío o una clase vacía, por el tema de la indentaciónde python.

Por otro lado, definimos una función nuestra, con el nombre que querramos, crearProducto, y la implementamos. Lo que hace es parecido a la de c, pero más simple (sin malloc, ni punteros, etc). Crea un nuevo producto, y le asigna los atributos nombre y precio con los valores que recibe por parámetro.

La última linea muestra como utilizarlo para crear un producto Aceite.

Pueden ver el código de este ejemplo acá

Redefiniendo como se ve un Struct al imprimirlo

En ambos casos van a ver que si imprimen uno de sus structs en consola, o bien imprimen una lista con sus structs, se va a ver medio feo. Algo así en nuestro ejemplo:

<__main__.Producto instance at 0x7f034aa0c0e0>

Esto es porque nuestro struct no sabe representarse como texto. Aunque si quisiéramos podemos "enseñarle". Esto se hace, definiendo una nueva función, que dado un objeto del tipo de nuestro struct, retorna un string.

La particularidad es que esta función debe que estar definida dentro del struct (como en el primer ejemplo definimos el __init__), y tiene que tener un nombre especial (__repr__).

Veamos un ejemplo:

class Servicio:
    def __init__(self, nombre, costo):
        self.nombre = nombre
        self.costo = costo
    def __repr__(self):
        return "Servicio[%s, %i]" % (self.nombre, self.costo)

Probamos eso ahora

print Servicio("Asesoria Legal", 110.40)
>> Servicio[Asesoria Legal, 110]

Acá de paso estamos mostrando un operador nuevo en Python, el % que se utiliza para genera un string reemplazando valores dinámicos, como en C teníamos el printf