Comparar objetos en Python y otras sutilezas

En esta entrada vamos a continuar aprendiendo técnicas y conceptos relacionados con la programación orientada a objetos, que en la anterior entrega quedaron postergados de forma deliberada porque de por sí daban para bastante discusión.

En concreto, veremos tres cosas muy útiles cuando se trata de hacer programas potentes:

  • Comparación entre objetos de una misma clase: cómo comparar objetos correctamente.
  • Acceso a los métodos y variables de la superclase, o clase de la cual heredamos.
  • Ocultación de la información, o encapsulamiento.

¡Vamos allá!

Comparación entre objetos

La comparación entre objetos tiene truco. Siempre tiene truco. Define la clase siguiente en IDLE:

class Test:
    def __init__(self, cantidad, color):
        self.cantidad = cantidad
        self.color = color
    def set_color(self, color):
        self.color = color
    def set_id (self, cantidad):
        self.cantidad = cantidad

Esta clase es un simple contenedor de dos variables: cantidad y color. Si creamos dos objetos con valores distintos y los comparamos en IDLE, veremos que el resultado que arroja (False ) es el esperado.

t1 = Test(0, "rojo")
t2 = Test(1, "verde")
t1 == t2

 

Comparamos dos objetos con atributos cuyo valor es distinto; de momento todo bien
Pinta bien

Sin embargo, si creamos una tercera instancia con valores idénticos a la segunda, t3 = Test(1, "verde"), veremos que el resultado sigue siendo False , y esta vez no es el esperado.

Esto no nos lo esperábamos.
Esto no nos lo esperábamos.

¿Qué ocurre aquí? Que Python, si no le decimos otra cosa, compara las instancias de los objetos. Es decir, lo que nos dice Python es si las dos variables, t2 y t2, se refieren al mismo objeto, no comparan el contenido de los objetos. A un nivel máximo de detalle, lo que está comparando Python es a qué zona de memoria están apuntando esas variables:

¿Qué estamos comparando?
Las dos variables se refieren a objetos diferentes, y eso es lo que compara Python

¿Qué podemos hacer para comparar los contenidos? Tenemos dos opciones:

  1. Sobrecargar el operador ==
  2. Definir una función más, que realice las comparaciones necesarias

Ambas tienen sentido, cada una bajo ciertas circunstancias.

Sobrecargar el operador == … y los demás

Es tan sencillo como definir una función llamada __eq__ que recibirá como parámetros el objeto mismo, es decir, self , y el otro, que llamaremos other . Lo que debemos hacer dentro de esa función es… obvio: comparar los atributos del objeto, uno por uno. Si son iguales devolveremos True , y si no lo son, False .

Redefine la clase Test para que incluya la función resaltada:

class Test:
    def __init__(self, cantidad, color):
        self.cantidad = cantidad
        self.color = color
    def set_color(self, color):
        self.color = color
    def set_id (self, cantidad):
        self.cantidad = cantidad
    def __eq__(self, other):
        if self.cantidad == other.cantidad and self.color == other.color:
            return True
        else:
            return False

Tendrás que recrear todos los objetos (t1, t2 y t3), pero verás que ahora sí que sí:

Comparación por sobrecarga
Comparación por sobrecarga

No basta con sobreescribir solamente el método de igualdad si quieres tener todos los operadores referidos al contenido de tu objeto, tendrás que trabajar algo más. Igual que __eq__ tienes otros métodos al respecto como __le__ (menor o igual), __lt__ (menor estricto), __ne__ (distinto), __gt__ (mayor estricto), __ge__ (mayor o igual).

¿Qué ocurre con esta forma de resolver nuestro problema? Pues que si necesitásemos la funcionalidad por defecto del operador == , por la razón que fuera, la habríamos perdido.

Definir otra función de comparación

Si no queremos perder el comportamiento por defecto, o nativo, de los operadores, lo que podemos hacer es llamar a las funciones de otro modo, sin más. Redefine tu clase para que, en lugar de __eq__ se llame de otro modo, como puede ser es_igual(self, other). De ese modo, el operador original de Python conservará su función, que es la que te dice si las variables referencian a la misma instancia, o no.

class Test:
    def __init__(self, cantidad, color):
        self.cantidad = cantidad
        self.color = color
    def set_color(self, color):
        self.color = color
    def set_id (self, cantidad):
        self.cantidad = cantidad
    def es_igual(self, other):
        if self.cantidad == other.cantidad and self.color == other.color:
            return True
        else:
            return False

Ahora ya no podrás usar el operador, si no que tendrás que invocar la función directamente:

Función equivalente
Función equivalente

Acceso a los métodos y variables definidos en la clase de la que heredamos, o superclase

