El paradigma de programación tradicional (piense en Fortran, C, MATLAB, etc.) se denomina procedural.
Funciona de la siguiente manera
Otros dos paradigmas importantes son la programación orientada a objetos (POO) y la programación funcional.
En el paradigma de la programación orientada a objetos, los datos y las funciones se agrupan en «objetos»; en este contexto, las funciones se denominan métodos.
Los métodos se invocan para transformar los datos contenidos en el objeto.
append()
y pop()
que transforman los datos. Los lenguajes de programación funcionales se basan en la idea de componer funciones.
¿En cuál de estas categorías encaja Python?
En realidad, Python es un lenguaje pragmático que mezcla los estilos orientado a objetos, funcional y procedimental, en lugar de adoptar un enfoque purista.
!pip install rich
En Python, un objeto es una colección de datos e instrucciones almacenados en la memoria que los caracterizan 3 elementos
Cada objeto de Python pertenece a un ´type´, a una familia: es esto lo que dice lo que puede hacer o no.
Ejemplo
s = 'Este objeto, creado con comillas, es un tipo string'
type(s)
# Tipo integer
x = 42
type(x)
El tipo de un objeto es importante para muchas expresiones.
Por ejemplo, el operador de suma entre dos cadenas significa concatenación
'300' + 'cc'
Entre dos numeros
300 + 400
Pero que pasa si sumamos dos tipos de objetos distintos
'300' + 400
Aquí estamos mezclando tipos, y el interprete de Python no tiene lo que el usuario quiere
'300
en un entero y luego sumarlo a 400
, o 400
en cadena y luego concatenarla con '300'
. Algunos lenguajes podrían intentar adivinarlo, pero Python está fuertemente tipado.
TypeError
. Para evitar el error, es necesario aclararle al interprete qué es lo que realmente quieres hacer, haciendo que los tipos sean compatibles: por ejemplo,
int('300') + 400
En Python, cada objeto tiene un identificador único, que ayuda a Python (y a nosotros) a seguir la pista del objeto.
La identidad de un objeto puede obtenerse mediante la función id()
.
y = 2.5
z = 2.5
id(y)
id(z)
En este ejemplo, y
y z
tienen el mismo valor (es decir, 2,5
), pero no son el mismo objeto.
De hecho, la identidad de un objeto no es más que su dirección en memoria.
Pero si hago
y = z = 2.5
print(id(y), id(z))
Recordar que los objetos pueden ser mutables e inmutables
Si establecemos x = 42
entonces creamos un objeto de tipo int
que contiene
el dato 42
.
De hecho, contiene más, como muestra el siguiente ejemplo
x = 42
x
x.imag
x.__class__
Cuando Python crea este objeto entero, almacena con él diversa información auxiliar, como la parte imaginaria y el tipo.
Cualquier nombre que siga a un punto se llama atributo del objeto a la izquierda del punto.
imag
y __class__
son atributos de x
. En este ejemplo vemos que los objetos tienen atributos que contienen información auxiliar.
También tienen atributos que actúan como funciones, llamados methods.
Estos atributos son importantes, así que vamos a discutirlos en profundidad.
Los métodos son funciones que van unidas a objetos.
Formalmente, los métodos son atributos de los objetos que son callable, esto es,llamables - es decir, atributos que pueden ser llamados como funciones
x = ['foo', 'bar']
callable(x.append)
callable(x.__doc__)
Los métodos suelen actuar sobre los datos contenidos en el objeto al que pertenecen, o combinan esos datos con otros.
x = ['a', 'b']
x.append('c')
s = 'Uruguay'
s.upper()
s.lower()
s.replace('Uruguay', 'Montevideo')
Gran parte de la funcionalidad de Python se organiza en torno a llamadas a métodos.
Por ejemplo, considere el siguiente fragmento de código
x = ['a', 'b']
x[0] = 'aa' # Assignación de un item
x
No parece que haya ningún método utilizado aquí, pero de hecho la notación de asignación de corchetes es sólo una interfaz conveniente para una llamada a un método.
Lo que realmente ocurre es que Python llama al método __setitem__
, como sigue
x = ['a', 'b']
x.__setitem__(0, 'aa') # Equivalente a x[0] = 'aa'
x
Nota: Dado que es open source, si quisieras podrías modificar el método __setitem__
, para que la asignación de corchetes haga algo totalmente diferente
from rich import inspect
x = 10
inspect(10)
Si queremos ver también los métodos, podemos utilizar
inspect(10, methods=True)
De hecho, aún hay más métodos, como puedes ver si ejecutas inspect(10, all=True)
.
Si bien Python es un lenguaje orientado a objetos, OOP, prima la legibilidad y la coherencia en el estilo.
Por ejemplo, esto parece más una forma de script de procedimientos:
x = ['a', 'b']
m = len(x)
m
Si Python está orientado a objetos, ¿por qué no usamos x.len()
?
La respuesta está relacionada con el hecho de que Python busca la legibilidad y un estilo consistente.
Hay tres tipos de funciones: métodos, de terceros, o built-ins.
Como los usuarios crean funciones, para mantener cierto orden se crean funciones generales dentro de Python cuyo nombre siempre refleje lo mismo.
Por ejemplo, los usuarios pueden querer añadir métodos que miden la longitud de del objeto, convenientemente definida.
Al nombrar un método de este tipo, las opciones naturales son len()
y length()
.
Si algunos usuarios eligen len()
y otros eligen length()
, entonces el estilo será
inconsistente y difícil de recordar.
Para evitar esto, el creador de Python decidió añadir
len()
como función incorporada, para ayudar a enfatizar que len()
es la convención.
Ahora, habiendo dicho todo esto, Python sigue siendo orientado a objetos bajo el capó.
De hecho, la lista x
de la que hablamos antes tiene un método llamado __len__()
.
Todo lo que hace la función len()
es llamar a este método.
En otras palabras, el siguiente código es equivalente:
x = ['a', 'b']
len(x)
y
x = ['a', 'b']
x.__len__()
Imprime una lista de métodos del objeto booleano: True
.
Puedes utilizar callable()
para comprobar si un atributo de un objeto puede ser llamado como una funciónn
Primero, encontrar todos los atributos de True
print(sorted(True.__dir__()))
o
print(sorted(dir(True)))
Dado que el tipo de datos boolean es un tipo primitivo, también puede encontrarlo en el espacio de nombres incorporado
print(dir(__builtins__.bool))
Aquí utilizamos un for
loop para filtrar los atributos que son callable, i.e., métodos
attributes = dir(__builtins__.bool)
callablels = []
for attribute in attributes:
# Usar eval() para evaluar un string como una expresion
if callable(eval(f'True.{attribute}')):
callablels.append(attribute)
print(callablels)
La programación orientada a objetos es útil por la misma razón que la abstracción: para reconocer y explotar la estructura común.
Se trata de abstracciones que reúnen «objetos» del mismo «tipo».
Reconocer una estructura común nos permite emplear herramientas comunes.
Veamos un ejemplo sencillo para ver de qué estamos hablando.
Si queremos crear nuestros propios tipos de objetos necesitamos usar definiciones de clase.
Una class definition es un modelo para englobar una clase particular de objetos (por ejemplo, listas, cadenas o números complejos).
Describe
Qué tipo de datos almacena la clase
Qué métodos tiene para actuar sobre estos datos
Un objeto o instancia es el output, la realización clase, creada a partir del modelo básico.
Cada instancia tiene sus propios datos.
Los métodos establecidos en la definición de la clase actúan sobre los datos contenidos en la instancia.
En Python, los datos y métodos de un objeto se denominan colectivamente atributos.
A los atributos se accede mediante la «notación de atributos por puntos»
nombre_objeto.datos
nombre_objeto.nombre_método()
Todo círculo se define de manera única por $(x_0,y_0)$ y su radio $R$.
Estos serían sus atributos: esto es, podemos recoger estos tres números como atributos de datos en una clase.
Por tanto, los atributos caracterizan circulos distintos, pero todos pertenece a la clase de circulos.
Por ello, los valores de $x_0$, $y_0$, y $R$ tiene que ser inicializados cada vez que creamos un círculo nuevo. Esto es, utilizamos un constructor de instancias.
Detro de los clase de círculos, podemos definir otras cosas que los caracterizan, como pude ser el área o circunferencia.
Para ello, usamos métodos que sirven para todos los membros de la clase círculo.
import numpy as np
class Circulo():
def __init__(self, x0, y0, R):
self.x0 = x0
self.y0 = y0
self.R = R
def area(self):
return np.pi*self.R**2
def circunferencia(self):
return 2*np.pi*self.R
# Vamos a crear un circulo
c_1 = Circulo(0,1,2)
c_1.__class__
c_1.x0