Programar Juegos Arcade con Python y Pygame

Programar Juegos Arcade
con Python y Pygame

Chapter 12: Introducción a las Clases

Las clases y los objetos son poderosas herramientas de programación. Facilitan la tarea de programar. De hecho, ya estás familiarizado con el concepto de clases y objetos. Una clase es la “clasificación” de un objeto. Tal como “persona” o “imagen.” Un objeto es una instancia particular de una clase. Tal como “María” es una instancia de “Persona.”

Los objetos poseen atributos, tales como el nombre, altura y edad de una persona. Los objetos también poseen métodos. Los métodos definen qué acciones pueden realizar los objetos, tales como correr, saltar o sentarse.

12.1 ¿Por Qué Estudiar Clases?

Vídeo: Por qué Clases?

Cada personaje en un juego de aventuras necesita datos: un nombre, ubicación, fuerza, está levantando el brazo, en qué dirección se dirige, etc. Además, esos personajes hacen cosas. Corren, saltan, golpean y hablan.

Si no empleamos clases, nuestro código Python para guardar todo esto tendría este aspecto:

nombre = "Link"
sexo = "Varón"
golpe_puntos_max = 50
golpe_puntos_actuales = 50

Para poder hacer cualquier cosa con este personaje, necesitaremos pasar esos datos a una función:

def mostrar_personaje(nombre, sexo, golpe_puntos_max, golpe_puntos_actuales):
    print(nombre, sexo, golpe_puntos_max, golpe_puntos_actuales)

Ahora imagínate crear un programa que tenga un conjunto de variables como ésta por cada personaje, monstruo y/o objeto de tu juego. Entonces, necesitamos crear funciones que trabajen con todos ellos. De repente, parece que nos hemos metido en un berenjenal de datos, y ya nada de esto parece divertido.

¡Pero espera, que esto empeora aún más! A medida que nuestro juego se expanda, necesitaremos agregar nuevos campos que describan a nuestro personaje. Por ejemplo, en este caso hemos agregado velocidad_max:

nombre = "Link"
sexo = "Varón"
golpe_puntos_max = 50
golpe_puntos_actuales = 50
velocidad_max = 10

def mostrar_personaje(nombre, sexo, golpe_puntos_max, golpe_puntos_actuales,velocidad_max):
    print(nombre, sexo, golpe_puntos_max, golpe_puntos_actuales)

En el ejemplo anterior solo hay una función. Pero en un videojuego grande, podríamos tener cientos de funciones que trataran con el personaje principal. Añadir un nuevo campo que ayude a describir lo que tiene y hace un personaje, requeriría ir una por una de las funciones, y añadirlo a su lista de parámetros. Eso significaría un montón de trabajo. Y, a lo mejor, necesitamos añadir velocidad_max a otros tipos de personajes tales como monstruos. Debe existir una forma más eficaz de hacer esto. De alguna manera, nuestro programa necesita empaquetar todos esos campos de datos y así poder manejarlos más fácilmente.

12.2 Definir y Crear Clases Simples

Vídeo: Definir Clases Simples

Una forma eficaz de manejar múltiples atributos es definir una estructura que contenga toda esa información. Podemos darle un nombre a ese “agrupamiento”, algo así como Personaje o Dirección. Esto se puede hacer fácilmente en Python, y en cualquier otro lenguaje moderno, usando una clase.

Por ejemplo, podemos definir una clase que represente un personaje del juego:

class Personaje():
    """ Esta es una clase que representa al protagonista principal del juego. """
    def __init__(self):
        """ Este es un método que establece las variables del objeto. """
        self.name = "Luis"
        self.sexo = "Varón"
        self.puntos_impacto_max = 50
        self.puntos_impacto_actuales = 50
        self.vel_max = 10
        self.cantidad_escudos = 8

Este es otro ejemplo, definimos una clase que contenga todos los campos para una dirección:

class Direccion():
    """ Contiene todos los campos para una dirección postal. """
    def __init__(self):
        """ Establece los campos de la dirección. """
        self.nombre = ""
        self.via1 = ""
        self.via2 = ""
        self.ciudad = ""
        self.provincia = ""
        self.cod_postal = ""

En el código anterior, Direccion es el nombre de la clase. Las variables en la clase, como nombre y ciudad, son conocidas como atributos o campos. (Observa las similitudes y diferencias entre declarar una clase y una función.)

Al contrario que con las funciones y variables, los nombres de las clases deberían empezar con una letra mayúscula. Aunque es totalmente posible hacerlo con minúsculas, no se considera una buena práctica.

La expresión def __init__(self): es una función especial llamada constructor la cual se ejecuta automáticamente cuando la clase es creada. Hablaremos más del constructor en un momento

El self. es algo así como el pronombre mi. Cuando se encuentra dentro de la clase Direccion, estamos hablando de mi nombre, de mi ciudad, etc. No debemos usar self., fuera de la definición de la clase Direccion, para referirnos a un campo de Direccion ¿Por qué? Porque como pasa con el pronombre “mi”, éste tiene un significado totalmente distinto cuando es dicho por otra persona!

Para visualizar mejor las clases y cómo se relacionan, los programadores usan habitualmente diagramas. En la Figura 12.1 podemos ver un diagrama para la clase Direccion. Observa cómo el nombre de la clase se sitúa por encima del nombre de los atributos. A la derecha de éstos se encuentra el tipo de dato que contienen, tales como cadenas de textos (strings) o números enteros (integer).

fig.address_example_3
Figure 12.1: Diagrama de Clase

El código de la clase la define como tal, pero realmente no crea una instancia de ella. El código le dice al ordenador qué campos tiene una dirección y cuáles serán sus valores iniciales. Pero aún no tenemos una dirección real. Podemos definir una clase sin crearla, de la misma forma que podemos definir una función sin llegar a llamarla. Para crear una clase y establecer sus campos, observa el siguiente ejemplo:

# Crea una dirección
casa_direccion = Direccion()

# Establece los campos de la dirección
casa_direccion.nombre = "John Smith"
casa_direccion.via1 = "701 N. C Street"
casa_direccion.via2 = "Carver Science Building"
casa_direccion.ciudad = "Indianola"
casa_direccion.provincia = "IA"
casa_direccion.cod_postal = "50125"

En la línea 2 se crea una instancia para la clase dirección. Observa cómo usamos el nombre de clase Direccion seguido de paréntesis. El nombre de la variable puede ser cualquiera que siga las reglas habituales.

Para establecer los campos de la clase, el programa debe usar el operador punto. Este operador es el punto que está en medio de casa_direccion y el nombre del campo. Observa cómo hacemos lo mismo para las líneas de la 5 a la 10.

Un problema muy común cuando trabajamos con clases, es no especificar con qué instancia de la clase queremos hacerlo. Si solo se ha creado una dirección, es comprensible asumir que el ordenador sabrá cómo usar la dirección de la que estás hablando. Sin embargo, esto no siempre es así. Observa el siguiente ejemplo:

class Direccion():
    def __init__(self):
        self.nombre = ""
        self.via1 = ""
        self.via2 = ""
        self.ciudad = ""
        self.provincia = ""
        self.cod_postal = ""

# Crea una dirección
mi_direccion = Direccion()

# Cuidado! Esto no establece el nombre para la dirección!
nombre = "Dr. Craven"

# Esto tampoco
Direccion.nombre = "Dr. Craven"

# Esto sí está bien:
mi_direccion.nombre = "Dr. Craven"

Podemos crear una segunda dirección y usar los campos de ambas instancias. Mira el siguiente ejemplo:

class Direccion():
    def __init__(self):
        self.nombre = ""
        self.via1 = ""
        self.via2 = ""
        self.ciudad = ""
        self.provincia = ""
        self.cod_postal = ""

# Crea una dirección
casa_direccion = Direccion()

# Establece los campos de la dirección
casa_direccion.nombre = "John Smith"
casa_direccion.via1 = "701 N. C Street"
casa_direccion.via2 = "Carver Science Building"
casa_direccion.ciudad = "Indianola"
casa_direccion.provincia = "IA"
casa_direccion.cod_postal = "50125"

# Crea otra dirección
casa_vacaciones_direccion = Direccion()

#Establece los campos de la nueva dirección
casa_vacaciones_direccion.nombre = "John Smith"
casa_vacaciones_direccion.via1 = "1122 Main Street"
casa_vacaciones_direccion.via2 = ""
casa_vacaciones_direccion.ciudad = "Panama City Beach"
casa_vacaciones_direccion.provincia = "FL"
casa_vacaciones_direccion.cod_postal = "32407"

print("La dirección principal del cliente está en " + casa_direccion.ciudad)
print("Su casa de vacaciones está en " + casa_vacaciones_direccion.ciudad)

La línea 11 crea la primera instancia para Direccion; la línea 22 crea la segunda. La variable casa_direccion apunta a la primera instancia y casa_vacaciones_direccion apunta a la segunda.

Las líneas de la 25-30 establecen los campos para esta nueva instancia de clase. La línea 32 imprime la ciudad donde está la vivienda habitual, porque casa_direccion aparece antes del operador punto. La línea 33 imprime su casa de vacaciones debido a que casa_vacaciones_direccion aparece antes del operador punto.

En este ejemplo, decimos que Direccion es la clase, ya que define una nueva clasificación para un objeto de datos. Las variables casa_direccion y casa_vacaciones_direccion aluden a objetos, porque se refieren a instancias reales de la clase Direccion. Básicamente, diríamos que un objeto es una instancia de una determinada clase. De la misma forma que “Bob” y “Nancy” son instancias de la clase Humana.

Podemos visualizar la ejecución del siguiente código utilizando www.pythontutor.com. Existen tres variables en juego. Una apunta a la definición de la clase Direccion. Las otras dos apuntan a diferentes objetos de direcciones y a los datos contenidos en ellos.

fig.two_addresses
Figure 12.2: Dos Direcciones

Colocar muchos campos de datos dentro de una clase, facilita el tránsito de estos dentro y fuera de una función. En el siguiente código, la función toma una dirección como parámetro y lo imprime en pantalla. No es necesario pasar un parámetro para cada campo de la dirección.

# Imprime una dirección en pantalla
def imprimir_direccion(direccion):
    print(direccion.nombre)
    # Si existe una via1 en esa dirección, imprímela
    if( len(direccion.via1) > 0 ):
        print (direccion.via1)
    # Si existe una via2 en esa dirección, imprímela
    if( len(direccion.via2) > 0 ):
        print( direccion.via2 )
    print( direccion.ciudad+", "+direccion.provincia+" "+direccion.cod_postal )

imprimir_direccion( casa_direccion )
print()
imprimir_direccion( casa_vacaciones_direccion )

12.3 Añadir Métodos a las Clases

Vídeo: Métodos

Además de poseer atributos, las clases también pueden tener métodos. Un método es una función que existe dentro de una clase. Ampliando un ejemplo anterior, definimos la clase Perro y le añadimos el código para que ladre:

class Perro():
    def __init__(self):
        self.edad = 0
        self.nombre = ""
        self.peso = 0

    def ladra(self):
        print("Guau")

Entre las líneas 7-8 está contenida la definición del método. Las definiciones de métodos en una clase, son casi idénticas a las definiciones de las funciones. La mayor diferencia es la inclusión del parámetro self en la línea 7. El primer parámetro, en cualquier método de una clase, debe ser self. Este parámetro es imprescindible, incluso aunque la función no haga uso de él.

Estas son las ideas que tenemos que tener en mente cuando creamos métodos para clases:

Podemos invocar a los métodos de manera similar a cómo referenciamos atributos de un objeto. Observa el siguiente código:

mi_perro = Perro()

mi_perro.nombre = "Spot"
mi_perro.peso = 20
mi_perro.edad = 3

mi_perro.ladra()

La línea 1 crea al perro. Las líneas 3-5 establecen los atributos de los objetos. La línea 7 llama a la función ladra. Observa que a pesar de que la función ladra tiene un parámetro self, al llamarla no le pasamos nada. Esto se debe a que se asume que el primer parámetro es una referencia al objeto perro en sí mismo. Entre bastidores Python realiza una llamada que tiene este aspecto:

# Ejemplo, no es legal realmente
Perro.ladra(mi_perro)

Si la función ladra necesita hacer referencia a cualquiera de los atributos, lo hace utilizando la variable de referencia self. Por ejemplo, podemos modificar la clase Perro, de forma que cuando el perro ladre, también imprima su nombre. En el siguiente código, se accede al atributo nombre usando el operador punto y la referencia self.

    def ladra(self):
        print( "dice Guau", self.nombre )

Los atributos son adjetivos y los métodos, verbos. El esquema de una clase podría ser como el de la Figura 12.3.

fig.dog_2_1
Figure 12.3: Clase Perro

12.3.1 Ejemplo: Clase Pelota

Podemos usar este ejemplo en Python/Pygame para dibujar una pelota. El tener todos los parámetros contenidos dentro de una clase, nos facilita el manejo de los datos. El diagrama para la clase Pelota se puede ver en la Figura 12.4.

fig.ball_2_1
Figure 12.4: Clase Pelota
class Pelota():
    def __init__(self):
    # --- Atributos de la Clase ---
        # Posición de la pelota
        self.x = 0
        self.y = 0

        # vector Pelota
        self.cambio_x = 0
        self.cambio_y = 0

        # Dimensiones de la Pelota
        self.talla = 10

        # color de la Pelota
        self.color = [255,255,255]

        # --- Métodos para la Clase ---
    def mover(self):
        self.x += self.cambio_x
        self.y += self.cambio_y

    def dibujar(self, pantalla):
        pygame.draw.circle(pantalla, self.color, [self.x, self.y], self.talla )

El siguiente código, que creará y establecerá los atributos de la pelota, irá antes del bucle principal:

laPelota = Pelota()
laPelota.x = 100
laPelota.y = 100
laPelota.cambio_x = 2
laPelota.cambio_y = 1
laPelota.color = [255,0,0]

El siguiente código, que dibujará y moverá la pelota, irá dentro del bucle principal:

laPelota.mover()
laPelota.dibujar(pantalla)

12.4 Referencias

Vídeo: Referencias

Llegamos al punto donde se separan los verdaderos programadores de los aspirantes a serlo: la comprensión de las referencias de clases. Observa el siguiente código:

class Persona:
    def __init__(self):
        self.nombre = ""
        self.dinero = 0

bob = Persona()
bob.nombre = "Bob"
bob.dinero = 100

nancy = Persona()
nancy.nombre = "Nancy"

print(bob.nombre, "tiene", bob.dinero, "dólares.")
print(nancy.nombre, "tiene", nancy.dinero, "dólares.")

El código anterior crea dos instancias para la clase Persona(). Podemos visualizar las dos clases usando www.pythontutor.com Figura 12.5.

fig.two_persons
Figure 12.5: Dos Personas

El anterior código no tiene nada nuevo, pero el siguiente sí:

class Persona:
    def __init__(self):
        self.nombre = ""
        self.dinero = 0

bob = Persona()
bob.nombre = "Bob"
bob.dinero = 100

nancy = bob
nancy.nombre = "Nancy"

print(bob.nombre, "tiene", bob.dinero, "dólares.")
print(nancy.nombre, "tiene", nancy.dinero, "dólares.")

Ves la diferencia en la línea 10?

Un error muy común al trabajar con objetos, es asumir que la variable bob es el objeto Persona. Pero no es así. La variable bob es una referencia al objeto Persona. Es decir, guarda la dirección de memoria donde se encuentra el objeto, no el objeto en sí.

Si realmente bob fuera el objeto, entonces, la línea 9 podría crear una copia del objeto, con lo que tendríamos dos objetos en existencias. En principio, la salida del programa debería mostrar que tanto Bob como Nancy tienen 100 dólares. Pero cuando realmente ejecutamos el programa, nos encontramos con esta otra salida en su lugar:

Nancy tiene 100 dólares.
Nancy tiene 100 dólares.

Lo que almacena bob es una referencia al objeto. En lugar de usar el término referencia, uno podría llamarlo también; dirección, puntero, o handle (manija). Una referencia es una dirección en la memoria del ordenador. Un lugar donde es almacenado el objeto. Esta dirección es un número hexadecimal, que si lo imprimiéramos, tendría un aspecto similar a 0x1e504. Al ejecutarse la línea 9, lo que realmente se copia es la dirección y no el objeto al que apunta esta dirección. Observa la Figura 12.6.

fig.ref_example1
Figure 12.6: Referencias a Clases

También podemos ejecutar este código en www.pythontutor.com para observar cómo ambas variables apuntan al mismo objeto.

fig.one_person
Figure 12.7: Una Persona, Dos Punteros

12.4.1 Funciones y Referencias

Observa el siguiente código. En la línea 1 se crea una función que toma un número como parámetro. La variable dinero contiene una copia del número que le han pasado a la función. Sumarle 100 a ese número, no cambia el valor almacenado por bob.dinero en la línea 11. Por ello, la sentencia print en la línea 14, imprime 100 en lugar de 200.

def dameDinero1(dinero):
    dinero += 100

class Persona():
    def __init__(self):
        self.nombre = ""
        self.dinero = 0
    
bob = Persona()
bob.nombre = "Bob"
bob.dinero = 100

dameDinero1(bob.dinero)
print(bob.dinero)

Si ejecutamos esto en PythonTutor, podremos observar que existen dos instancias para la variable dinero. Una de ellas es una copia y, además, local a la función dameDinero1.

fig.function_references_1
Figure 12.8: Referencias a Funciones

Observa el siguiente código adicional. Este código provoca que bob.dinero se incremente y que la sentencia print escriba 200.

def dameDinero2(persona):
    persona.dinero += 100

dameDinero2(bob)
print(bob.dinero)

¿Por qué ocurre esto? Pues esto se debe a que persona contiene una copia de la dirección de memoria del objeto, y no el objeto en sí mismo. Pensemos en ello como si se tratase del número de una cuenta bancaria. La función tiene una copia de ese número de cuenta, no una copia de todos nuestros depósitos. Por ello, usando una copia del número de cuenta para depositar 100 dólares, conseguimos que el balance bancario de Bob aumente.

fig.function_references_2
Figure 12.9: Referencias a Funciones

Los arrays funcionan de la misma forma. Una función que tome un array (lista) como parámetro y modifique los valores del mismo, estará modificando el mismo array que el propio código ha creado. Lo que se copia es la dirección del array, no el array entero.

12.4.2 Preguntas de Repaso

  1. Crea una clase llamada Gato. Otórgale atributos tales como nombre, color, y peso. Dale un método llamado miau.
  2. Crea una instancia de la clase gato, completa sus atributos y llama al método miau.
  3. Crea una clase llamada Monstruo. Dale un atributo para nombre y un atributo entero (int) para resistencia. Crea un método llamado reducirResistencia que tome un parámetro cantidad y reduzca en esa cantidad la resistencia de nuestro monstruo. Dentro de ese método se debe imprimir que el animal ha muerto si su resistencia está por debajo de cero.

12.5 Constructores

Vídeo: Constructores

Tenemos un terrible problema con la siguiente clase Perro. Por defecto, cuando creamos el perro, éste no tiene nombre. Todos sabemos que los perros deben tener un nombre. No podemos permitir que nazcan perros a los que nunca se les asigne un nombre. Pero el código de abajo permite esto, y ese perro nunca tendrá un nombre.

class Perro()
    def __init__(self):
        self.nombre = ""

mi_perro = Perro()

Python no quiere que esto suceda. Por ello, en Python, las clases tienen una función especial que es llamada en el momento en que una instancia de esa clase es creada. Añadiendo esa función, llamada constructor, el programador puede añadir el código necesario, el cual, automáticamente, será ejecutado cada vez que una instancia de la clase sea creada. Observa el siguiente ejemplo:

class Perro():
    def __init__(self):
    # Llamada al constructor cuando creamos un objeto de este tipo
    	self.nombre = ""
        print("Ha nacido un perro nuevo!")

# Esto crea al perro
mi_perro = Perro()

El constructor empieza en la línea 2. Debe ser nombrado como __init__. Hay dos guiones bajos antes y después de la palabra init. Un error muy común es usar uno solo.

El constructor debe tomar self como primer parámetro, tal como sucede con otros métodos en una clase. Cuando el programa es ejecutado, imprimirá:
Ha nacido un perro nuevo!
Cuando el objeto Perro es creado en la línea 8, la función __init__ es llamada automáticamente y el mensaje aparece en pantalla.

12.5.1 Evitar este Error

Todo lo necesario para un método lo colocamos dentro de una sola definición. No lo definimos dos veces. Por ejemplo:

# Mal:
class Perro():
    def __init__(self):
        self.edad = 0
        self.nombre = ""
        self.peso = 0

    def __init__(self):
        print("¡Un perro nuevo!")

El ordenador sencillamente ignorará el primer __init__ y se irá a la última definición. Haz esto en su lugar:

# Bien:
class Perro():
    def __init__(self):
        self.edad = 0
        self.nombre = ""
        self.peso = 0
        print("¡Un perro nuevo!")

Podemos utilizar un constructor para inicializar y establecer los datos de un objeto. El ejemplo anterior de la clase Perro permitía que el atributo nombre permaneciera en blanco aún después de la creación del objeto perro. ¿Cómo podemos impedir esto? Muchos objetos necesitan tener valores en el momento preciso de ser creados. Podemos usar la función constructor para lograrlo. Observa el siguiente código:

class Perro():
   
    def __init__(self, nombre_nuevo):
        """Constructor"""
        self.nombre = nombre_nuevo

# Esto crea al perro
mi_perro = Perro("Spot")

# Imprime el nombre para verificar que así ha sido
print(mi_perro.nombre)

# Esta línea producirá un error porque
# el nombre no ha sido introducido.
su_perro = Perro()

Ahora, en la línea 3, la función constructor tiene un nuevo parámetro llamado nombre_nuevo. El valor de éste es usado para establecer el atributo nombre para la clase Perro en la línea 8. Ya no será posible crear una clase Perro que no tenga un nombre. En la línea 15 se intenta esto mismo. Se produce un error en Python y el código no se ejecuta. Un error común es nombrar al parámetro de la función __init__ de la misma forma que al atributo, y asumir que esos dos valores se sincronizarán automáticamente. Esto no sucederá.

12.5.2 Preguntas de Repaso

  1. ¿Cómo debería comenzar el nombre de una clase; por mayúsculas o minúsculas?
  2. ¿Cómo debería comenzar el nombre de un método; por mayúsculas o minúsculas?
  3. ¿Cómo debería comenzar el nombre de un atributo; por mayúsculas o minúsculas?
  4. ¿Qué es lo que deberíamos listar primero en un clase; atributos o métodos?
  5. ¿De qué otra forma podemos llamar a una referencia?
  6. ¿De qué otra forma podemos llamar a una variable de instancia?
  7. ¿Cuál es el nombre para la instancia de una clase?
  8. ¿Crea una clase llamada Estrella que imprima “Ha nacido una estrella!” cada vez que es creada.
  9. Crea una clase llamada Monstruo con atributos para resistencia y nombre. Añádele un constructor que establezca la resistencia y nombre del objeto, con datos que se pasen como parámetros.

12.6 Herencia

Vídeo: Herencia

Otro rasgo poderoso de las clases y objetos es la posibilidad de hacer uso de la herencia. Es posible crear una clase que herede todos los atributos y métodos de una clase padre.

Por ejemplo, un programa podría crear una clase llamada Barco que tenga todos los atributos necesarios para dibujar un barco en un juego:

class Barco():
    def __init__(self):
        self.tonelaje = 0
        self.nombre = ""
        self.esta_atracado = True

    def atracar(self):
        if self.esta_atracado:
            print("Ya has atracado.")
        else:
            self.esta_atracado = True
            print("Atracando")

    def desatracar(self):
        if not self.esta_atracado:
            print("No estás atracado.")
        else:
            self.esta_atracado = False
            print("Desatracando")

Para comprobar nuestro código:

b = Barco()

b.atracar()
b.desatracar()
b.desatracar()
b.atracar()
b.atracar()

Las salidas:

Ya has atracado.
Desatracando.
No estás atracado.
Atracando
Ya has atracado.

(Si miras el vídeo de esta sección, notarás que la clase "Barco" no se ejecuta. El código anterior ya ha sido corregido, cosa que no he hecho con el vídeo. Recuérdalo, no importa lo simple que parezca el código, ni cuán experimentado te consideres, comprueba tu código antes de entregarlo!)

Nuestro programa necesita también de un submarino. Nuestro submarino puede hacer todo lo que el barco, pero además necesitamos un comando sumergirse. Si no usamos la herencia, tenemos dos opciones.

Afortunadamente existe un camino mejor. Nuestro programa puede crear una clase hija que herede todos los atributos y métodos de su clase padre. Entonces, la clase hija podría añadir campos y métodos que se correspondan con sus necesidades. Por ejemplo:

class Submarino(Barco):
    def sumergirse(self):
        print("Sumergirse!")

La línea 1 es la clave. Con solo haber añadido Barco entre paréntesis, durante la declaración de la clase, hemos llamado automáticamente, a todos los atributos y métodos de la clase Barco. Si actualizamos Barco, la clase hija Submarino, se actualizará automáticamente. ¡Así de fácil es la herencia!

El siguiente código está esquematizado en la Figura 12.10.

fig.person1
Figure 12.10: Diagrama de Clase
class Persona():
    def __init__(self):
        self.nombre = ""

class Empleado(Persona):
    def __init__(self):
        # Llamamos primero a la clase consstructor padre
        super().__init__()
        
        # Ahora establecemos las variables
        self.nombre_del_puesto= ""

class Cliente(Persona):
    def __init__(self):
        super().__init__()
        self.email = ""

john_smith = Persona()
john_smith.nombre = "John Smith"

jane_empleado = Empleado()
jane_empleado.nombre = "Empleado Jane"
jane_empleado.nombre_del_puesto = "Desarrollador Web"

bob_cliente = Cliente()
bob_cliente.nombre = "Bob Cliente"
bob_cliente.email = "enviame@spam.com"

Colocando Persona entre los paréntesis de las líneas 5 y 13, el programador le ha dicho al ordenador que Persona es una clase padre para Empleado y Cliente. Esto permite al programa establecer el atributo nombre en las líneas 19 y 22.

Los métodos también se heredan. Cualquier método que posea la clase padre será heredado por la clase hija. ¿Pero qué sucederá si tenemos un método en cada clase, hija y padre?

Existen dos opciones. En la primera podemos ejecutar ambas usando la palabra super(). Usamos super() seguido por el operador punto, así el nombre del método te permite llamar a la versión padre del método.

El código anterior muestra la primera opción, no sólo usamos super para el constructor hijo, sino también para el padre.

Si estás escribiendo un método para el hijo y quieres llamar al método padre, éste será la primera línea en el método hijo. Observa cómo lo hemos hecho en el ejemplo anterior.

Todos los constructores deberían llamar al constructor padre. Es muy triste un hijo sin su padre. De hecho, hay lenguajes que fuerzan a esta regla. Python no.

¿La segunda opción? Los métodos pueden ser sobreescritos por una clase hija para proporcionar funcionalidades diferentes. El siguiente ejemplo muestra ambas opciones. El Empleado.informe sobreescribe a Persona.informe debido a que nunca llama ni ejecuta el método padre. El informe Cliente llama al método padre, y el método informe en Cliente se añade a la funcionalidad de Persona.

class Persona():
    def __init__(self):
        self.nombre = ""

    def informe(self):
        # Informe básico
        print("Informe para", self.nombre)

class Empleado(Persona):
    def __init__(self):
        # Llamamos primero a la clase constructor padre/super 
        super().__init__()

        # Establecemos las variables ahora
        self.nombre_del_puesto = ""

    def informe(self):
        # Aquí solo sobreescribimos informe:
        print("Informe para", self.nombre)

class Cliente(Persona):
    def __init__(self):
        super().__init__()
        self.email = ""

    def informe(self):
        # Ejecutamos el informe padre:
        super().informe()
        # Añadimos ahora nuestro propio código al final, de forma que hacemos los dos
        print("e-mail del Cliente:", self.email)

john_smith = Persona()
john_smith.nombre = "John Smith"

jane_empleado = Empleado()
jane_empleado.nombre = "Empleado Jane"
jane_empleado.nombre_del_puesto = "Desarrollador Web"

bob_cliente = Cliente()
bob_cliente.nombre = "Cliente Bob"
bob_cliente.email = "envia_me@spam.com"

john_smith.informe()
jane_empleado.informe()
bob_cliente.informe()

12.6.1 Es-Una y Tiene-Una Relación

Las clases tienen dos tipos principales de relaciones. Son “es una” y “tiene una” relación.

La clase padre debería siempre ser una versión más general, abstracta de la clase hija. Este tipo de relación hija-padre se denomina relación es una. Por ejemplo, una clase padre Animal puede tener una clase hija Perro. La clase Perro podría tener una clase hija Poodle. Otro ejemplo, un delfín es un mamífero. No funciona en sentido inverso, un mamífero no tiene por qué ser un delfín. Por ello, la clase Delfín nunca sería la clase padre de la clase Mamífero. De la misma forma, la clase Mesa no podría ser padre de la clase Silla, debido a que una silla no es una mesa.

El otro tipo es tiene una. Este tipo relación se implementa mediante atributos de clase. Un perro tiene un nombre, por ello, la clase Perro tiene un atributo para nombre. De la misma forma, una persona podría tener un perro, lo que se implementaría de forma que la clase Persona tuviera un atributo para Perro. La clase Persona no derivaría de Perro. Lógicamente esto puede ser ofensivo.

Observando el ejemplo anterior podemos ver que:

12.7 Variables Estáticas vs. Variables de Instancia

La diferencia entre una variable estática y otra de instancia es confusa. Afortunadamente no necesitamos comprender completamente la diferencia en estos momentos. Pero si continúas programando lo necesitarás. Por lo tanto, de momento, haremos una breve introducción aquí.

También existen algunas particularidades del Python que me tuvieron confuso durante los primeros años en los que estuvo disponible este libro. Por ello, es probable que te encuentres con vídeos y ejemplos antiguos donde yo estaba equivocado.

Una variable de instancia es la clase de variable que hemos estado empleando hasta ahora. Cada instancia de la clase tiene su propio valor. Por ejemplo, en una habitación llena de personas, cada una tiene su propia edad. Algunas de las edades serán las mismas, pero nosotros necesitamos llevar la cuenta individual de cada edad.

Con variables de instancia, no podemos decir simplemente “edad” en una habitación llena de gente. Necesitamos especificar la edad de quién estamos hablando. Además, si no hay gente en la habitación, hablar de edad allí donde no hay personas que la tengan, no tiene mucho sentido.

Con las variables estáticas, el valor es el mismo para cada instancia particular de la clase. Aún en el caso de que no hayan instancias, todavía hay un valor para la variable estática. Por ejemplo, podríamos tener una variable estática contar para el número de clases Humano que hayan en existencia. No tenemos seres humanos? El valor es cero, y aún así, existe la variable.

En el siguiente ejemplo, la ClaseA crea una variable de instancia. La ClaseB crea una variable estática .

# Ejemplo de una variable de instancia
class ClaseA():
    def __init__(self):
        self.y = 3

# Ejemplo de una variable estática
class ClaseB():
    x = 7

# Creamos las instancias de clase
a = ClaseA()
b = ClaseB()

# Dos formas de imprimir la variable estática.
# La segunda es la forma correcta de hacerlo.
print(b.x)
print(ClaseB.x)

# Una forma de imprimir una variable de instancia
# La segunda genera un error, ya que no sabemos a que instancia referirnos.
print(a.y)
print(ClaseA.y)

En el anterior ejemplo, las líneas 16 y 17 imprimen la variable estática. La línea 17 es la forma “correcta” de hacerlo. Al contrario de lo que pasaba antes, podemos referirnos al nombre de la clase, cuando usamos variables estáticas, en lugar de usar una variable que apunte a una instancia particular. Debido a que estamos trabajando con el nombre de la clase, viendo la línea 17, podemos inmediatamente decir que estamos ante una variable estática. La línea 16 podría ser a la vez una variable de instancia o estática, por lo que emplear la línea 17 resulta ser una mejor opción.

La línea 22 imprime la variable de instancia, tal como ya hemos hecho en anteriores ejemplos. La línea 23 generará un error, debido a que cada instancia de y es diferente (después de todo, es una variable de instancia) y nosotros no le estamos diciendo al ordenador de que instancia de la ClaseA estamos hablando.

12.7.1 Variables de Instancia. Ocultando Variables Estáticas

Este es un rasgo de Python que no me gusta. Es posible tener una variable estática, y una variable de instancia con el mismo nombre. Observa el siguiente ejemplo:

# Clase con una variable estática
class ClaseB():
    x = 7

# Creamos una instancia de clase
b = ClaseB()

# Esto imprime 7
print(b.x)

# Esto también imprime 7
print(ClaseB.x)

# Asignamos un nuevo valor a x usando el nombre de clase
ClaseB.x = 8

# Esto imprime 8
print(b.x)

# Esto también imprime 8
print(ClaseB.x)

# Asignamos un nuevo valor a x usando la instancia.
# ¡Pero espera, realmente no está asignando un nuevo valor a x!
# Esto crea una variable x completamente nueva. Esta x es una
# variable de instancia. La variable estática también se llama
# x. Pero son dos variables diferentes. Esto es supercomplicado y 
# y es una muy mala costumbre.

b.x = 9

# Esto imprime 9
print(b.x)

# Esto imprime 8. NO 9!!!
print(ClaseB.x)

Esto de permitir que las variables de instancia oculten a las variables estáticas, me ha confundido durante muchos años!

12.8 Repaso

12.8.1 Test

Haz click para ir al Test.

12.8.2 Ejercicios

Haz click para ir a los Ejercicios.

12.8.3 Taller

Haz click para ir al Taller.


You are not logged in. Log in here and track your progress.