En ocasiones nos puede ser necesario acceder a datos o comportamientos que están definidos en la clase de la cual heredamos a la hora de extenderlos. No me refiero a, desde fuera, invocar los métodos heredados (en el artículo anterior, estaría hablando de usar el método frenar  en los objetos de clase BiciCarreras , estando definido en la clase Bicicleta ), sino a invocarlos desde la definición de la clase que extiende a la superclase.

Para hacerlo debemos hacer dos cosas:

  1. Todas las clases deben pertenecer a lo que en Python se llama “clases del nuevo estilo”. Esto se consigue haciendo que la clase de la que vamos a heredar herede, a su vez, de object .
  2. Una vez hecho esto, dentro de nuestras clases, podremos usar la función super(Clase, self) .

La función super(Clase, self)  localiza en la jerarquía de objetos de self  la definición de la clase de la cual nuestra Clase  hereda, y nos permite ejecutar sus métodos desde nuestro código para ampliarlos, por ejemplo.

Vamos a conseguir que una clase Prueba, heredando de Test, redefina el método es_igual escribiendo un mensaje por la pantalla además de invocar al método es_igual tal y como se define en la clase Test.

Lo primero, volvemos a declarar la clase Test para que extienda (o herede de) object:

class Test(object):
    def __init__(self, cantidad, color):
        self.cantidad = cantidad
        self.color = color
    def set_color(self, color):
        self.color = color
    def set_id (self, cantidad):
        self.cantidad = cantidad
    def es_igual(self, other):
        if self.cantidad == other.cantidad and self.color == other.color:
            return True
        else:
            return False

A continuación, definimos la clase Prueba  tomando como superclase Test , y usando la función super()  para acceder al método tal y como se definió en Test:

class Prueba(Test):
    def set_color(self, color):
        self.color = color
    def set_id (self, cantidad):
        self.cantidad = cantidad
    def es_igual(self, other):
        print("Llamando a la superclase")
        return super(Prueba, self).es_igual(other)

Si ahora creas dos instancias de Prueba y ejecutas su método de comparación, verás que el efecto es el pretendido:

Ampliación de métodos por sobrecarga
Ampliación de métodos por sobrecarga

Para acceder a las variables basta con hacer self.variable , puesto que igual que no tiene sentido sobreescribirlas, no tiene sentido acceder a las de la superclase: se heredan tal cual.

Ocultación de métodos y variables en nuestro código

Es algo que hemos visto ya en nuestros experimentos de videojuegos. Reproduzco aquí el fragmento, tal cual, que usaba para introducir una clase que representaba un cuadrado:

class Cuadrado:
    def __init__(self, x, y, lado, color, fondo):
        self.x = x
        self.y = y
        self.color = color
        self.fondo = fondo
        self.lado = lado
        self.rect = pygame.Rect(self.x,
                                self.y,
                                self.lado,
                                self.lado)

    def __pinta(self, pantalla, color):
        'Realiza el dibujo efectivo'
        pygame.draw.rect(pantalla, color, self.rect, 0)
        
    def pinta(self, pantalla):
        'Pinta el cuadrado con el color propio'
        self.__pinta(pantalla, self.color)

    def borra(self, pantalla):
        'Borra el cuadrado'
        # Realmente lo pinta con el color de fondo
        self.__pinta(pantalla, self.fondo)

    def colisiona_con(self, cuadrado2):
        'Comprueba la colisión con otro cuadrado'
        return self.rect.colliderect(cuadrado2.rect)

    def mover(self, avance_x, avance_y):
        'avance del cuadrado en un cuadro (frame)'
        self.rect.move_ip(avance_x, avance_y)

Como veis, he añadido la creación de un objeto de tipo pygame.Rect  en el constructor, pasándole la posición inicial y la dimensión de su lado. Después he implementado su comportamiento haciendo uso del objeto rect.

El método __pinta  está oculto para el programador, de forma que si intentáseis invocarla directamente, el intérprete os devolvería un error. Sólo se puede usar desde dentro del propio objeto, como en los métodos pinta y borra: es una función privada gracias al prefijo “__”, esto es, dos caracteres de subrayado (aunque realmente no está muy bien protegida, pero eso es otro tema).

Puedes probarlo en IDLE (no te hará falta ni siquiera definir una pantalla, aunque sí importar Pygame) como verás en la imagen siguiente:

Los miembros de una clase, si empiezan por __, están ocultos (o son privados)
Los miembros de una clase, si empiezan por __, están ocultos (o son privados)

Los temas tratados en este artículo pueden resultar, a golpe y porrazo, algo áridos. Sin embargo, son útiles y a medida que avancemos en nuestro camino y ganemos experiencia los iremos valorando más y más. Mientras tanto, no dudéis en dejarme vuestros comentarios en este artículo o a través del formulario de contacto.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *