Programar Juegos Arcade
con Python y PygameChapter 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?
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
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).
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.
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
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:
- Los atributos deben ir primero, los métodos después.
- El primer parámetro, en cualquier método, debe ser self.
- Las definiciones de métodos deben ir indentadas exactamente una tabulación.
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.
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.
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
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.
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.
También podemos ejecutar este código en www.pythontutor.com para observar cómo ambas variables apuntan al mismo objeto.
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.
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.
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
- Crea una clase llamada Gato. Otórgale atributos tales como nombre, color, y peso. Dale un método llamado miau.
- Crea una instancia de la clase gato, completa sus atributos y llama al método miau.
- 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
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
- ¿Cómo debería comenzar el nombre de una clase; por mayúsculas o minúsculas?
- ¿Cómo debería comenzar el nombre de un método; por mayúsculas o minúsculas?
- ¿Cómo debería comenzar el nombre de un atributo; por mayúsculas o minúsculas?
- ¿Qué es lo que deberíamos listar primero en un clase; atributos o métodos?
- ¿De qué otra forma podemos llamar a una referencia?
- ¿De qué otra forma podemos llamar a una variable de instancia?
- ¿Cuál es el nombre para la instancia de una clase?
- ¿Crea una clase llamada Estrella que imprima “Ha nacido una estrella!” cada vez que es creada.
- 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
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.
- Una, añadir el comando sumergirse() a nuestro barco. No es una gran idea. No deberíamos dar la impresión de que nuestro barco se sumerge normalmente.
- Dos, podríamos crear una copia de la clase Barco y llamarla Submarino. En esta nueva clase añadiríamos el comando sumergirse(). Al principio sería fácil, pero las cosas podrían complicarse si cambiáramos la clase Barco. El programador debería recordar que no solo debemos cambiar la clase Barco, sino también hacer los mismos cambios en la clase Submarino. Mantener sincronizado este código es laborioso y está sujeto a errores.
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.
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:
- Empleado es una persona.
- Cliente es una persona.
- Persona tiene un nombre.
- Empleado tiene un puesto.
- Cliente tiene un e-mail.
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
12.8.2 Ejercicios
Haz click para ir a los Ejercicios.
12.8.3 Taller
You are not logged in. Log in here and track your progress.
English version by Paul Vincent Craven
Spanish version by Antonio Rodríguez Verdugo
Russian version by Vladimir Slav
Turkish version by Güray Yildirim
Portuguese version by Armando Marques Sobrinho and Tati Carvalho
Dutch version by Frank Waegeman
Hungarian version by Nagy Attila
Finnish version by Jouko Järvenpää
French version by Franco Rossi
Korean version by Kim Zeung-Il
Chinese version by Kai Lin