Programación Orientada a Objetos

Programación Orientada a Objetos#

https://en.wikipedia.org/wiki/Object-oriented_programming

Tenemos las siguientes definiciones:

Clase: es una plantilla (template) para crear objetos (objects).

Objeto: los objetos son tipos de variables que a su vez tienen member variables y member functions o métodos. Estos métodos representan comportamiento y/o funcionalidad asociada al objeto.

Ejemplos#

En Python, todo es un objeto.

x = 2

¿Cómo me doy cuenta? La variables x tiene métodos, por ejemplo:

x.to_bytes(1, 'big')
b'\x02'
from datetime import date
fecha = date(2021, 8, 25)

El objeto fecha, que es de tipo date se construye con tres enteros positivos que representan el año, mes y día. Si uso una combinación inválida se genera un error.

date(2021, 2, 29) # El 2021 no es bisiesto.
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[4], line 1
----> 1 date(2021, 2, 29) # El 2021 no es bisiesto.

ValueError: day is out of range for month

El objeto fecha tiene métodos.

fecha.isoformat()
'2021-08-25'

Las funciones son objetos.

import math
type(math.exp)
builtin_function_or_method

La función math.exp tiene métodos.

math.exp.__doc__
'Return e raised to the power of x.'

Definir una Clase#

Podemos definir nuestras propias clases y objetos. En el siguiente ejemplo vamos a definir una clase que represente una coordenada cartesiana en el espacio.

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

El método __init__ es el constructor de la clase, el parámetro self representa la instancia de la clase (un objeto concreto).

Nota nerd: en Python los métodos que empiezan y terminan con doble guión bajo __ se llaman dunder methods.

https://dbader.org/blog/python-dunder-methods

¿Cómo se da de alta (instantiate) un objeto de tipo PointInSpace?

punto = PointInSpace(0, 0, 0)
type(punto)
__main__.PointInSpace

Luego, a través de punto podemos acceder a sus coordenadas.

punto.x
0
punto.y
0
punto.z
0

Lo siguiente genera un error.

punto.w
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-16-a1eeaf757af9> in <module>
----> 1 punto.w

AttributeError: 'PointInSpace' object has no attribute 'w'

Podemos agregar más comportamiento o funcionalidad a la clase, y en consecuencia, a todos los objetos de esa clase.

class PointInSpace:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z
        
    def norma(self):
        return math.sqrt(self.x**2 + self.y**2 + self.z**2)
punto = PointInSpace(1, 2, 3)
punto.norma()
3.7416573867739413

Un objeto es mutable:

punto.x = 2
punto.x
2

Incluso puedo agregarle métodos:

def norma_geom(self):
    return math.pow(self.x * self.y * self.z, 1./3)
PointInSpace.norma_geom = norma_geom
punto = PointInSpace(1, 2, 3)
punto.norma_geom()
1.8171205928321397

Otros dunder methods#

import pandas as pd
data = [
    {
        'nombre': 'Donald',
        'especie': 'pato',
    },
    {
        'nombre': 'Mickey',
        'especie': 'ratón',
    },
    {
        'nombre': 'Tribilín',
        'especie': 'perro',
    }
]
data_df = pd.DataFrame(data)
data_df
nombre especie
0 Donald pato
1 Mickey ratón
2 Tribilín perro
print(data_df.__doc__)
    Two-dimensional, size-mutable, potentially heterogeneous tabular data.

    Data structure also contains labeled axes (rows and columns).
    Arithmetic operations align on both row and column labels. Can be
    thought of as a dict-like container for Series objects. The primary
    pandas data structure.

    Parameters
    ----------
    data : ndarray (structured or homogeneous), Iterable, dict, or DataFrame
        Dict can contain Series, arrays, constants, or list-like objects.

        .. versionchanged:: 0.23.0
           If data is a dict, column order follows insertion-order for
           Python 3.6 and later.

        .. versionchanged:: 0.25.0
           If data is a list of dicts, column order follows insertion-order
           for Python 3.6 and later.

    index : Index or array-like
        Index to use for resulting frame. Will default to RangeIndex if
        no indexing information part of input data and no index provided.
    columns : Index or array-like
        Column labels to use for resulting frame. Will default to
        RangeIndex (0, 1, 2, ..., n) if no column labels are provided.
    dtype : dtype, default None
        Data type to force. Only a single dtype is allowed. If None, infer.
    copy : bool, default False
        Copy data from inputs. Only affects DataFrame / 2d ndarray input.

    See Also
    --------
    DataFrame.from_records : Constructor from tuples, also record arrays.
    DataFrame.from_dict : From dicts of Series, arrays, or dicts.
    read_csv
    read_table
    read_clipboard

    Examples
    --------
    Constructing DataFrame from a dictionary.

    >>> d = {'col1': [1, 2], 'col2': [3, 4]}
    >>> df = pd.DataFrame(data=d)
    >>> df
       col1  col2
    0     1     3
    1     2     4

    Notice that the inferred dtype is int64.

    >>> df.dtypes
    col1    int64
    col2    int64
    dtype: object

    To enforce a single dtype:

    >>> df = pd.DataFrame(data=d, dtype=np.int8)
    >>> df.dtypes
    col1    int8
    col2    int8
    dtype: object

    Constructing DataFrame from numpy ndarray:

    >>> df2 = pd.DataFrame(np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]),
    ...                    columns=['a', 'b', 'c'])
    >>> df2
       a  b  c
    0  1  2  3
    1  4  5  6
    2  7  8  9
    
print(data_df.__str__())
     nombre especie
0    Donald    pato
1    Mickey   ratón
2  Tribilín   perro
data_df.__repr__()
'     nombre especie\n0    Donald    pato\n1    Mickey   ratón\n2  Tribilín   perro'
data_df.__len__()
3

So?#

Vamos a desarrollar un pequeño ejemplo que nos de una mejor idea de las posibilidades que se abren cuando usamos objetos.

NOTA: este código es sólo de ejemplo y le falta mucho para poder ser usado en un entorno de producción.

class PosicionRF:
    def __init__(self, nemotecnico, nominal, valor_mercado, tir_mercado, duracion):
        self.nemotecnico = nemotecnico
        self.nominal = nominal
        self.valor_mercado = valor_mercado
        self.tir_mercado = tir_mercado
        self.duracion = duracion
        
    def sensibilidad(self, pb):
        _pb = pb / 10000
        return self.valor_mercado * self.duracion * _pb / (1 + self.tir_mercado)
    
    def __str__(self):
        result = f'nemotecnico: {self.nemotecnico}\n'
        result += f'nominal: {self.nominal}\n'
        result += f'valor_mercado: {self.valor_mercado}\n'
        result += f'tir_mercado: {self.tir_mercado}\n'
        result += f'duracion: {self.duracion}'
        return result
    
    def __repr__(self):
        result = f'nemotecnico: {self.nemotecnico}\n'
        result += f'nominal: {self.nominal}\n'
        result += f'valor_mercado: {self.valor_mercado}\n'
        result += f'tir_mercado: {self.tir_mercado}\n'
        result += f'duracion: {self.duracion}'
        return result
class Cuenta:
    def __init__(self, id_cliente):
        self.id_cliente = id_cliente
        self.posiciones = []
        
    def agrega_posicion(self, posicion):
        self.posiciones.append(posicion)
        
    def valor_mercado(self):
        if len(self.posiciones) > 0:
            return sum([p.valor_mercado for p in self.posiciones])
        return 0
    
    def sensibilidad(self, pb):
        if len(self.posiciones) > 0:
            return round(sum([p.sensibilidad(pb) for p in self.posiciones]), 6)
        return 0
    
    def __len__(self):
        return len(self.posiciones)
    
    def __getitem__(self, index):
        return self.posiciones[index]
    
    def __iter__(self):
        return (t for t in self.posiciones)
cuenta = Cuenta('alvaro')
posicion = PosicionRF('bono1', 100, 101, .01, 4)
len(cuenta)
0
cuenta.agrega_posicion(posicion)
len(cuenta)
2
cuenta[0]
nemotecnico: bono1
nominal: 100
valor_mercado: 101
tir_mercado: 0.01
duracion: 4
cuenta.valor_mercado()
101
cuenta.sensibilidad(100)
4.0
for p in cuenta:
    print(p)
nemotecnico: bono1
nominal: 100
valor_mercado: 101
tir_mercado: 0.01
duracion: 4
nemotecnico: bono1
nominal: 100
valor_mercado: 101
tir_mercado: 0.01
duracion: 4

Hay mucho más y muchas cosas muy interesantes …