PROGRAMACIÓN ORIENTADA A OBJETOS EN PYTHON

Zuria Bauer Hartwig ( CAChemE Lightning Talk)

21-11-2014 - Licenciado bajo Creative Commons (CC-BY)
Bibliografía en la que me he basado:

Antes de nada, ¿esto sirve para algo?:


In [1]:
"""
General Numerical Solver for the 1D Time-Dependent Schrodinger's equation.

adapted from code at http://matplotlib.sourceforge.net/examples/animation/double_pendulum_animated.py

Double pendulum formula translated from the C code at
http://www.physics.usyd.edu.au/~wheat/dpend_html/solve_dpend.c

author: Jake Vanderplas
email: vanderplas@astro.washington.edu
website: http://jakevdp.github.com
license: BSD
Please feel free to use and modify this, but keep the above information. Thanks!
"""

from numpy import sin, cos
import numpy as np
import matplotlib.pyplot as plt
import scipy.integrate as integrate
import matplotlib.animation as animation

class DoublePendulum:
    """Double Pendulum Class

    init_state is [theta1, omega1, theta2, omega2] in degrees,
    where theta1, omega1 is the angular position and velocity of the first
    pendulum arm, and theta2, omega2 is that of the second pendulum arm
    """
    def __init__(self,
                 init_state = [120, 0, -20, 0],
                 L1=1.0,  # length of pendulum 1 in m
                 L2=1.0,  # length of pendulum 2 in m
                 M1=1.0,  # mass of pendulum 1 in kg
                 M2=1.0,  # mass of pendulum 2 in kg
                 G=9.8,  # acceleration due to gravity, in m/s^2
                 origin=(0, 0)): 
        self.init_state = np.asarray(init_state, dtype='float')
        self.params = (L1, L2, M1, M2, G)
        self.origin = origin
        self.time_elapsed = 0

        self.state = self.init_state * np.pi / 180.
    
    def position(self):
        """compute the current x,y positions of the pendulum arms"""
        (L1, L2, M1, M2, G) = self.params

        x = np.cumsum([self.origin[0],
                       L1 * sin(self.state[0]),
                       L2 * sin(self.state[2])])
        y = np.cumsum([self.origin[1],
                       -L1 * cos(self.state[0]),
                       -L2 * cos(self.state[2])])
        return (x, y)

    def energy(self):
        """compute the energy of the current state"""
        (L1, L2, M1, M2, G) = self.params

        x = np.cumsum([L1 * sin(self.state[0]),
                       L2 * sin(self.state[2])])
        y = np.cumsum([-L1 * cos(self.state[0]),
                       -L2 * cos(self.state[2])])
        vx = np.cumsum([L1 * self.state[1] * cos(self.state[0]),
                        L2 * self.state[3] * cos(self.state[2])])
        vy = np.cumsum([L1 * self.state[1] * sin(self.state[0]),
                        L2 * self.state[3] * sin(self.state[2])])

        U = G * (M1 * y[0] + M2 * y[1])
        K = 0.5 * (M1 * np.dot(vx, vx) + M2 * np.dot(vy, vy))

        return U + K

    def dstate_dt(self, state, t):
        """compute the derivative of the given state"""
        (M1, M2, L1, L2, G) = self.params

        dydx = np.zeros_like(state)
        dydx[0] = state[1]
        dydx[2] = state[3]

        cos_delta = cos(state[2] - state[0])
        sin_delta = sin(state[2] - state[0])

        den1 = (M1 + M2) * L1 - M2 * L1 * cos_delta * cos_delta
        dydx[1] = (M2 * L1 * state[1] * state[1] * sin_delta * cos_delta
                   + M2 * G * sin(state[2]) * cos_delta
                   + M2 * L2 * state[3] * state[3] * sin_delta
                   - (M1 + M2) * G * sin(state[0])) / den1

        den2 = (L2 / L1) * den1
        dydx[3] = (-M2 * L2 * state[3] * state[3] * sin_delta * cos_delta
                   + (M1 + M2) * G * sin(state[0]) * cos_delta
                   - (M1 + M2) * L1 * state[1] * state[1] * sin_delta
                   - (M1 + M2) * G * sin(state[2])) / den2
        
        return dydx

    def step(self, dt):
        """execute one time step of length dt and update state"""
        self.state = integrate.odeint(self.dstate_dt, self.state, [0, dt])[1]
        self.time_elapsed += dt

#------------------------------------------------------------
# set up initial state and global variables
pendulum = DoublePendulum([180., 0.0, -20., 0.0])
dt = 1./30 # 30 fps

#------------------------------------------------------------
# set up figure and animation
fig = plt.figure()
ax = fig.add_subplot(111, aspect='equal', autoscale_on=False,
                     xlim=(-2, 2), ylim=(-2, 2))
ax.grid()

line, = ax.plot([], [], 'o-', lw=2)
time_text = ax.text(0.02, 0.95, '', transform=ax.transAxes)
energy_text = ax.text(0.02, 0.90, '', transform=ax.transAxes)

def init():
    """initialize animation"""
    line.set_data([], [])
    time_text.set_text('')
    energy_text.set_text('')
    return line, time_text, energy_text

def animate(i):
    """perform animation step"""
    global pendulum, dt
    pendulum.step(dt)
    
    line.set_data(*pendulum.position())
    time_text.set_text('time = %.1f' % pendulum.time_elapsed)
    energy_text.set_text('energy = %.3f J' % pendulum.energy())
    return line, time_text, energy_text

# choose the interval based on dt and the time to animate one step
from time import time
t0 = time()
animate(0)
t1 = time()
interval = 1000 * dt - (t1 - t0)

ani = animation.FuncAnimation(fig, animate, frames=300,
                              interval=interval, blit=True, init_func=init)


plt.show()

Ahora sí, comencemos :)

Paradigmas de programación...

Uno de los elementos básicos a la hora de realizar un programa es la modelización del problema que se quiere resolver. Los objetivos generales son la localización de nuestras variables y los aspectos importantes, así como saber llevar a cabo los pasos necesarios para obtener el resultados a partir de los datos iniciales, etc. Es decir, reducimos los detalles de forma que trabajemos con cada vez menos elementos.

Los paradigmas de programación son herramientas conceptuales para analizar, representar y abordar los problemas, presentando sistematizaciones complementarias para pasar del espacio de los problemas al de las implementaciones de una solución.

Algunos paradigmas habituales que conocemos los Ingenierios Químicos

Hay numerosas formas de resolver los problemas, los más habituales en la Ingeniería Química son:

  • La programación funcional: Se puede expresar un programa como una secuencia de aplicación de funciones. Es la forma clásica de trabajar con MATLAB.

  • La progrmación orientada a objetos: Los programas se definen en términos de clase de objetos que se comunican entre sí mediante el envío de mensajes. Ejemplos de Lenguajes: Java, Python o C++. MATLAB también permite trabajar así, aunque todas biblitecas son funciones (no objetos).

Programación multiparadigma

Los paradigmas son idealizaciones, y, como tales, no siempre se presentan de forma totalmente pura, ni siempre resultan incompatibles entre sí. Cuando se mezclan distintos paradigmas se produce lo que se conoce como programación multiparadigma.

El uso de distintos modelos resulta más natural y permite expresar de forma más clara y concisa nuestras ideas.

Python se adapta muy bien a la programación multiparadigma y dan gran libertad a la hora de resolver un problema.

Conceptos básicos para empezar a programar con Objetos

En Python todo es un objeto y debe ser tratado como tal. Pero... ¿Qué es un objeto? ¿De qué hablamos cuando nos referimos a "orientación a objetos"?

Pensar en Objetos

¿Qué es un objeto?? es simple. Dando una definición fuera del mundo de la informática:

"Un objeto es una cosa. Y, si una cosa es un sustantivo, entonces un objeto es un sustantivo".

Sencillo, ¿cierto?

¿Qué me decís si describimos las cualidades de un objeto? Describir un objeto es simplemente mencionar sus cualidades. Las cualidades son Adjetivos.

Para describir "la manera de ser" de un objeto, debemos preguntarnos ¿cómo es el objeto? Toda respuesta que comience por "el objeto es", seguida de un adjetivo, será una cualidad del objeto.

Los objetos, tambíen tienen la capacidad de hacer cosas Hemos descrito las cualidad de nuestros objetos, pero no hemos hablado de aquellas cosas que los objetos "pueden hacer", es decir, "cuáles son sus cualidades".

Los objetos tienen la capacidad de realizar acciones. Las acciones, son verbos. Para conocer las capacidades de un objeto debes preguntarte ¿Qué puede hacer el objeto? y la respuesta estará dada por aquellas que comiencen por la frase "el objeto puede", seguido de un verbo.

La parte difícil...

...es que en programación todo lo que acabamos de ver, se denomina de una forma particular.

Cuando en el documento... En la programación se denomina...
Hablamos de una cosa (generalmente un sustantivo) Objeto
Hablamos de atributos o cualidades (generalmente un adjetivo) Propiedades
Hablamos de acciones que puede realizar el objeto (funciones) Método
Hablamos de atributos-objeto Composición
Vemos que los objetos relacionados entre sí, tienen nombres de atributos iguales (por ejemplo: color y tamaño) y sin embargo, pueden tener valores diferentes Polimorfismo
Hablamos de objetos que son sub-tipos (o ampliación) de otros Herencia

Teniendo claro esto, vamos a asegurarnos que el código que viene a continuación es compatible con Python 2 y 3. Si es tu primera vez que usas Python (y estás usando la versión 3.x) puedes ignorar el siguiente código.


In [2]:
from __future__ import division, print_function, unicode_literals

Programación Orientada a Objetos

Como hemos dicho, la Programación Orientada a Objetos (POO) es un paradigma de programación. Como tal, nos enseña un método que se basa en las interacciones de objetos para poder resolver nuestro sistema.

Elementos y Características de la POO

Los elementos podemos definirlos como los materiales que necesitamos para diseñar y programar un sistema. Las características podrían asumirse como las herramientas de las cuáles disponemos para construir el sistema con esos materiales.

Elementos principales:

CLASES

Las clases son los modelos sobre los cuáles se construirán nuestros objetos.

En Python una clases se define con la instrucción class seguida de un nombre genérico para el objeto:


In [3]:
class ReactorRFP:
    pass

NOTA: El nombre de las clases se define en singular y se escriben en Mayúsculas. La instrucción pass indica que una vez creada la clase no se haga "nada".

PROPIEDADES

Las propiedades son las características del objeto. Éstas se representan a modo de atributos y vienen a ser parámetros del objeto. Podemos asignar estos valores a la clase creada de la siguiente forma:


In [4]:
ReactorRFP.longitud = 12. # [m]

Y consultar la información con la función print.


In [5]:
print(ReactorRFP.longitud)


12.0

Otra forma añadir esta informacióna la clase es en el momento de su creación:


In [6]:
class ReactorRFP:
    longitud = 12. # [m]
    diametro_tubo = 2e-2 # [m]
    temperatura_refrigeracion = 15. # [ºC]

MÉTODOS

Los métodos son funciones dentro de una clase y pueden representar acciones propias que puede realizar el objeto (y otro no):


In [7]:
class ReactorRFP:
    """Clase de Reactor de flujo pistón"""
    
    longitud = 12 # [L]
    diametro_tubo = 2e-2 # [m]
    temperatura_refrigeracion = 15 # [ºC]
    
    def ec_dif_BM (self, dndL, L):
        dndL = 0 #de momento no compliquemos el código :)
        return dndL

NOTA: El primer parámetro de un método SIEMPRE debe ser self. Explicaremos en el Vol.2 de las Lightning Talks de CAChemE su significado y utilidad.

OBJETO

Las clases por sí mismas, no son más que modelos que nos sirven para crear objetos en concreto. A la acción de crear objetos se le llama instanciar una clase, consiste en asignar la clase como valor a una variable. Ahora bien, en Python "todo son objetos" por lo que puedo crear otro clase con el componente de agua


In [8]:
class H2O:
    MW = 18 # [kg/kmol] Peso molecular
    densidad = 1000 # [kg/m^3] densidad

class ReactorRFP:
    """Clase de Reactor de flujo pistón"""
    
    longitud = 12 # [L]
    diametro_tubo = 2e-2 # [m]
    temperatura_refrigeracion = 15 # [ºC]
    
    # Código nuevo empieza aquí
    reactivo_A = H2O()
    
    def ec_dif_BM (self, dndL, L):
        dndL = 0 # de momento no compliquemos el código :)
        return dndL
HERENCIA: Característica Principal

Como se ha comentado anteriormente, algunos objetos comparten las mismas propiedades y métodos que otro objeto, y además agregan nuevas propiedades y métodos. A esto se le denomina herencia: una clase que hereda de otra.

NOTA: Cuando una clase no hereda de ninguna otra, debe hacerse heredar de object, que es la clase principal de Python, que define un objeto. Volvemos a crear las clases de forma "más" correcta:


In [9]:
class H2O(object):
    MW = 18 # [kg/kmol] Peso molecular
    densidad = 1000 # [kg/m^3] densidad

class ReactorRFP(object):
    """Clase de Reactor de flujo pistón"""
    
    longitud = 12 # [L]
    diametro_tubo = 2e-2 # [m]
    temperatura_refrigeracion = 15 # [ºC]
    
    # Código nuevo empieza aquí
    reactivo_A = H2O()
    
    def ec_dif_BM (self, dndL, L):
        dndL = 0 # de momento no compliquemos el código :)
        return dndL

Ahora, veamos un ejemplo de herencia:


In [10]:
class RFP_multitubo(ReactorRFP):
    n_tubos = 5 # nº de tubos
    diametro_tubo = 1e-3 # [m]

Acabamos de crear una nueva clase heredando la información del ReactorRFP general. Veamos que información tiene nuestro nueva clase.


In [11]:
print(RFP_multitubo.diametro_tubo)


0.001

In [12]:
print(ReactorRFP.diametro_tubo)


0.02

In [13]:
print(RFP_multitubo.n_tubos)


5

In [14]:
print(ReactorRFP.n_tubos)


---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-14-fd384f6efef1> in <module>()
----> 1 print(ReactorRFP.n_tubos)

AttributeError: type object 'ReactorRFP' has no attribute 'n_tubos'
Accediendo a los métodos y propiedades de un objeto

A pesar de haberlo utilizado aquí arriba, veamos como podemos obtener información de nuestro programa. Una vez hemos creado un objeto, es posible acceder a su método y propiedades. Para ello, Python utiliza una sintaxis muy simple: el nombre del objeto, seguido de punto y la propiedad o método al cuál se desea acceder:

objeto = MiClase() print (objeto.propiedad) objeto.otra_propiedad = "Nuevo valor" variable = objeto.metodo() print (variable) print (objeto.otro_metodo())

NOTA: Es un esquema de como acceder a un método y sus propiedades, ya que ninguno de estos se encuentra creado, si lo ejecutamos obtenemos error.

Ejemplo aplicado a la Ingeniería Química

In [ ]:
class RFP(object):
    caudal = 400
    longitud = 12
    diametro = 2
    temperatura_refrigeracion = 15
    alfa = [[-1, -1, 1],[-1, -1, 0]]
    energia_activacion = 125300
    entalpia_referencia = [-11000, -12000]
    r = 8.314
    
    def __init__(self, temperatura):
        self.temperatura = temperatura
    
    def Cp_A(self):
        return 10 + 0.1 * self.temperatura + 0.01 * self.temperatura ** 2.0
 
et = RFP(32)
print ('La longitud del reactor es de (m):', et.longitud)
print ('El diámetro del reactor es de (m):', et.diametro)
print ('La temperatura de refrigeración es de (ºC):', et.temperatura_refrigeracion)
print ('El valor de alfa es de:', et.alfa)
print ('La capacidad calorífica da:', et.Cp_A())

In [ ]:
class NuevoRFP(RFP):
    longitud = 10

et = NuevoRFP(32)
print ('La longitud del reactor es de (m):', et.longitud)
print ('El diámetro del reactor es de (m):', et.diametro)
print ('La temperatura de refrigeración es de (ºC):', et.temperatura_refrigeracion)
print ('El valor de alfa es de:', et.alfa)
print ('La capacidad calorífica da:', et.Cp_A())

In [ ]: