Python y programación orientada a objetos

Nuestra próxima incursión en la programación de videojuegos es una oportunidad fantástica para introducir la programación orientada a objetos en nuestra vida, de una forma sencilla y práctica con Python. Usaremos como ejemplos… galletas y bicicletas 🙂

Bowden Spacelander Bicycle - Museo de Brooklyn (licencia CC 3.0 - http://creativecommons.org/licenses/by/3.0)
Bowden Spacelander Bicycle

(Foto: Museo de Brooklyn – licencia CC 3.0)

La programación orientada a objetos es un paradigma (estilo, marco conceptual, filosofía) de programación que trata de organizar los programas modelando objetos, como si objetos del mundo real se tratasen. Las primeras nociones de objetos en un programa informático se dieron en el Instituto Tecnológico de Massachussets en las décadas de los 1950 y 1960. Por aquel entonces, un objeto era algo tan vago como “un elemento identificable con atributos asociados”. Este paradigma se formalizó en Oslo en el Centro de Cálculo Noruego, en el que intentaban simular movimientos navíos y sus cargas entre diferentes puertos usando Simula I. Como encontraban limitaciones en el lenguaje a la hora de plantear un modelo lo suficientemente completo, crearon una versión del lenguaje llamada Simula 67 que ya introducía muchos de los conceptos de lenguajes modernos orientados a objetos. Con este lenguaje ya podían declarar tipos concretos de barcos con propiedades y comportamientos muy detallados y específicos, creando así simulaciones mucho mejores. Anteriormente a este punto, los conceptos de la programación orientada a objetos eran difusos, no estaban explícitos en los lenguajes y eran más bien una forma de organizar el código que un modelo de programación establecido.

En este artículo vamos a ver los conceptos fundamentales de la programación orientada a objetos, y los aplicaremos a Python.

Conceptos básicos de la programación orientada a objetos

La programación orientada a objetos se basa en el concepto de objeto, como su nombre indica. Un objeto es una construcción de software que:

  1. Pertenece a una clase, o modelo, del cual es una representación concreta o instancia. Es como pensar en la idea general, abstracta, etérea… de bicicleta (clase) y ver tu bicicleta de montaña roja, con ruedas de 26 pulgadas y 24 marchas (objeto, instancia). Una clase sirve para crear objetos a partir de unos datos, de la misma forma que un molde de galletas crea galletas si le das la masa necesaria.
  2. Es totalmente identificable con respecto al resto. Es decir, tiene una identidad a través de la cual el programador (y el resto del programa) puede referirse a él
  3. Contiene datos en forma de variables que, en el contexto de un objeto, suelen llamarse atributos, o campos, que lo describen (en el caso de la bicicleta: color, talla, si es de montaña, de carreras, plegable,…). También contiene código, organizado en funciones que, en programación orientada a objetos, suelen llamarse métodos. Estos métodos ayudan a desempeñar su función o comportamiento

En cuanto a las clases, merecen su propio apartado. Una clase, como hemos dicho ya, es la definición del modelo que siguen todos los objetos que la representan.

  1. Una clase puede heredar propiedades de otra más general, especializándola. Por ejemplo, podemos tener una clase “bicicleta” con tres clases que heredan sus conceptos: bicicleta de montaña, bicicleta de carreras, bicicleta eléctrica. Son tres bicicletas más especializadas: más concretas o más específicas. En algunos lenguajes de programación como C++, una clase puede heredar de varias otras clases (herencia múltiple)
  2. En la definición de la clase es donde programamos los métodos y declaramos los atributos. Existen atributos de clase y atributos de instancia
    • Los atributos de clase se comparten entre todos los objetos de dicha clase. Es decir, todos pueden leer y escribir su valor
    • Los atributos de instancia,  o de objeto, sólo están accesibles en un objeto concreto
  3. Un programa orientado a objetos ejecuta, por lo general (hay excepciones), los métodos de los objetos, no de las clases. Por lo tanto, debemos pasar de una clase a un objeto mediante la creación de instancias. Hay quien llama a eso “instanciación”, pero a mí no me gusta nada

En cuanto a los métodos, conviene decir que se pueden redefinir, o sobreescribir: un determinado método del objeto bicicleta puede re-escribirse en las clases que heredan de bicicleta para, por ejemplo, tener un comportamiento diferente en la bicicleta eléctrica que en la bicicleta de montaña (particularmente en ese ejemplo, pedalear es bastante diferente). También se pueden sobrecargar: dos métodos con el mismo nombre (u “operador”) pero diferentes parámetros puede desempeñar comportamientos diferentes dentro de una misma clase.

Este tipo de discusiones pueden resultar densas, así que hasta aquí los conceptos de este artículo. ¡No son todos los que hay!, así que seguro que volveremos sobre este tema en artículos venideros. Vamos a practicar un poco con Python y ver algunos ejemplos que espero que os resulten aclaratorios.

Python orientado a objetos

Para crear una clase en Python usaremos la palabra clave class , de la siguiente forma:

class Nombre:
    'Documentación de la clase'
    Cuerpo de la clase

Por convenio, los nombres de clase empiezan por mayúscula. Igual que al definir funciones, la primera línea documenta la clase. El cuerpo de la clase son variables y funciones y van con un nivel de sangría, o “con un tabulador”. Vamos a hacer un ejemplo sencillo (y burdo) sobre el que ir comentando cosas.

class Bicicleta:
    'Clase base de bicicleta'

    def __init__(self):
        self.velocidad = 0

    def pedalear(self, rpm):
        self.velocidad = rpm*0.4

    def frenar (self, presion, tiempo):
        if self.velocidad > presion*tiempo/100 > 0:
            self.velocidad = self.velocidad - presion*tiempo/100
        else:
            self.velocidad = 0

En este ejemplo definimos una bicicleta con un sólo atributo: su velocidad. Asociados a ella tenemos dos métidos: pedalear, el cual incrementa la velocidad, y frenar, que la disminuye. Independientemente de lo simplificado y las muchas razones por las que pueden fallar estos dos métodos, fijaos en lo siguiente:

  • Existe un método llamado __init__ . Este método es el encargado de fijar las condiciones iniciales de un objeto, en el momento de su creación: es el constructor.
  • Todos los métodos tienen de primer parámetro uno que se llama self. Podía llamarse de cualquier otra forma e, incluso, podríamos llamarle de forma diferente en cada método; es una referencia a la instancia del objeto. Es decir, cuando la bicicleta esté creada,  self la representará, de tal forma que podremos referirnos a ella y consultar o modificar sus valores, o incluso llamar a sus métodos desde sí misma.
    • Para modificar la velocidad de la bicicleta hay que acceder a ella a través de  self , mediante  self.velocidad .
    • De la misma forma hemos declarado la variable  velocidad en el constructor __init__ .

Por lo demás, nada nuevo. Vamos a ver cómo se crea y se usa un objeto: abre IDLE y, en un mismo fichero, copia la declaración de la clase Bicicleta más estas líneas:

bicicleta1 = Bicicleta();

bicicleta1.pedalear(60)
print("Velocidad", bicicleta1.velocidad)
bicicleta1.frenar(4, 10)
print("Velocidad", bicicleta1.velocidad)
bicicleta1.frenar(100,100)
print("Velocidad", bicicleta1.velocidad)

Ten cuidado de no sangrar estas últimas líneas. Pulsa F5 desde el editor de IDLE. El resultado que deberías observar es el siguiente:

Nuestro primer objeto en acción
Nuestro primer objeto en acción

Para crear la instancia debemos llamar al constructor; no mediante su nombre __init__ , que es interno, sino usando el nombre de la clase: objeto = Nombre_clase(parametro1, parametro2,...). Además, self  se omite: el propio intérprete se encarga de incluirlo siempre. En nuestro ejemplo, el constructor no tiene argumentos, pero pronto le añadiremos alguno. Por otro lado los métodos y atributos del objeto se acceden mediante la construcción objeto.metodo(parametro1, parametro2,...) y objeto.atributo.

Realmente ya estás familiarizado con esta sintaxis, puesto que la hemos usado en muchas ocasiones en los artículos anteriores.

Ya tenemos una bicicleta básica; vamos a especializarla en una subclase, mediante herencia, creando la bicicleta de carreras. Tendrá un color, un número de platos y un número de piñones para poder cambiar de marchas.

Para crear una subclase en Python sólo tenemos que especificar el nombre de la clase de la cual hereda entre paréntesis, en la declaración. Así:

class BiciCarreras(Bicicleta):
    'Clase correspondiente a una bicicleta de velocidad'

Si dejásemos esa declaración así, podríamos crear instancias de la clase BiciCarreras  que, a efectos de datos y comportamiento, sería idéntica a la clase Bicicleta : puedes probarlo si quieres creando una instancia y ejecutando el método pedalear . Vamos a especializarla. Completa la definición de la clase para finalizar con este código que viene a continuación, y escríbelo al final de tu fichero Python de pruebas que has debido comenzar con la clase Bicicleta:

class BiciCarreras(Bicicleta):
    'Clase correspondiente a una bicicleta de velocidad'
    
    def __init__(self, color, platos, pinhones):
        self.color = color
        self.platos = platos
        self.pinhones = pinhones
        self.marchas = platos * pinhones
        self.plato_actual = math.floor(platos/2)
        self.pinhon_actual = math.floor(pinhones/2)

    def pedalear (self, rpm):
        self.velocidad = rpm*(self.pinhon_actual * self.plato_actual)/self.marchas

    def subir_pinhon (self):
        if self.pinhon_actual < self.pinhones:
            self.pinhon_actual = self.pinhon_actual + 1

    def bajar_pinhon (self):
        if self.pinhon_actual > 1:
            self.pinhon_actual = self.pinhon_actual - 1

    def subir_plato (self):
        if self.plato_actual < self.platos:
            self.plato_actual = self.plato_actual + 1

    def bajar_plato (self):
        if self.plato_actual > 1:
            self.plato_actual = self.plato_actual - 1
    
    def __str__ (self):
        return "Bicicleta de carreras de color " + self.color + " y " + str(self.marchas) + " marchas."

Vamos a ir identificando conceptos en el código:

  1. Estamos introduciendo un constructor con parámetros en la función __init__
  2. Estamos sobreescribiendo el método pedalear para diferenciar el comportamiento de esta clase de bicicleta respecto a la anterior, de la cual hereda, para hacerla más específica. Estamos además sobreescribiendo además el método __str__ , que sirve para particularizar el texto que se imprime cuando hacemos un print(instancia_bici_carreras)
  3. Definimos nuevos atributos y métodos propios de una bicicleta con más de una marcha.

En el fichero de Python que has empezado al comienzo de este artículo, importa el módulo math (lo tendrás que hacer en la primera línea), complétalo con esta clase BiciCarreras, y además con las siguientes:

print(bicicleta1)
bicicleta2 = BiciCarreras("rojo", 3, 8)
print(bicicleta2)
bicicleta2.pedalear(60)
print("Velocidad", bicicleta2.velocidad)
bicicleta2.subir_pinhon()
bicicleta2.subir_plato()
bicicleta2.pedalear(60)
print("Velocidad", bicicleta2.velocidad)

Fíjate que la primera línea imprime directamente el objeto instancia de la bicicleta más genérica. La salida que deberías ver ahora es la siguiente:

Salida por consola del ejemplo completo
Salida por consola del ejemplo completo

Localiza lo siguiente:

  • Imprimir la instancia de una clase en la que no sobreescribimos el método __str__  ofrece una información muy técnica (posición en la jerarquía de objetos dentro del intérprete de Python y dirección de memoria donde se ubica). Por otro lado, al sobreescribirlo podemos obtener una descripción más clara si podemos prescindir de la información “estándar”.
  • El comportamiento de la BiciCarreras  es totalmente diferente al pedalear y, como pretendíamos, reacciona a los cambios de marcha (piñón y plato).

Como introducción a la programación orientada a objetos, este contenido es suficiente. A medida que vayamos usando estos conceptos con los sucesivos artículos, sobre todo en los de videojuegos, irán apareciendo nuevos conceptos y técnicas más avanzados que acabaré reuniendo en una continuación de este artículo. Algunos de esos conceptos son los siguientes:

  • Comparación entre objetos.
  • Serialización de objetos: cómo guardar objetos en un fichero para luego recuperarlos.
  • Acceso a métodos de la clase de la cual heredamos (“superclase”).
  • Ocultación de información (encapsulamiento): cómo podemos definir algunos de los miembros de una clase (atributos y métodos) para que no puedan ser accedidos directamente.

Como ejercicio para vosotros, os propongo que reescribáis el ejemplo de la calculadora usando orientación a objetos.

 

2 comentarios en “Python y programación orientada a objetos

Deja un comentario

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