Python – Sumando objetos

Objetivo: Introducción a la sobrecarga de operadores en Python.

Consideremos una operación aritmética primordial, la primera con la que nos atrevimos a coquetear, siendo unos críos, una vez aprendimos a utilizar los números para contar los objetos de nuestro mundo alrededor: la suma.

Todos sabemos sumar. Python, por supuesto, también:

>>> 2 + 3
5

Tal vez nunca te hayas parado a pensar que Python no sólo sabe sumar números:

>>> 'caperucita' + 'roja'
'caperucitaroja'

Pues sí, Python también sabe «sumar» cadenas de caracteres: es lo que se denomina concatenación.

Observa, simplemente como nota, que en este último ejemplo la operación suma no cumple la propiedad conmutativa: no es lo mismo sumar ‘caperucita’ + ‘roja’, que ‘roja’ + ‘caperucita’.

Python también sabe que hay cosas que no puede sumar. Si tratamos de engañarle con código como éste, se queja con razón:

>>> 'caperucita' + 5
Traceback (most recent call last):
  File "<pyshell#46>", line 1, in <module>
    'caperucita' + 5
TypeError: Can't convert 'int' object to str implicitly

En efecto, Python no sabe qué hacer cuando le dices que sume un número a una cadena de caracteres. Si lo que pretendíamos era concatenar ambos era preciso convertir previamente el número a string.

La suma también aparece definida para otros tipo de objetos, como por ejemplo las listas:

>>> [1, 2] + [3, 4]
[1, 2, 3, 4]

Sin embargo, Python no admite la suma de conjuntos:

>>> {1, 2} + {3, 4}
Traceback (most recent call last):
  File "<pyshell#48>", line 1, in <module>
    {1, 2} + {3, 4}
TypeError: unsupported operand type(s) for +: 'set' and 'set'

Donde quiero ir a parar es que te fijes que, para un mismo operador, el ‘+’, Python sabe cómo actuar dependiendo del tipo de objetos, los operandos, que coloquemos a la izquierda y a la derecha del operador.

Así pues, si los operandos son números, efectuará una suma aritmética; si son cadenas de caracteres las concatenará y otro tanto de lo mismo si son listas. Además, Python sabe qué «sumas» no están permitidas.

Se podía haber elegido un operador diferente a ‘+’ para la concatenación de strings, pero no sería muy adecuado, pues intuitivamente tiene pleno significado para nosotros el uso del ‘+’ para una concatenación. Sería mucho más incómodo tener que recordar un operador distinto. Y tanto más cuántos más tipos de objetos diferentes quisiéramos sumar.

Llegamos al quid de la cuestión: un mismo operador, pero muchas maneras de comportarse en función del tipo de operandos que empleemos.

A esta exquisita filosofía de diseño se le denomina sobrecarga de operadores, reflejando el hecho de que un mismo operador (en el ejemplo de hoy, la suma), «carga sobre sus espaldas» código para saber cómo responder ante operandos de distinta naturaleza.

La sobrecarga de operadores, unida a la sobrecarga de métodos, tema que abordaremos en otra ocasión, está relacionada con una de las características más refinadas de la POO y uno de sus pilares: el polimorfismo, ser capaz de comportarse de múltiples formas diferentes presentando una misma interfaz común al programador.

¿Podríamos sobrecargar aún más nuestro ya de por sí sobrecargado operador suma? Planteando la cuestión de otro modo: ¿podríamos implementar una suma de objetos pertenecientes a clases creadas por nosotros?

La respuesta es afirmativa y aprenderemos a hacerlo a continuación.

Comencemos por un ejemplo sencillo. Imaginemos que creamos una clase que representa a vectores libres en el plano caracterizados por sus dos componentes x e y. Sería intuitivamente práctico poder definir una operación suma de dos vectores.

Por ejemplo, si tenemos un vector v1 = (1, 1) y otro v2 = (5, -3), queremos definir la suma de vectores de tal modo que v1 + v2 sea el esperado (6, -2).

Esta sería nuestra eventual clase Vector:

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

No me he complicado mucho y únicamente he incluido el inicializador, que se ocupa de crear las variables miembro x e y referenciando a los valores facilitados durante la creación del objeto.

Para que esta clase sobrecargue el operador ‘+’ y podamos hablar de suma de dos vectores necesitamos incluir en la clase un método especial:__add__. Al igual que __init__, observa que comienza y termina con dos guiones bajos, indicando que es un nombre con un significado especial para Python.

El código que incluyamos en ese método «mágico» especial determinará cómo se comportará la clase ante la operación suma.

Es importante tener muy claro algo antes. La suma objeto1 + objeto2 es equivalente a aplicar el método __add__ sobre objeto1, recibiendo como argumento objeto2. Es decir:

objeto1.__add__(objeto2)

Asegúrate de entender bien esto, pues es todo el misterio que tiene la sobrecarga de operadores: objeto1, el operando de la izquierda del +, debe tener un método __add__ que recibirá como argumento el operando de la derecha, objeto2. Esto se manifestará crucial en el ejemplo que pondremos un poco más adelante.

La clase Vector modificada sobrecargando la suma queda así:

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __add__(self, v):
        sumax = self.x + v.x
        sumay = self.y + v.y
        vectorsuma = Vector(sumax, sumay)
        return vectorsuma

Analicemos con cuidado el código del método __add__:

Aparte de self, presente en todos los métodos como primer parámetro, y que referenciará al objeto actual cuando se cree (el de la izquierda del +), observa que toma otro parámetro, que hemos denominado v, que no es otra cosa sino el vector a la derecha del +. En el código de la función distinguirás como, por un lado, nos referimos a las componentes x e y del objeto propio como self.x y self.y, mientras que las del segundo operando, facilitado como argumento, son v.x y v.y.

El código suma las x con las x y las y con las y:

sumax = self.x + v.x
sumay = self.y + v.y

A continuación, invoca al constructor de la clase Vector para crear un nuevo vector, devuelto como vectorsuma, de componentes con el resultado de estas sumas:

vectorsuma = Vector(sumax, sumay)
return vectorsuma

Veamos, ahora sí, la clase al completo y un ejemplo de su utilización:

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __add__(self, v):
        sumax = self.x + v.x
        sumay = self.y + v.y
        vectorsuma = Vector(sumax, sumay)
        return vectorsuma

>>> v1 = Vector(1, 1)
>>> v2 = Vector (5, -3)
>>> vectorsuma = v1 + v2
>>> vectorsuma.x
6
>>> vectorsuma.y
-2

El ejemplo que figura a continuación ilustra la aplicación de la sobrecarga del operador suma en el caso de objetos de diferentes clases.

Consideremos, por ejemplo, una clase denominada Camiseta que mantiene información sobre el color, talla y tipo de manga de ciertas camisetas deportivas. Por otro lado, la clase Pantalon registra el color y talla de pantalones de deporte.

Podríamos imaginarnos algo así:

camiseta + pantalón = un equipamiento deportivo completo

Es decir, aparecería en escena una tercera clase, Equipo, que tendría como constituyentes un objeto del tipo Camiseta y otro del tipo Pantalon.

Estas son nuestras tres clases:

class Camiseta:
    def __init__(self, color, talla, manga):
        self.color = color
        self.talla = talla
        self.manga = manga

class Pantalon:
       def __init__(self, color, talla):
        self.color = color
        self.talla = talla

class Equipo:
    def __init__(self, camiseta, pantalon):
        self.camiseta = camiseta
        self.pantalon = pantalon

Y ahora la clave. Debemos dotar de sentido a la expresión camiseta + pantalon. Para ello, recuerda lo que dijimos más arriba:

camiseta + pantalon = camiseta.__add__(pantalon)

El objeto de la izquierda del más debe contar con un método especial, __add__ que recibirá como argumento el objeto de la derecha del +.

Por lo tanto, la clase Camiseta, que es la presente a la izquierda del +, precisa definir el método __add__. Veámoslo:

def __add__(self, pantalon):
    return Equipo(self, pantalon)

Simplemente invoca al constructor de la clase Equipo para construir un nuevo objeto con la camiseta (self)y pantalón facilitados.

Veamos las clases al completo junto a un ejemplo de utilización:

class Camiseta:
    def __init__(self, color, talla, manga):
        self.color = color
        self.talla = talla
        self.manga = manga

    def __add__(self, pantalon):
        return Equipo(self, pantalon)

class Pantalon:
       def __init__(self, color, talla):
        self.color = color
        self.talla = talla

class Equipo:
    def __init__(self, camiseta, pantalon):
        self.camiseta = camiseta
        self.pantalon = pantalon

>>> camiseta1 = Camiseta('blanca', 'L', 'corta')
>>> pantalon1 = Pantalon('azul', 'XL')
>>> equipo1 = camiseta1 + pantalon1
>>> equipo1.camiseta.color
'blanca'
>>> equipo1.pantalon.talla
'XL'

Observa la doble notación punto para acceder a los miembros de un objeto que están, a su vez, dentro de otro objeto.

Fíjate muy bien que sólo hemos definido el método __add__ en la clase Camiseta. Por lo tanto, si invirtiéramos el orden de los sumandos, la aplicación no funcionaría. En efecto:

>>> equipo2 = pantalon1 + camiseta1
Traceback (most recent call last):
  File "<pyshell#31>", line 1, in <module>
    equipo2 = pantalon1 + camiseta1
TypeError: unsupported operand type(s) for +: 'Pantalon' and 'Camiseta'

Esto es así porque pantalon1 + camiseta1 es equivalente a:

pantalon1.__add__(camiseta1)

Y la clase Pantalon no tiene definido ningún método __add__.

Si, además de sumar camisetas más pantalones, deseas sumar pantalones más camisetas, necesitas definir también un método __add__en la clase Pantalon (te lo dejo como ejercicio). No presupongas nunca que se cumple la propiedad conmutativa salvo que expresamente la definas.

Existen muchos más operadores que se pueden sobrecargar, pero la mecánica es similar a la aplicada aquí, salvo con otros métodos «mágicos» diferentes. Los veremos en otros artículos. Pero mi consejo es que no esperes y los busques por ti mismo, no te será difícil encontrarlos. Si has entendido bien este artículo no deberá suponerte el mínimo esfuerzo comprender su funcionamiento.

Javier Montero Gabarró


Python – Sumando objetos


El texto de este artículo se encuentra sometido a una licencia Creative Commons del tipo CC-BY-NC-ND (reconocimiento, no comercial, sin obra derivada, 3.0 unported)


El Club del Autodidacta


Consulta el índice completo de artículos relacionados con Python.

Safe Creative #1505044013911

Python – El poderoso inicializador


Objetivo: presentar el inicializador de objetos en Python, invocado automáticamente por el constructor para facilitar las características iniciales del objeto.

Hemos comenzado nuestra andadura en el mundo de la POO en Python presentando los conceptos esenciales de clase y objeto. La clase representa el modelo, la plantilla con la que después crearemos los objetos concretos.

Regresemos a la sencilla clase Coche con la que hemos estado trabajando en las últimas entregas:

class Coche:
    
    marca = ''
    modelo = ''
    color = ''
    numero_de_puertas = 0
    cuenta_kilometros = 0
    velocidad = 0
    arrancado = False

    def arrancar(self):
        if not self.arrancado:
            print('Roarrrr')
            self.arrancado = True
        else:
            # Dale al encendido estando el coche arrancado y escucha...
            print('Kriiiiiiiiiiicccc')
  
    def parar(self):
        self.arrancado = False
  
    def acelerar(self):
        if self.arrancado:
            self.velocidad = self.velocidad + 1

    def frenar(self):
        if self.velocidad > 0:
            self.velocidad = self.velocidad - 1

    def pitar(self):
        print('Bip Bip Bip')

    def consultar_velocimetro(self):
        return self.velocidad

    def consultar_cuenta_kilometros(self):
        return self.cuenta_kilometros

Para fabricar coches a partir de este esquema, invocamos al constructor:

>>> coche1 = Coche()

Como puedes comprobar, el valor de los atributos de datos del objeto recién creado son los que aparecen en la definición de la clase. Por ejemplo:

>>> coche1.marca
''

De modo que, para poder montarte en el coche, lo primero que habría que hacer es darle algo de forma y color:

>>> coche1.marca = "Seat"
>>> coche1.modelo = "Ibiza"
>>> coche1.color = "Blanco"
>>> coche1.numero_de_puertas = 3

Los demás campos podemos dejarlos con sus valores iniciales, no parece muy apropiado que nada más fabricar el coche se nos estampe a toda pastilla contra una pared.

Esta manera de proceder, que habría que repetir con cada coche que fabricáramos, no es, obviamente, ni práctica, ni elegante ni inteligente.

¿Por qué no facilitamos estos valores iniciales en la propia invocación al constructor?

Es decir, algo así como:

coche1 = Coche("Seat", "Ibiza", "Blanco", 3)

Mucho más elegante, menos propenso a errores y, como veremos, permitiéndonos además un mayor control sobre los valores que pueden tomar los atributos.

Para poder comunicarnos de este modo con la clase Coche necesitamos ampliar su definición incorporando un nuevo método: el inicializador.

El inicializador es un método particular cuya función es obvia: ocuparse de los rudimentos iniciales necesarios a la hora de construir el objeto. Toma la forma siguiente, obsérvala con atención:

def __init__(self, argumento1, argumento2, ..., argumentoN):
    código

La palabra init va precedida y seguida por dos símbolos de subrayado (dos delante y dos detrás). Esto, que puede parecer algo esotérico, es una buena convención práctica de Python para evitar posibles conflictos de nombres.

Observa que el primer argumento es self, como en cualquier otro método, que ya sabes que referencia al objeto actual (enseguida lo verás en acción). Los demás argumentos deberán cuadrar con la invocación en el constructor:

coche1 = coche(argumento1, argumento2, ..., argumentoN)

Recuerda que self no se indica dentro del constructor.

Estamos en condiciones de introducir código en el inicializador que, en este caso, es bien simple:

    def __init__(self, marca, modelo, color, puertas):
        self.marca = marca
        self.modelo = modelo
        self.color = color
        self.numero_de_puertas = puertas

En la declaración de parámetros he empleado puertas en lugar de numero_de_puertas simplemente para ilustrar la flexibilidad que tenemos a la hora de elegir nombres de parámetros en las funciones. Naturalmente, en la última línea del código hay que utilizar el nombre real del atributo para que éste se nutra del valor facilitado.

La clase Coche quedaría entonces así:

class Coche:
    
    marca = ''
    modelo = ''
    color = ''
    numero_de_puertas = 0
    cuenta_kilometros = 0
    velocidad = 0
    arrancado = False

    def __init__(self, marca, modelo, color, puertas):
        self.marca = marca
        self.modelo = modelo
        self.color = color
        self.numero_de_puertas = puertas

    def arrancar(self):
        if not self.arrancado:
            print('Roarrrr')
            self.arrancado = True
        else:
            # Dale al encendido estando el coche arrancado y escucha...
            print('Kriiiiiiiiiiicccc')

etcétera, ...

En breve la mejoraremos aún más, pero vayamos asegurando los conceptos paso a paso.

Con esta definición, la siguiente llamada al constructor ya es lícita:

>>> coche1 = Coche("Seat", "Ibiza", "Blanco", 3)

Los IDEs (aquí IDLE) nos facilitan la vida
Los IDEs (en la imagen, IDLE) nos facilitan la vida

Lo que sucede a continuación no debe sorprenderte ahora: los argumentos «Seat», «Ibiza», «Blanco» y 3 son facilitados, respectivamente, a marca, modelo, color y puertas de __init__.

Una vez dentro de __init__, self referencia al objeto actual que se crea al invocar al constructor, es decir, coche1. De modo que interiormente __init__ realiza esto:

coche1.marca = "Seat"
coche1.modelo = "Ibiza"
coche1.color = "Blanco"
coche1.numero_de_puertas = 3

Puedes comprobar que coche1 ha sido inicializado correctamente. Por ejemplo:

>>> coche1.color
'Blanco'
>>> coche1.numero_de_puertas
3

Otro ejemplo de la inteligencia de IDLE
Otro ejemplo de la inteligencia de IDLE

Ahora sí, nuestra clase Coche empieza a tener clase de verdad. Pero aún podemos hacerla más fina…

Sabemos que las variables en Python no necesitan declararse y que se construyen en el momento en que reciben un valor. Este hecho nos permite simplificar nuestra clase. Si los atributos de datos van a ser inicializados dentro de __init__, ¿por qué molestarnos siquiera en dar valores iniciales al definir la clase? Simplifiquemos, entonces:

class Coche:
    
    cuenta_kilometros = 0
    velocidad = 0
    arrancado = False

    def __init__(self, marca, modelo, color, puertas):
        self.marca = marca
        self.modelo = modelo
        self.color = color
        self.numero_de_puertas = puertas

    def arrancar(self):
        if not self.arrancado:
            print('Roarrrr')
            self.arrancado = True
        else:
            # Dale al encendido estando el coche arrancado y escucha...
            print('Kriiiiiiiiiiicccc')

etcétera, ...

Hemos dejado sólo los atributos que expresamente no establecemos en __init__.

Aún podemos ir más allá. ¿Por qué no incluir toda la inicialización en __init__, incluso la de aquellos miembros que no se faciliten explícitamente en la invocación al constructor?

class Coche:
    
    def __init__(self, marca, modelo, color, puertas):
        self.marca = marca
        self.modelo = modelo
        self.color = color
        self.numero_de_puertas = puertas
        self.cuenta_kilometros = 0
        self.velocidad = 0
        self.arrancado = False

    def arrancar(self):
        if not self.arrancado:
            print('Roarrrr')
            self.arrancado = True
        else:
            # Dale al encendido estando el coche arrancado y escucha...
            print('Kriiiiiiiiiiicccc')

etcétera, etcétera...

Los atributos de datos, objetos también en sí (en Python, todo lo es), serán construidos en el momento de la inicialización.

Por lo general, bien sea a la hora de inicializar un objeto, o bien llegado el momento de modificar cualquiera de sus atributos de datos, es buena práctica dejar que sean los métodos quienes se encarguen de ello, en lugar de atacar a las variables miembro directamente. Dentro de un método podemos analizar el valor facilitado, comprobar que sea legítimo, corregirlo o incluso rechazarlo si no procede.

No sería muy sensato escribir código como el siguiente:

coche1.velocidad = 1000

No hay coche que pueda alcanzar 1000 Km/h. Tampoco podría estar parado y, en el instante siguiente, circular a 100 Km/h. Usando los métodos acelerar y frenar que hemos definido en la clase se garantiza un incremento o decremento de la velocidad gradual. Estos métodos, además, podrían asegurarse de que no se excediera en ningún caso un valor máximo por mucho que pisáramos el acelerador (ni mínimo cuando frenásemos, algo que ya hace el método frenar).

Tienes ya las bases suficientes para empezar a utilizar la Programación Orientada a Objetos en tus programas. Te advierto, es una vía casi sin retorno: una vez escribes tu primer programa POO, apetece poco volver a la programación estructurada tradicional.

Pero esto apenas ha empezado, aún queda mucho camino por descubrir…

Javier Montero Gabarró


Python – El poderoso inicializador


El texto de este artículo se encuentra sometido a una licencia Creative Commons del tipo CC-BY-NC-ND (reconocimiento, no comercial, sin obra derivada, 3.0 unported)


El Club del Autodidacta


Consulta el índice completo de artículos relacionados con Python.

Python – Fabricando objetos


Objetivo: presentar el concepto de Objeto o Instancia de una Clase en Python.

En el anterior artículo introdujimos las clases, auténticos pilares de la Programación Orientada a Objetos. Podemos pensar en ellas como diseños de moldes o plantillas para representar en Python (abstraer) una determinada realidad.

Aprendimos, por ejemplo, a modelar un coche:

class Coche:
    
    marca = ''
    modelo = ''
    color = ''
    numero_de_puertas = 0
    cuenta_kilometros = 0
    velocidad = 0
    arrancado = False

    def arrancar(self):
        if not self.arrancado:
            print('Roarrrr')
            self.arrancado = True
        else:
            # Dale al encendido estando el coche arrancado y escucha...
            print('Kriiiiiiiiiiicccc')
  
    def parar(self):
        self.arrancado = False
  
    def acelerar(self):
        if self.arrancado:
            self.velocidad = self.velocidad + 1

    def frenar(self):
        if self.velocidad > 0:
            self.velocidad = self.velocidad - 1

    def pitar(self):
        print('Bip Bip Bip')

    def consultar_velocimetro(self):
        return self.velocidad

    def consultar_cuenta_kilometros(self):
        return self.cuenta_kilometros

En la definición de clase nos encontramos sus características o atributos, que toman forma de variables (marca, modelo, color, etc…) y la funcionalidad propia de la clase o métodos, que nos indican qué cosas podemos hacer con ella, representados mediante funciones (arrancar(), acelerar(), frenar(), etc.).

Pero esto no es más que un esquema, un molde. Lo que queremos ahora es fabricar coches concretos, como un Seat León de cinco puertas o un Ford Fiesta de tres.

Al hecho de «fabricar» objetos a partir de un molde de clase se le denomina instanciar, y al producto obtenido se le conoce como instancias o, simplemente, objetos de esa clase.

De modo que vamos a asegurarnos que tenemos los conceptos claros: las clases son los moldes a los que recurrimos después para crear objetos concretos.

Para poner en marcha nuestra fábrica de coches virtual no necesitaremos una gran inversión en infraestructura y personal; nos bastará con ejecutar el intérprete de Python.

Para mayor comodidad, supongamos que hemos preparado ya todo el código con la definición de la clase Coche y lo hemos incluido en un fichero tal como coche.py.

La primera labor consistirá, por lo tanto, en ejecutar dicho código. En mi caso, lo abro en Idle y lo ejecuto con Run, pero tú puedes elegir cualquier otra forma de hacerlo. La cuestión es tener el intéprete interactivo con toda la definición de la clase cargada ya en memoria.

Comprobemos su existencia:

>>> Coche
<class '__main__.Coche'>

Para fabricar coches concretos necesitamos invocar a lo que se conoce como el constructor de clase, que se ocupará de crear el objeto en memoria. El rito para que haga acto de presencia es muy simple: basta con escribir el nombre de la clase seguido de un par de paréntesis.

Construyamos un primer coche:

>>> coche1 = Coche()

Típicamente los paréntesis los utilizaremos para definir valores iniciales del objeto. Más adelante, cuando presentemos el inicializador, aprenderás a usar esa característica.

Parémonos un instante a contemplar nuestra primera fabricación:

>>> coche1
<__main__.Coche object at 0x0234B7D0>

Lo que nos está diciendo el intérprete es que coche1 es un objeto de tipo Coche y está ubicado en la dirección de memoria 0x0234B7D0.

Cada objeto posee en Python un identicador único que podemos consultar con la función id():

>>> id(coche1)
37009360

En CPython, la implementación más común de Python, este identificador coincide con la dirección de memoria anterior, como podrás comprobar si te tomas la molestia de pasarlo a hexadecimal.

Ya que nos ha ido tan bien, probemos a fabricar un segundo automóvil:

>>> coche2 = Coche()

Aunque de la misma clase, se trata en efecto de un objeto diferente:

>>> id(coche2)
37634800

Para acceder a los atributos y métodos del objeto recurrimos a la notación punto, separando el nombre del objeto del atributo o método mediante un punto.

Establezcamos algunos atributos de coche1:

<code>>>> coche1.marca = "Seat"
>>> coche1.modelo = "León"
>>> coche1.color = "negro"

Y otros tantos de coche2:

coche2.marca = "Ford"
coche2.modelo = "Fiesta"
coche2.numero_de_puertas = 3

Esta forma de proceder, que puede parecer natural, no es la común. Tal como ya hemos apuntado, para dar valores iniciales a los objetos suele emplezarse la figura del inicializador, como veremos más adelante. Lo importante ahora es comprender bien el concepto.

Comprobemos los atributos de coche1:

>>> coche1.marca
'Seat'
>>> coche1.modelo
'León'
>>> coche1.color
'negro'
>>> coche1.numero_de_puertas
0
>>> coche1.cuenta_kilometros
0
>>> coche1.velocidad
0
>>> coche1.arrancado
False

Aprecia como los atributos que no hemos inicializado expresamente toman los valores que tenían en la definición de la clase.

Juguemos ahora un poco con los métodos, funciones que definen lo que podemos hacer con los objetos.

Traigamos a la palestra el primero de ellos. Lo reescribo aquí por comodidad:

    def arrancar(self):
        if not self.arrancado:
            print('Roarrrr')
            self.arrancado = True
        else:
            # Dale al encendido estando el coche arrancado y escucha...
            print('Kriiiiiiiiiiicccc')

El parámetro self, tal como explicamos en el artículo anterior, es obligatorio y debe figurar el primero en la declaración del método. Hace referencia al objeto en cuestión sobre el que se aplicará el método.

Para invocar el método, recurrirmos nuevamente a la notación punto, pasando entre paréntesis los argumentos que requiere el método en particular sin contar a self. Como en arrancar sólo existe self, a efectos prácticos es como si no tuviera ninguno:

>>> coche1.arrancar()
Roarrrr

Aunque no esté presente en la invocación, se está pasando implícitamente como self el nombre del método que llama a arrancar(), en este caso, coche1. Observa el código del método. Lo primero que hace es comprobar que el coche no está arrancado, en cuyo caso imprime «Roarrr», cambiando a continuación el atributo arrancado a True. Verifiquemos que en efecto se ha realizado esto último:

>>> coche1.arrancado
True

Naturalmente, coche2 permanece completamente ajeno a estas operaciones, pues arrancar no ha actuado sobre él, sino sobre coche1. Comprobemos que sigue detenido:

>>> coche2.arrancado
False

Estando coche1 arrancado, si intentamos arrancarlo de nuevo obtendremos un chirriante y desagradable ruido propio de forzar el motor de arranque sin necesidad:

>>> coche1.arrancar()
Kriiiiiiiiiiicccc

Revisemos algunos métodos más de la definición de la clase Coche:

    def parar(self):
        self.arrancado = False
  
    def acelerar(self):
        if self.arrancado:
            self.velocidad = self.velocidad + 1

    def frenar(self):
        if self.velocidad > 0:
            self.velocidad = self.velocidad - 1

Experimentemos con ellos. Cada vez que aceleramos, la velocidad del vehículo se incrementa en una unidad:

>>> coche1.acelerar()
>>> coche1.velocidad
1
>>> coche1.acelerar()
>>> coche1.velocidad
2

Para detener el vehículo, primero lo frenamos completamente:

>>> coche1.frenar()
>>> coche1.frenar() # Al tener velocidad 2 aplicamos dos veces el método
>>> coche1.velocidad
0

Y, a continuación, sacamos la llave de contacto:

>>> coche1.parar()
>>> coche1.arrancado
False

Es muy importante que estudies con cuidado en estos ejemplos cómo hemos accedido desde los métodos a los atributos del objeto a través de self. En la siguiente lección, cuando presentemos el inicializador, le daremos a esta operatividad un carácter esencial.

Javier Montero Gabarró


Python – Fabricando objetos


El texto de este artículo se encuentra sometido a una licencia Creative Commons del tipo CC-BY-NC-ND (reconocimiento, no comercial, sin obra derivada, 3.0 unported)


El Club del Autodidacta


Consulta el índice completo de artículos relacionados con Python.

Python – El concepto de Clase

Objetivo: presentar en Python el concepto de clase, pilar sobre el que está construido todo el paradigma de la programación orientada a objetos.

Ha llegado el momento de que introduzcamos la Programación Orientada a Objetos en Python. Supondremos que esta es la primera vez que te acercas a esta metodología y que desconoces completamente o tienes nociones imprecisas de lo que son las clases y los objetos, de modo que comenzaremos por lo esencialmente básico para que no arrastres lagunas conforme el tema vaya adquiriendo más complejidad.

Por cuestiones de economía me he permitido autoplagiarme empleando los ejemplos, e incluso las mismas palabras, que utilicé en la introducción a la POO que escribí en la serie dedicada a Java. Naturalmente, nuestros caminos divergirán tan pronto como comencemos a picar código concreto en Python.

El tratamiento empleado por Python para la POO difiere de manera importante en algunos aspectos al utilizado en otros lenguajes como Java, C++ o C#. Aunque podríamos habernos acercado a este tema directamente de un modo más pythonista, he preferido que nuestros primeros pasos sean sólidos, apoyados en un marco conceptualmente universal, como el compartido por los lenguajes de programación anteriormente citados.

¿Preparado para una nueva manera de resolver problemas?

Voy a proponerte un sencillo juego. Sal a la calle, mira a tu alrededor y contempla el mundo de un modo particular.

Veo veo… ¿Qué ves?
Una cosita… ¿Y qué cosita es?

Para empezar, quiero que identifiques qué tipos de objetos ves…

Puedes, por ejemplo, encontrarte coches, bares, semáforos o personas, por citar sólo unos cuantos tipos. Date cuenta de que, cuando digo tipos de objetos, hablo de un modo conceptual y general. Si te resulta chocante referirte a una persona como un objeto, prueba a sustituir la expresión por tipos de entidades, en su lugar.

Tipo y clase son dos términos sinónimos, de modo que vamos a cambiar «tipos de objetos» por «clases de objetos», aproximándonos un poco más al concepto que da pie al artículo de hoy.

¿Qué clases de objetos vemos? Coches, bares, semáforos, personas…

La siguiente transformación lingüística consiste en simplificar clases de objetos por simplemente clases:

¿Qué clases vemos? Coches, bares, semáforos, personas…

Y ahora sí, nos sumergimos plenamente en la terminología de la programación orientada a objetos:

Tenemos la clase Coche, la clase Bar, la clase Semáforo, la clase Persona

Poco interesan ahora los detalles concretos. No importa si esa persona es Marta, está en el bar «Casa Manolo», si tiene un Seat Panda o si el semáforo está en rojo. Estamos pensando en términos clasificatorios.

Vamos a adentrarnos ahora en el interior de cada una de estas clases.

¿Cómo son los coches? ¿Qué características pueden diferenciar uno de otro?

Por ejemplo, podríamos indicar la marca, el modelo, el color, la cilindrada, etc.

A las características las vamos a denominar atributos, término muy utilizado en la programación orientada a objetos.

¿Qué atributos tiene la clase Bar?

Todo bar tiene un nombre, una ubicación, un estado (si está abierto o cerrado), una lista de precios,…

Como atributos de la clase Semáforo podríamos indicar su ubicación y estado (rojo, verde o ámbar).

La clase Persona podría definir como atributos el nombre, sexo, edad, estado civil, altura, etc.

A continuación pensemos en el comportamiento de estas clases. Preguntémonos qué cosas hacen, qué tipo de acciones pueden realizar.

Un coche puede arrancar, detenerse, girar a la izquierda, acelerar, frenar, encender sus luces. Un semáforo puede cambiar de estado. Un bar puede abrir, cerrar, servirte una cerveza, cobrarla, modificar la lista de precios. Una persona puede hablar, dormir, conducir un coche, tomarse una cerveza en un bar.

En terminología de la programación orientada a objetos, a estas funciones que determinan el comportamiento de una clase se las conoce como métodos.

Empleando un lenguaje puramente pythonista, tanto a los atributos como a los métodos aquí descritos se les conoce como simplemente atributos. Para diferenciar unos de otros, Python emplea el término atributos de datos para referirse a los datos característicos de la clase. No obstante, al menos mientras continuemos presentando los conceptos básicos, seguiremos distinguiendo aquí entre atributos y métodos.

Sabemos qué es una clase y que está compuesta de atributos y métodos. Nuestra labor ahora consistirá en modelar en Python estas clases.

Modelar no es otra cosa sino crear una abstracción que represente de algún modo una determinada realidad.

Para crear en Python la clase Coche comenzamos usando la palabra reservada class, seguida del nombre de la clase y del símbolo de dos puntos:

class Coche:
    instrucción 1
    instrucción 2
    ...
    instrucción n

La clase más simple de todas no haría nada:

class Coche:
    pass

La sentencia pass pasa completamente, no hace absolutamente nada. Es útil en contextos en los que se requiere la presencia de al menos una instrucción, hasta que la inspiración acuda y coloquemos código sustituto.

Típicamente, indentados en la definición, introduciremos los atributos y métodos de que consta la clase Coche.

El nombre Coche lo escribimos con la primera letra en mayúsculas, pues es práctica común entre la POO que los nombres de clases empiecen así.

Introduzcamos algunos atributos:

class Coche:

    marca = ''
    modelo = ''
    color = ''
    numero_de_puertas = 0
    cuenta_kilometros = 0
    velocidad = 0
    arrancado = False

Hemos utilizado, provisionalmente, valores iniciales para los atributos: la cadena vacía para los tipo string, cero para los numéricos y False para el booleano, pero podrían haber sido otros cualesquiera. En el momento en que comencemos a fabricar objetos de esta clase todos estos atributos podrán recibir su valor concreto y particular.

Definamos ahora los métodos de la clase Coche:

class Coche:
    
    marca = ''
    modelo = ''
    color = ''
    numero_de_puertas = 0
    cuenta_kilometros = 0
    velocidad = 0
    arrancado = False

    def arrancar(self):
        pass
  
    def parar(self):
        pass
  
    def acelerar(self):
        pass

    def frenar(self):
        pass

    def pitar(self):
        pass

    def consultar_velocimetro(self):
        pass

    def consultar_cuenta_kilometros(self):
        pass

Observa que los métodos se implementan exactamente igual que las funciones. Como tales, junto a la palabra clave def aparece el nombre del método seguido de unos paréntesis con la declaración de parámetros formales, finalizando con dos puntos e indentando el cuerpo del método a continuación. Al igual que las funciones ordinarias, los métodos podrán o no devolver un valor.

Fíjate en self, un parámetro que debe figurar siempre al comienzo de todo método. Su significado quedará completamente claro en el siguiente artículo, cuando expliquemos el concepto de Objeto. Por el momento, como primera toma de contacto, que te vaya sonando que self hace referencia al objeto concreto sobre el que se está invocando el método.

La elección de self como nombre del parámetro es arbitraria, podría servir cualquier otro término. Aquí nos sumaremos a la denominación favorita entre la comunidad pythonista.

Hemos rellenado el cuerpo de los métodos con la instrucción nula pass, pero seamos algo atrevidos y escribamos algo de código:

class Coche:
    
    marca = ''
    modelo = ''
    color = ''
    numero_de_puertas = 0
    cuenta_kilometros = 0
    velocidad = 0
    arrancado = False

    def arrancar(self):
        if not self.arrancado:
            print('Roarrrr')
            self.arrancado = True
        else:
            # Dale al encendido estando el coche arrancado y escucha...
            print('Kriiiiiiiiiiicccc')
  
    def parar(self):
        self.arrancado = False
  
    def acelerar(self):
        if self.arrancado:
            self.velocidad = self.velocidad + 1

    def frenar(self):
        if self.velocidad > 0:
            self.velocidad = self.velocidad - 1

    def pitar(self):
        print('Bip Bip Bip')

    def consultar_velocimetro(self):
        return self.velocidad

    def consultar_cuenta_kilometros(self):
        return self.cuenta_kilometros

Intenta comprender el código aunque aún no hayamos explicado la notación punto ni el uso de self. No te preocupes, en absoluto, si te surgen dudas, pues todo quedará claro en el siguiente artículo. Lo importante ahora es que te quedes con los conceptos de clase, atributo y método.

La siguiente podría ser una primera aproximación simple al modelado de una persona:

class Persona:
    
    sexo = ''
    nombre = ''
    edad = 0
    coche = Coche()   # El coche que conduce esa persona
  
    def saludar(self):
        print('Hola, me llamo', self.nombre)

    def dormir(self):
        print('Zzzzzzzzzzz')

    def obtener_edad(self):
        return self.edad

Fíjate en que uno de los atributos es precisamente de la clase Coche que acabamos de definir:

coche = Coche() # El coche que conduce esa persona

No confundas el nombre del atributo (coche, en minúsculas), con el nombre de la clase (Coche, con la primera en mayúsculas). Recuerda que para Python son identificadores distintos. Nuevamente, no te preocupes si no comprendes la sentencia, que hace mención a la construcción explícita de un objeto de tipo Coche. El propósito es simplemente ilustrar que entre los atributos de una clase, además de tipos comunes (números, strings, listas, etc…), pueden figurar objetos de cualquier clase, incluso de las creadas por nosotros mismos.

Te propongo como ejercicio que te entretengas modelando las otras clases indicadas al comienzo, u otras de tu elección, para asentar estos conceptos primarios.

Las entidades no tienen por qué ser tangibles, necesariamente. El siguiente código modela nuestras emociones:

class Emocion:
    emocion = ''
  
    def obtener_emocion(self):
        return self.emocion
    
    def cambiar_emocion(self, nueva_emocion):
        self.emocion = nueva_emocion

Observa el método cambiar_emocion(), que además de self define un nuevo parámetro.

En el próximo artículo explicaremos el concepto de Objeto, gracias al cual materializaremos nuestras clases en entidades concretas, coches rugientes, personas de carne y hueso o sentidas emociones. Hasta entonces, bienvenido al apasionante mundo de la programación orientada a objetos.

Javier Montero Gabarró


Python – El concepto de Clase


El texto de este artículo se encuentra sometido a una licencia Creative Commons del tipo CC-BY-NC-ND (reconocimiento, no comercial, sin obra derivada, 3.0 unported)


El Club del Autodidacta


Consulta el índice completo de artículos relacionados con Python.

Java: El concepto de objeto

lasecreObjetivo: comprender el concepto de Objeto, diferenciándolo del de Clase y presentar la notación punto.

Una vez hemos entendido el concepto de Clase en el paradigma de la Programación Orientada a Objetos, es momento de matizar que es un Objeto en su sentido más práctico.

Una Clase, como hemos visto, no es más que una especificación que define las características y el comportamiento de un determinado tipo de objetos. Piensa en ella como si se tratara de una plantilla, molde o esquema a partir del cual podremos construir objetos concretos.

Consideremos la clase Coche que definimos en el artículo anterior:

class Coche
{
  String marca;
  String modelo;
  String color;
  int numeroDePuertas;
  int cuentaKilometros;
  int velocidad;
  boolean arrancado;
  
  void arrancar()
  {
    arrancado = true;
  }
  
  void parar()
  {
    arrancado = false;
  }
  
  void acelerar()
  {
    velocidad = velocidad + 1;
  }
  
  void frenar()
  {
    velocidad = velocidad - 1;
  }
  
  void pitar()
  {
    System.out.println("Piiiiiiiiiiiiiiiiii");
  }

  int consultarCuentaKilometros()
  {
    return cuentaKilometros;
  }
}

Con este esquema en nuestras manos, vamos a crear coches concretos de la marca, modelo y color que nos apetezca. Cada uno que creemos será un objeto, o instancia, de la clase Coche. Fíjate, en esta terminología, en la equivalencia de los términos objeto e instancia para referirnos, ahora sí, a entidades concretas de una determinada clase.

Fabriquemos, entonces, nuestro primer coche…

Cada objeto, como todas las variables en Java, ha de ser declarado antes de ser utilizado:

Coche coche1;

Con esta instrucción declaramos la variable coche1 de tipo Coche. En cuanto creemos, con el comando que escribiremos a continuación, el objeto concreto, coche1 contendrá una referencia a ese objeto, es decir, almacenará la dirección de memoria en la que realmente se halla el objeto propiamente dicho. Esto es muy importante: coche1 no contendrá el objeto en sí, sino una dirección de memoria que apunta a él.

Materialicemos nuestro primer coche del siguiente modo:

coche1 = new Coche();

La palabra reservada new se emplea para crear nuevos objetos, instancias de una determinada clase que indicamos a continuación seguida de un par de paréntesis.

Veremos esto a su debido momento, pero por ahora debe bastarte retener que se está invocando a un método especial que tienen todas las clases que sirve para construir el objeto en cuestión facilitándole sus valores iniciales. A este método se le conoce como constructor de la clase.

Podríamos habernos ocupado de la declaración y la creación en una sola instrucción:

Coche coche1 = new Coche();

Otro trasto más contaminando nuestras ciudades. Hagamos, al menos, que no consuma mucho:

coche1.marca = "Seat";
coche1.modelo = "Panda";

Consulta el cuadro de arriba con la definición de la clase y observa que tanto marca como modelo son dos atributos de tipo String, por lo que referencian cadenas de caracteres que escribimos entre comillas.

Observa con cuidado la notación punto. Separamos el nombre de la variable que referencia al objeto del atributo empleando un punto como separador.

Pintemos de azul nuestro flamante vehículo:

coche1.color = "Azul";

Además tiene tres puertas, el cuenta kilómetros indica 250.000 Km (bueno, algo viejo parece que es) y su velocímetro refleja 0 Km/h. Valores correspondientes a los atributos numeroDePuertas, cuentaKilometros y velocidad, respectivamente, todos de tipo entero.

coche1.numeroDePuertas = 3;
coche1.cuentaKilometros = 250000;
coche1.velocidad = 0;

Su motor está detenido, hecho que representamos a través de la variable booleana arrancado:

coche1.arrancado = false;

Cuando nos cansemos de enredar con los atributos podemos jugar con los métodos. Arranquemos el Panda:

coche1.arrancar();

De nuevo, empleamos también la notación punto para separar la variable del método.

Si observas el código verás que este método se limita a hacer que la variable booleana arrancado valga ahora true. Podrías decir que hubiéramos logrado el mismo resultado actuando sobre el atributo directamente, en lugar de invocar al método:

coche1.arrancado = true;

En efecto, es así; pero, como comprenderás más adelante, no suele ser buena idea dejar que los programas campen a sus anchas y modifiquen arbitrariamente los atributos de un objeto, siendo preferible que sean los métodos los que se ocupen de esa labor. Imagina que el programa intenta hacer que el cuenta kilómetros marque una cantidad negativa, o que el número de puertas sea igual a cincuenta. Un método correctamente diseñado podría gestionar que los valores estuvieran dentro del rango adecuado y asegurarse de que se cumplen las condiciones que permitirían la modificación del atributo.

La Programación Orientada a Objetos implementa un mecanismo, denominado encapsulación, que nos permite ocultar determinadas facetas de nuestro objeto y dejar sólo accesibles aquellas partes que nos interesen. Pero vayamos por orden, todo a su momento…

Sigamos jugando con nuestro viejo cacharro azul:

coche1.acelerar();

Lo que provocará, si consultas el código, que el velocímetro incremente en una unidad su valor.

coche1.pitar();

Lo que ocasionará un estridente pitido.

Puedes crear todos los objetos de la clase Coche que desees:

Coche coche2 = new Coche();

Cada uno con su propia colección de atributos:

coche2.marca = "Ford";
coche2.modelo = "Fiesta";
coche2.color = "Negro";

Incluso podrías crear otros Seat Pandas, por supuesto. Aunque, en un instante dado, compartieran los mismos valores en sus atributos, se trataría de objetos distintos, ubicados en direcciones de memoria diferentes y cada uno podría seguir su propia trayectoria vital.

En el próximo artículo aprenderemos a crear un programa que nos permita probar nuestra clase Coche y en el que veremos en acción las líneas de código que hoy hemos esbozado.

Javier Montero Gabarró


Java: El concepto de objeto


El texto de este artículo se encuentra sometido a una licencia Creative Commons del tipo CC-BY-NC-ND (reconocimiento, no comercial, sin obra derivada, 3.0 unported)


El Club del Autodidacta


Índice completo de artículos sobre Java.

Uso de cookies

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información.plugin cookies

ACEPTAR
Aviso de cookies