La orientación a objetos se describe por 3 características:
La noción fundamental de orientación a objetos es que los datos, métodos y funciones están mejor organizados en clases.
Un objeto se define como aquello que tiene atributos y métodos. Los atributos son las características de los objetos, mientras que los métodos son operaciones o actividades, definidas como funciones, que puede realizar el objeto.
a = 1
help(a)
Se observa que el resultado de help(a) indica que a es un objeto. Además de esto, escribiendo dir(a) se despliegan los atributos y métodos de a, demostrando que cumple las condiciones para ser un objeto.
dir(a)
En un nuevo jupyter notebook:
Es mejor usar las funciones integradas de Python sobre los métodos, ya que estos últimos realizan controles de seguridad adicionales que demoran más la operación y obtienen el mismo resultado.
Las funciones de módulos como math también son objetos. Utilicemos la función sin() para verificar esto
import math
dir(math.sin)
# Un método en particular es __doc__, que es donde Python almacena
# la documentación relacionada con la función.
math.sin.__doc__
Las clases son colecciones lógicas de atributos que describen un tipo de objeto y definen cómo crear un objeto en particular.
Un físico decide usar clases para abstraer detalles de implementación de objetos en una simulación de física de partículas
Un físico debe decidir qué clases crear. Estas clases deben asegurar que los datos y las funciones relacionadas a diferentes objetos, estén separadas (encapsuladas) de las demás.
Ya que es un caso de física de partículas, lo más lógico es crear una clase Particle.
class Particle(object):
""" La partícula es la unidad constituyente del Universo. """
# Acá va a definición del cuerpo de la clase
¡Creamos nuestra primera clase!
Ahora, para hacerla una clase bien formada se pueden incluir atributos como:
Son datos aplicables a todos los objetos de la clase. Ya que todas las partículas solo tienen en común que son partículas...
class Particle(object):
""" La partícula es la unidad constituyente del Universo. """
# Acá va a definición del cuerpo de la clase
roar = "Soy una partícula!" # Variable de clase
# No es necesario acceder al atributo roar por medio de un objeto
# ya que es propio de la clase y no de los objeto en la clase.
print(Particle.roar)
# Aunque también se puede acceder por medio de un objeto.
# Creamos el objeto higgs e imprimimos el atributo roar por medio de este.
higgs = Particle()
print(higgs.roar)
Por tanto, las variables de clase son excelentes para datos y métodos que son universales en la clase. Para los atributos únicos para cada objeto, existen las variables de instancia.
Debido a que existen características únicas para cada partícula, que no se comparten con las demás partículas de la clase, se deben crear las variables de instancia que son aquellas variables propias de cada instancia de la clase.
Existen varias caracterísiticas como posición, masa, carga y espín. Por ahora crearemos 2 partículas con sus respectivas posiciones.
# Primero se crea una lista vacía para las partículas que se observan
obs = []
# Luego se agrega la primera partícula a la lista
obs.append(Particle())
# Y seguido, se le asigna una posición (r)
obs[0].r = {'x':1 , 'y':2, 'z':3}
# Y se imprime la posición de esta partícula
print(obs[0].r)
En la siguiente celda, añada una nueva partícula a la lista, asígnele una posición e imprimala.
# Partícula #2
Una clase se puede interpretar como el plano que describe cómo es un objeto de la clase, por tanto podemos entender que a partir de la clase podemos fabricar objetos. A ese objeto construido se le denomina instancia, y al proceso de construir un objeto se le llama instanciación.
Cuando se instancia un objeto, un constructor es ejecutado de manera inmediata y automática, para dar un valor inicial a los atributos del nuevo objeto. Es un método que se crea por defecto bajo el nombre de init (), aunque también puede ser creado por el usuario.
Aunque esta función se ejecuta automáticamente cuando el objeto es creado, es una buena práctica de programación hacer que esta función sea la responsable de la inicialización de todas las variables de instancia de un objeto.
class Particle(object):
""" La partícula es la unidad constituyente del Universo.
Atributos:
----------
c : carga en unidades de [e]
m : masa en unidades de [kg]
r : posición en unidades de [m]
"""
roar = "Soy una partícula!"
def __init__(self):
""" Inicializa la partícula con valores por defecto
para carga c, masa m y posición r.
"""
self.c = 0
self.m = 0
self.r = {'x': 0, 'y': 0, 'z': 0}
Ya que init () es una función de una clase, se denomina método. Es necesario que todo método tenga al menos una entrada y que el primer argumento sea la instancia a la que va a estar atado. En este caso, el método tiene una entrada self que se refiere a la instancia. La palabra self es una convención, se puede cambiar por cualquier otra dentro de la definición del método.
Para poder añadir los valores reales de los atributos de un objeto, hay que crear el objeto y luego agregar 1 por 1 los valores de cada atributo. Esto para cada objeto... Por tanto es necesario modificar el método init () para hacerlo más eficiente al momento de agregar datos de algún objeto.
class Particle(object):
""" La partícula es la unidad constituyente del Universo.
Atributos:
----------
c : carga en unidades de [e]
m : masa en unidades de [kg]
r : posición en unidades de [m]
"""
roar = "Soy una partícula!"
def __init__(self, carga, masa, posicion): # La no-tilde fue intencional
""" Inicializa la partícula con valores por defecto
para carga c, masa m y posición r.
"""
self.c = carga
self.m = masa
self.r = posicion
Ahora, se pueden asignar los valores correspondientes a cada atributo cuando se llame el método!! YEIIIII!!!
# Se importa el módulo 'constants' de scipy
from scipy import constants as cts
m_p = cts.m_p # Masa del protón
r_p = {'x':1, 'y':2,'z':0}
# Se crea el objeto 'proton' y se le asignan los valores de carga, masa y posición
proton = Particle(1, m_p, r_p)
# Se verifica que los valores hayan quedado asignados a los atributos correspondientes
print 'La carga del protón es', proton.c
print 'La masa del protón es', proton.m
print 'La posición del protón es', proton.r
Los métodos se distinguen de las funciones por el hecho de que están ligadas a una definición de clase. El método init () es súper importante ya que es constructor. Ahora, añadamos otro método que nos permita ver los valores de los atributos de un objeto sin tener que imprimirlos explícitamente.
class Particle(object):
""" La partícula es la unidad constituyente del Universo.
Atributos:
----------
c : carga en unidades de [e]
m : masa en unidades de [kg]
r : posición en unidades de [m]
"""
roar = "Soy una partícula!"
def __init__(self, carga, masa, posicion): # La no-tilde fue intencional
""" Inicializa la partícula, cuyos valores para carga c,
masa m y posición r son entradas del método
"""
self.c = carga
self.m = masa
self.r = posicion
def holis(self):
""" Va a ser la presentación de la partícula, exhibiendo todos
sus atributos característicos
"""
myroar = self.roar + (
" Mi carga es " + str(self.c) + "e."
" Mi masa es " + str(self.m) + "kg."
" Mi posición en x es " + str(self.r['x']) + "m."
" Mi posición en y es " + str(self.r['y']) + "m."
" Mi posición en z es " + str(self.r['z']) + "m." )
print(myroar)
Ahora, se pueden imprimir todos los atributos de la partícula con una sola línea!
# Se importa el módulo 'constants' de scipy
from scipy import constants as cts
m_p = cts.m_p # Masa del protón
r_p = {'x':1, 'y':2,'z':0}
# Se crea el objeto 'proton' y se le asignan los valores de carga, masa y posición
proton = Particle(1, m_p, r_p)
# Se verifica que los valores hayan quedado asignados a los atributos correspondientes
proton.holis()
Los métodos también puede modificar variables de instancia. En física de partículas, los quarks tienen una característica llamada sabor, el cual puede tomar los valores: up, down, top, bottom, strange y charm. La interacción débil puede cambiar el sabor de un quark de up a down, de top a bottom o de strange a charm.
En la siguiente celda cree una clase llamada Quark, el método init () para añadir el sabor y un método flip() que cambie el sabor (variable de instancia) de los quarks.
# Se crea la clase Quark
# RESPUESTAAAAAAA!
class Quark(object):
""" Los quarks cambian de sabor
Atributos:
----------
s: sabor
"""
def __init__(self, sabor):
""" Inicializa el quark con el sabor como
una entrada del método
"""
self.s = sabor
def flip(self):
if self.s == "up":
self.s = "down"
elif self.s == "down":
self.s = "up"
elif self.s == "top":
self.s = "bottom"
elif self.s == "bottom":
self.s = "top"
elif self.s == "strange":
self.s = "charm"
elif self.s == "charm":
self.s = "strange"
else :
print("The quark cannot be flipped, because the flavor is not valid.")
quark1 = Quark('charm')
print quark1.s
quark1.flip()
print quark1.s
Los métodos son poderosos porque tienen acceso a los atributos de un objeto. Por esto, los físicos los han utilizado ampliamente.
Un físico modificó la clase Particle para agregar un método que calcule el menor valor posible de incertidumbre en la posición de una partícula por medio del Principio de incertidumbre de Heisenberg
from scipy import constants as cts
class Particle(object):
""" La partícula es la unidad constituyente del Universo.
Atributos:
----------
c : carga en unidades de [e]
m : masa en unidades de [kg]
r : posición en unidades de [m]
"""
roar = "Soy una partícula!"
def __init__(self, carga, masa, posicion): # La no-tilde fue intencional
""" Inicializa la partícula, cuyos valores para carga c,
masa m y posición r son entradas del método
"""
self.c = carga
self.m = masa
self.r = posicion
def holis(self):
""" Va a ser la presentación de la partícula, exhibiendo todos
sus atributos característicos
"""
myroar = self.roar + (
" Mi carga es " + str(self.c) + "e."
" Mi masa es " + str(self.m) + "kg."
" Mi posición en x es " + str(self.r['x']) + "m."
" Mi posición en y es " + str(self.r['y']) + "m."
" Mi posición en z es " + str(self.r['z']) + "m." )
print(myroar)
def delta_x_min(self, delta_p_x):
hbar = cts.hbar
delta_xmin = hbar / (2.0 * delta_p_x)
print 'La menor incertidumbre en la posición es',delta_xmin
car = 1
mas = 2
pos = {'x':1, 'y':2, 'z':0}
hey = Particle(car,mas,pos)
hey.delta_x_min(1e-20)
Se pueden crear métodos que estén asociados con la clase pero que no cambien de valor para cada objeto, estos son los métodos estáticos.
Siguiendo el ejemplo, se puede crear un método estático de los posibles sabores de los quarks:
from scipy import constants as cts
class Particle(object):
""" La partícula es la unidad constituyente del Universo.
Atributos:
----------
c : carga en unidades de [e]
m : masa en unidades de [kg]
r : posición en unidades de [m]
"""
roar = "Soy una partícula!"
def __init__(self, carga, masa, posicion): # La no-tilde fue intencional
""" Inicializa la partícula, cuyos valores para carga c,
masa m y posición r son entradas del método
"""
self.c = carga
self.m = masa
self.r = posicion
def holis(self):
""" Va a ser la presentación de la partícula, exhibiendo todos
sus atributos característicos
"""
myroar = self.roar + (
" Mi carga es " + str(self.c) + "e."
" Mi masa es " + str(self.m) + "kg."
" Mi posición en x es " + str(self.r['x']) + "m."
" Mi posición en y es " + str(self.r['y']) + "m."
" Mi posición en z es " + str(self.r['z']) + "m." )
print(myroar)
def delta_x_min(self, delta_p_x):
hbar = cts.hbar
delta_xmin = hbar / (2.0 * delta_p_x)
print 'La menor incertidumbre en la posición es',delta_xmin
@staticmethod # Decorador que permite que se cree un método estático
def posibles_sabores():
return ["up", "down", "top", "bottom", "strange", "charm"]
car = 1
mas = 2
pos = {'x':1, 'y':2, 'z':0}
hey = Particle(car,mas,pos)
hey.posibles_sabores()
Anteriormente se dijo que todo método debía tener por lo menos 1 entrada y que la primera entrada debía ser el objeto. En este caso, como los métodos estáticos no están ligados a los objetos, no es necesario poner esta entrada.
"When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck"
Esto se refiere a que Python no revisa el tipo de objeto explícitamente, sino que revisa el comportamiento del objeto cuando se evoca un método. Por esto, en la instanciación, no se definió el tipo de objeto que se creaba, sino que se puso un nombre, una clase y con ella, unos atributos y métodos.
En nuestro ejemplo de física de partículas, todas las partículas tienen un método carga() válido y por tanto se pueden tratar por igual. Se puede crear una función que calcule la carga total de un grupo de partículas, sin saber nada de cada una de ellas.
from scipy import constants as cts
class Proton(object):
""" Clase protón.
Atributos:
----------
c : carga en unidades de [e]
m : masa en unidades de [kg]
"""
def __init__(self):
""" Inicializa la partícula con una carga y masa definida
"""
self.c = 1
self.m = cts.m_p
class Electron(object):
""" Clase electrón.
Atributos:
----------
c : carga en unidades de [e]
m : masa en unidades de [kg]
"""
def __init__(self):
""" Inicializa la partícula con una carga y masa definida
"""
self.c = -1
self.m = cts.m_e
def carga_total(particles):
tot = 0
for p in particles:
tot += p.c
return tot
p = Proton()
e1 = Electron()
e2 = Electron()
particles = (p, e1, e2) # La colección de partículas se da como una tupla
print 'Con una tupla, la carga total es ',carga_total(particles)
particles2 = [p, e1, e2] # La colección de partículas se da como una lista
print 'Con una lista, la carga total es ',carga_total(particles2)
particles3 = {p, e1, e2} # La colección de partículas se da como un conjunto
print 'Con un conjunto, la carga total es ',carga_total(particles3)
De ser necesario, se puede evitar el duck typing mediante una verificación del tipo de objeto, esto se hace explícitamente con la función integrada isinstance(), que verifica la clase a la que pertenece el objeto.
def carga_total(particles):
tot = 0
for p in particles:
if isinstance(p, Particle):
tot += p.c
return tot
p = Proton()
e1 = Electron()
e2 = Electron()
particles = (p, e1, e2)
print 'La carga total es ',carga_total(particles)
El poliformismo ocurre cuando una clase hereda los atributos de la clase base (o parent class). Es la habilidad de redefinir métodos para subclases. Vamos a crear una clase llamada Elemental(), cuyos objetos serán todas las partículas elementales
class Elemental(Particle):
def __init__(self, carga, masa, posicion, spin):
# Toma los atributos carga, masa y posición de la clase base
super(Elemental, self).__init__(carga, masa, posicion)
self.s = spin
self.es_fermion = bool(spin % 1.0)
self.es_boson = not self.es_fermion
Se observa que la clase Elemental() acepta a otra clase como entrada, (la clase Particle()), en vez de object. Esta es la manera de denotar que Elemental() es una subclase de Particle(), de tal manera que Elemental() hereda los atributos y comportamientos de la clase Particle().
En la siguiente celda, crear Compuestas() como una subclase de Particle(), ya que las partículas compuestas, se constituyen de partículas elementales, pero no comparten sus atributos y en cambio, si tienen carga, masa y posición, es decir, los atributos de la clase base. En esta subclase, dentro del constructoe se va a crear el atributo constituyentes, donde irá una lista de partículas elementales para cada partícula compuesta.
# Crear la subclase Compuestas()
Las subclases Elemental() y Compuestas son objetos para la clase base (Particle()), por tanto, estas subclases reciclan las funciones y los atributos asignados a la clase base, para no tener que reescribirlos. Además, en una subclase se pueden modificar los atributos ya definidos en la clase base:
class Elemental(Particle):
roar = "Soy una partícula elemental!"
def __init__(self, carga, masa, posicion, spin):
# Toma los atributos carga, masa y posición de la clase base
super(Elemental, self).__init__(carga, masa, posicion)
self.s = spin
self.es_fermion = bool(spin % 1.0)
self.es_boson = not self.es_fermion
car = 1
mas = 2
pos = {'x':1, 'y':2, 'z':0}
spi = 1.5
p = Elemental(car,mas,pos,spi)
p.holis()
La superclase es la clase base de una subclase, así que cualquier clase puede ser una superclase. Por ejemplo, Elemental() es una subclase perteneciente a Particle(), pero como los quarks son un tipo de partícula elemental, Quark() puede ser una subclase de Elemental() para heredar sus atributos, haciendo que esta última se convierta en una superclase.
class Quark(Elemental):
def __init__(self, carga, masa, posicion, spin, color, sabor):
super(Quark, self).__init__(carga, masa, posicion, spin)
self.color = color
self.sabor = sabor
car = 1
mas = 2
pos = {'x':1, 'y':2, 'z':0}
spi = 1.5
col = 'green'
sab = 'up'
p = Quark(car,mas,pos,spi,col,sab)
print p.color
Es cuando una subclase hereda de más de una superclase. Por ejemplo en mecánica cuántica existe la dualidad onda-partícula y por tanto, las partículas elementales deberán heredar atributos de onda como una frecuencia y amplitud, y atributos de partícula como la carga.
En un nuevo jupyter notebook: