09 - Curso de Python - Clases en Python

De ChuWiki

Cualquier duda suelo atender en este foro de python

Todo el código de este curso de Python gratuito está en github https://github.com/chuidiang/chuidiang-ejemplos/tree/master/PYTHON/curso-python. Puedes usar línea comandos python para ir siguiendo los ejemplos.

Anterior: 08 - Curso de Python - Modulos -- Índice: Curso de Python -- Siguiente: 10 - Curso de Python - Datetime

¿Qué es una clase en python?[editar]

Una clase es una estructura de Python donde agrupamos variables y funciones que están muy relacionadas entre ellas. Por ejemplo, podemos hacer una clase Telefono que tenga datos del telefono (variables) como marca, modelo, precio etc. Y que tenga también funciones asociadas a un telefono y que usen esos datos. Por ejemplo, una función dame_precio() a la que pasemos como parámetro un descuento y nos dé el precio del teléfono aplicando ese descuento.

En una clase, las funciones también se llaman métodos. De esta forma se distingue cuándo una función está suelta, es decir, no pertenece a ninguna clase. O cuando pertenece a una clase que entonces se llama método. Las variables de la clsae también pueded recibir el nombre de propiedades o atributos, para distinguirlos de variables sueltas que no vayan asociadas a ninguna clase.

¿Cómo se define una clase en Python?[editar]

La forma de definir una clase en Python es con la palabra servada class, el nombre de la clase, dos puntos : y luego, en líneas consecutivas con sangrado respecto a la palabra class, las variables y definición de funciones. Vamos a hacer una clase Persona. La que vamos a hacer ahora no está bien e indicaremos más adelante por qué. Pero para simplificar y que se entienda, nos vale.

class Telefono:
    marca='Nokia'
    modelo='XR20'
    precio=500

    def dame_precio(descuento):
        return 500 - 500*descuento/100

Hemos definido una clase correspondiente a un teléfono Nokia, con algunos datos y la función de obtener precio con descuento. Si guardamos esto en un fichero Telefono.py, podemos usarlo de la siguiente manera

>>> from Telefono import Telefono
>>> Telefono.marca
'Nokia'
>>> Telefono.modelo
'XR20'
>>> Telefono.precio
500
>>> Telefono.dame_precio(10)
450.0

¿Qué son las instancias de un clase?[editar]

Dada una clase que representa un tipo de dato, podemos querer tener varias instancias de esa clase para obtener tipos de datos concretos. Es decir, si tenemos una clase Teléfono, podemos querer tener teléfonos concretos (instancias). Varios modelos de teléfonos Nokia, de iPhone, de Samsumg. No tiene sentido definir una clase para cada uno de ellos y repetir las variables y funciones una y otra vez. Lo ideal es tener una única clase y luego que se puedan crear a partir de ellas instancias concretas con datos concretos. Esto se consigue de la siguiente manera

>>> nokia_XR20 = Telefono()
>>> nokia_110 = Telefono()
>>> iPhone_XR = Telefono()

Las instancias se consiguen llamando a una función, llamada constructor, que toda clase proporciona. La función es el nombre de la clase poniendo () detrás. Ahora tenemos tres instancias (copias) de la clase Telefono metidas en tres variables distinta. Vamos a ver una posibilidad y vemos que está mal. Esto nos dará paso al siguiente apartado.

>>> nokia_XR20 = Telefono()
>>> iPhone_XR = Telefono()
>>> nokia_XR20.marca
'Nokia'
>>> iPhone_XR.marca
'Nokia'
>>> Telefono.marca='iPhone'
>>> nokia_XR20.marca
'iPhone'
>>> iPhone_XR.marca
'iPhone'

Hemos creado dos instancias y hemos sacado por pantalla la marca. Como esperábamos, ambas son Nokia. Cambiamos Telefono.marca por iPhone, sacamos los valores marca de las dos instancias y fíjate que han cambiado ambas. Veamos por qué

Variables de clase y de instancia[editar]

Las variables que hemos declarado de esta manera se llaman variables de clase. Son compartidas por todas las instancias. Este es el motivo por el que al principio dijimos que la forma de declarar la clase no era correcta. No vamos a poner tener instancias de teléfono de distintas marcas, modelos y precios. Vamos a poder tener muchas instancias de la misma marca, modelo y precio. Para que la instancia tenga sus propios valores, en vez de variables de clase, debemos declarar variables específicas para la instancia. Veamos como hacerlo.

Comentamos que las clases tienen una función constructor que se puede llamar con el nombre de la clase entre paréntesis. Esta llamada construye una instancia concreta. Por defecto, la función constructor no hace nada, pero podemos definir nosotros la función constructor para que haga lo que queramos. Vamos a hacerlo con Telefono para que cree las variables de instancia marca, modelo y precio. El código sería el siguiente

class Telefono:
    def __init__(self, marca, modelo, precio):
        self.marca=marca
        self.model=modelo
        self.precio=precio

    def dame_precio(descuento):
        return 500 - 500*descuento/100

Veamos los detalles. Hemos quitado las variables marca, modelo y telefono. No nos interesan tener esas variables compartidas ente todas las instancias. Hemos puesto una función __init__() con parámetros. Estsa función con este nombre es el constructor. Si no la ponemos, el constructor de la clase no hace nada. Si la ponemos, el constructor de la clase hace lo que diga esta función. Esta función siempre lleva un primer parámetro self. Ese parámetro representa la instancia que se está creando. Así en el constructor podemos hacer cosas la instancia concreta que se está creando.

El resto de parámetros es lo que nosotros queramos. Puede no haber más, puede haber uno, o puede haber muchos. En este ejemplo hemos puesto marca, modelo y precio. Y dentro del constructor aprovechamos para meter en la instancia los valores de marca, modelo y precio. Al poner self.marca = ... estamos creando en la instancia una variable marca al que estamos asignando un valor. Esa variable marca es de la instancia y no de la clase. Cada instancia tendrá su variable con su propio valor.

Así que podemos probar este código de la siguiente forma

>>> from Telefono import Telefono
>>> nokia_XR20 = Telefono('Nokia','XR20',500)
>>> iPhone_XR = Telefono('iPhone','XR', 300)
>>> nokia_XR20.marca
'Nokia'
>>> iPhone_XR.marca
'iPhone'

Fíjate que ahora la llamada al constructor admite parámetros, en concreto, la marca, el modelo y el precio. No tenemos que poner el parámetro self, de ese se encarga Python internamente. Tras crear el nokia y el iPhone, sacamos por pantalla la marca de cada uno y vemos que, efectivamente, cada uno tiene la suya.

Ambito de las variables[editar]

Aquí hay algo importante. Vamos con un ejemplo un tanto extraño, pero que lo puede dejar claro

class una_clase:
    variable = 1
    def __init__(self):
        self.variable=2
        variable=3

Hemos definido una clase con una variable de clase a la que asingamos valor 1, una variable de instancia con el mismo nombre a la que asignamos valor 2 y dentro del constructor, a una variable del mismo nombre le damos valor 3. Vamos ahora a ver qué ha pasado

>>> from ambito_variables import una_clase
>>> instancia=una_clase()
>>> una_clase.variable
1
>>> instancia.variable
2

Importamos la clase y creamos una instancia. La variable de clase una_clase.variable tiene el valor 1 que le asignamos. La variable de instancia tiene el valor 2 que le asignamos instancia.variable. Pero la variable de valor 3 no ha modificado su valor. ¿Por qué?. Por que la variable con self es de la instancia y la variable sin self es propia de la función constructor, desaparece en cuanto el constructor termina de ejecutarse.

Es importante tener esto claro, si no, puedes tener efectos inesperado asignando valores a sitios que no esperas. En cualquier caso, lo evitas si no llamas a todas las variables igual. Resumiendo, las variables de clase son compartidas por todas las instancias y debería accederse a ellas con nombre_clase.variable. Las variables de instancia en el constructor se usan con self. delante y desde fuera de la clase se accede con nombre_instancia.variable.

Y tienes que tener cuidado con el siguiente caso, fruto de una equivocación. Hacemos una clase muy tonta, con sola una variable de clase.

class otra_clase:
    variable = 1

Y vamos a jugar con ella

>>> from ambito_variables import otra_clase
>>> instancia1 = otra_clase()
>>> instancia2 = otra_clase()
>>> instancia1.variable=2
>>> otra_clase.variable
1
>>> instancia1.variable
2
>>> instancia2.variable
1

Importamos la clase, creamos dos instancias y en una de ellas cambiamos el valor de variable. Los resultados pueden sorprender. La variable de clase otra_clase.variable no ha cambiado y la de la instancia2 instancia2.variable tampoco, sólo ha cambiado para instancia1. ¿Por qué?. Por que al hacer instancia1.variable=2 python NO está modificando la variable de clase, está creando al vuelo una variable de instancia para instancia1 y solo para instancia1. Cuando imprimimos instancia1.variable sale el valor de la variable de instancia. Cuando escribmos instancia2.variable, como instancia2 no tiene una variable de instancia de ese nombre, saca la variable de clase. Probamos, tras este código a cambiar el valor de la variable de clase a ver qué pasa

>>> otra_clase.variable=3
>>> otra_clase.variable
3
>>> instancia1.variable
2
>>> instancia2.variable
3

Pues cambia el valor de la variable de clase y de la instancia2. Pero el de instancia1 sigue inalterado. Lógico, instancia2 tiene su propia variable de instancia que aunque se llama igual, no tiene nada que ver con la de clase.

Esta creación sobre la marcha de variables de instancia tiene que hacer que tengas un cuidado especial. Si tienes una serie de variables de instancia creadas, por ejemplo, marca, y al usar el código en un momento te equivocas y pones nokia.mraca = 'Nokia' (fíjate que he escrito mal a posta marca), estás creando una nueva variable mraca. Este lapsus al teclear puede darte bastantes dolores de cabeza luego cuando el programa no funcione.

Herencia y herencia múltiple[editar]

Imagina que tienes una clase Perona que tiene cosas como nombre, apellidos, fecha de nacimiento, dirección, email, teléfono, etc. Y tienes la clase Empleado que, como es una persona, tienen las mismas cosas que Persona pero además tiene cosas como empresa en la que trabaja, sueldo, email de la empresa, teléfono de la empresa, etc. En este tipo de casos en que una clase Empleado es otra clase Persona y evitar repetir las variables, tenemos la herencia. Veamos un ejemplo sencillo. Clase persona y empleado

class persona:
    def __init__ (self, nombre, email):
        self.nombre=nombre
        self.email=email

class empleado(persona):
    def __init__(self, nombre, email, email_empresa):
        persona.__init__(self, nombre, email)
        self.email_empresa=email_empresa

Nada nuevo en la clase persona. En la clase empleado hempos puesto:

  • Al definir la clase entre parénteis "persona". Esto indica que la clase empleado debe tener todo lo que tenga persona y lo que definamos aquí adicionalmente.
  • El constructor __init__(). Aparte de self, ponemos todos los parámetros de persona más el propio de empleado.
  • En el constructor de empleado llamamos al constructor de persona.__init__() pasando los parámetros que le tocan, es decir, self, nombre y email.
  • Y añadimos adicionalmente el email_empresa.

Vamos a usar esto para ver cómo funciona.

>>> from empleado import empleado
>>> juan = empleado('juan','juan@personal.mail','juan@empresa.mail')
>>> juan.nombre
'juan'
>>> juan.email
'juan@personal.mail'
>>> juan.email_empresa
'juan@empresa.mail'

Creamos la instancia juan pasando los parámetros correspondientes y los vamos sacando por pantalla para ver que están correctamente asignados. empleado tiene nombre y email heredados de persona

Hay veces que una clase puede heredar de varias. Por ejemplo, imagina las clases corredor, nadador y ciclista. En cada una de ellas están las marcas que hace un determinado deportista. Imagina ahora que un deportista es un triatleta que hace los tres deportes. El triatleta hereda los datos de los tres deportes, es decir, las marcas que hace en cada uno de ellos. En un fichero triatleta.py podemos meter todas estas clases

class corredor:
    def __init__(self,marca_corredor):
        self.marca_corredor=marca_corredor

class nadador:
    def __init__(self,marca_nadador):
        self.marca_nadador=marca_nadador

class ciclista:
    def __init__(self,marca_ciclista):
        self.marca_ciclista=marca_ciclista

class triatleta(corredor,nadador,ciclista):
    def __init__(self,marca_corredor,marca_nadador,marca_ciclista):
        corredor.__init__(self,marca_corredor)
        nadador.__init__(self,marca_nadador)
        ciclista.__init__(self,marca_ciclista)

Vemos que para heredar de varias clases sólo tenemos que poner entre paréntesis todas las clases de las que heredamos. Y en el constructor ir llamando a los constructores que queramos de las clases padre. Vamos a usar este código

>>> from triatleta import triatleta
>>> pedro = triatleta(10,20,30)
>>> pedro.marca_corredor
10
>>> pedro.marca_ciclista
30
>>> pedro.marca_nadador
20
>>> quit()

MRO u orden de búsqueda en la herencia[editar]

Si echas un poco de imaginación, puedes liar esto como quieras. Imagina que corredor, ciclista y nadador heredan a su vez de la clase persona que hicimos antes. Cosa lógica, todo deportista es una persona. Y líalo más, imagina que tienen métodos dame_marca() repetidos aunque cada uno da su marca concreta. Cuando llamas a pedro.nombre o pedro.dame_marca() ... ¿a la de qué clase padre llama?. Pues bien, python tiene su propia regla para buscar y decidir qué ejecuta según hayas declarado la herencia. Se llama MRO (Method resolution order). Básicamente busca la función en la clase hija triatleta. Si no lo enuentra busca en la primera clase padre que hemos puesto entre paréntesis. Si no la encuentra ahí, busca en las clases padre de esta y así sucesivamante. Si no la encuentra, entonces va a la segunda clase que hayamos puesto en triatleta y así sucesivamente.

Como puedes imaginar, esto es un lío. Así que en programación no conviene liar mucho las herencias. Hay lenguajes de programación, como Java, que no permiten herencia múltiple. Y las buenas constumbres de programación aconsejan no abusar de la herencia, preferir siempre otros mecanismos.

Clases abstractas, interfaces y polimorfismo[editar]

En python las variables tiene valores dentro, de cualquier tipo. La misma variable puede contener ahora una valor de un tipo, luego un valor de otro tipo. Cuando la variable tiene instancias de clases, contamos con un método adecuado para saber si es de una clase o no. Por ejemplo, recuperemos el ejemplo de un empleado que es una persona. Si tenemos una variable pedro que sabemos que tiene una instancia de un objeto, podemos interrogar si realmente lo es antes de liarnos a llamar a sus métodos. Por ejemplo, si quiero el nombre de pedro, puedo hacer esto

>>> from empleado import empleado
>>> from empleado import persona
>>> pedro = empleado('pedro','pedro@gmail.com','pedro@empresa.com')

>>> # Verificamos que es de tipo persona antes de pedir el nombre
>>> isinstance(pedro,persona)
True
>>> pedro.nombre
'pedro'

La función isinstance() nos permite saber si una variable contiene una instancia de una clase concreta. Aquí quizás no ves ninguna ventaja, puesto que el código está todo seguido y sabes que emplreado hereda de persona. Pero imagina un código más complejo, varios programadores, que pedro te llega como parámetro de una función y alguien ha creado la instancia pedro en otro sitio y resulta que también tienes perros que no son personas y han hecho la gracia de llamar pedro a un perro. No está de más, salvo en casos muy evidentes, el verificar que una variable tiene una instancia de la clase que esperamos antes de liarnos a llamar a sus variables y funciones.

Imagina ahora que tiens una clase poligono y de ella vas a heredar clases triangulo, cuadrado, pentagono, etc. Y quieres que todas tengan una función que te calcule el área del polígono. En las clases triángulo, cuadrado, pentagono tienes más o menos clara la fórmula para calcular el área y puedes poner el método. Pero en polígono, la clase padre, igual no lo tienes tan claro. Sería un poco inmatenibel que en tu código hagas tres isinstance() para ver si es un triángulo, un cuadrado o un pentágono y así poder llamar la méodo dame_area(). Aparte de engorroso de escribir, ¿que pasa si en el futuro haces la clase trapecio que herea de poligono?. Te toca revisar todo el código para añadir una condición más al if.

Lo ideal sería que la clase poligono tuviera el método dame_area() sin rellenar. En la condición sólo miraríamos si isinstance() de polígono y entonces podríamos llamar al método. Y aquí viene la magia que se llama polimorfismo. Se ejecutará el método dame_area() de la instancia concreta que tengamos, es decir, triangulo, cuadrado, pentagono o trapecio. No el de la clase poligono.

¿Como definimos en poligono el método dame_area() sin ponerle código?. Fácil, python tiene la sentencia pass que no hace nada. Así que podemos hacer

class poligono:
   def dame_area(self):
      pass

y si la usamos

>>> from poligono import poligono
>>> poligono.dame_area()
>>>

Vemos que la llamada no hace nada, pero no falla. Hagamos solo como ejemplo el cuadrado

class cuadrado(poligono):
    def __init__ (self, lado):
        self.lado = lado

    def dame_area(self):
        return self.lado*self.lado

y lo usamos

>>> from poligono import poligono
>>> poligono.dame_area()
>>>

Bien, en programación orientada a objetos (programación con clases), cuando hacemos una clase que tienes métodos que no hacen nada y que esperamos que las clases hijas hagan ese algo, se conocen como clases abstractas, es decir, clases a las que les falta algo para estar completas. En python este concepto queda difuminado, ya que el método dame_area() de poligono sí está implemetado, pero está implementado cun una sentencia que no hace nada <oce>pass.

Si en una clase dejamos todos los métodos sin implementar, es decir, todos con pass, en otros lenguajes de programación se conoce como interface. Es una lísta de métodos que las clases hijas tiene que tener e implementar.

Tanto el concepto de poner métodos sin implementar en clases padre abstractas o interfaces nos facilitan la vida a la hora de verificar si una instancia tiene ese método o no antes de llamarlo. Nos basta con ver si isinstance() de la clase padre/interface que tenga el método y entonces podemos llamarlo con ciertas garantías de que no tengamos un error por llamar a algo que no existe.

Anterior: 08 - Curso de Python - Modulos -- Índice: Curso de Python -- Siguiente: 10 - Curso de Python - Datetime