Ir mas lejos con el lenguaje Python y la POO
Introducción
Después del descubrimiento del lenguaje, de su lado objeto, de algunos módulos y librerías, todavía quedan algunas nociones por abordar en la POO, en el lenguaje Python objeto. Ese es el objetivo de este capítulo.
Algunos conceptos de objeto esenciales
Esta sección cubre solo los casos más comunes. Pero debes saber que hay 23 patrones de diseños en POO (Design Patterns en inglés). Todos se describen en el libro Design Patterns: Elements of Reusable Software publicado en 1994 y escrito por "la banda de los cuatro" (Gang of Four: Gof): Señores Gamma, Helm, Johnson y Vlissides.
1. El polimorfismo
Justo antes de abordar la herencia múltiple, primero debemos conocer el concepto de polimorfismo en POO.
Hay varios tipos, pero para una función en Python, el polimorfismo significa que la misma función se puede usar para diferentes tipos.
Como la función len(), por ejemplo, que le permite averiguar el número de elementos de un objeto.
>>> len("abcd") # una cadena de caracteres
4
>>> len( [1,2,3,4] ) # una lista
4
Se dice que la función len() es polimórfica, porque trabaja con varios tipos de datos.
En el caso de Python, se trata del concepto "duck typing" (tipado de pato).
Duck typing es un concepto derivado de la frase atribuida a James Withcomb Riley (escritor y poeta estadounidense de principios del siglo XX): "Si veo un pájaro que vuela como un pato, grazna como un pato y nada como un pato, entonces llamo a este pájaro un Pato ".
Aplicado al caso de nuestra función len(), si un objeto tiene una función que le permite ser contado, entonces es contable.
Y en Python, contar un objeto es pedirle que ejecute una función especial __len__ (las funciones especiales en Python se abordarán pronto).
>>> "abcd".__len__()
4
Si un objeto tiene esta función, entonces es contable ("si vuela como un pato y si grazna como un pato…").
En Python, tenemos en cuenta las características de los objetos en lugar del tipo de los objetos.
2. La herencia múltiple
En el capítulo de descubrimiento de la POO, se discutió la noción de herencia.
En POO, algunos lenguajes permiten la herencia múltiple, como Python, C++, Eiffel y otros no, como Java, Ruby o Ada.
Hasta ahora, nadie ha podido demostrar si uno es mejor que el otro. Esto existe en Python, pero tampoco es obligatorio.
Y como siempre ocurre...
Los métodos especiales de instancias
Como acabamos de ver, las instancias de objetos de Python tienen métodos especiales. Cuando queremos contar los elementos de un objeto, usamos la instrucción len().
Esto ejecuta la función __len__ del objeto. Si no existe, es un descuido del programador o la función len() no es aplicable.
Los dobles guiones bajos significan (por convención), que es una función privada.
Solo son especiales en el nombre, porque en realidad son funciones definidas por el lenguaje que se utilizarán en determinados contextos.
Por ejemplo:
-
El contexto de enumeración con __len__
-
El contexto de conversión en cadenas de caracteres con __str__
-
El contexto de inicialización de instancia con __init__
Un pequeño ejemplo con __len__:
>>> class A():
... def __len__(self):
... return 5
...
>>> o = A()
>>> len(o)
5
Hay muchos otros y esta sección detallará los principales.
1. Las funciones especiales clásicas
A continuación se muestra una tabla con las funciones especiales más comunes; algunas se conocen desde el principio.
La mayoría solo necesitan implementarse de una manera tan sencilla como __len__, Otras, por otro lado, se deben estudiar primero.
Función |
Contexto |
Observaciones sobre su uso |
del__ |
Destrucción del objeto |
|
init__ |
Inicialización del objeto |
|
new__ |
Llamada antes de la creación del objeto |
Debe devolver un objeto nuevo |
repr__ |
Representación del objeto |
print o en el intérprete |
getattr__ |
Acceso a atributos inexistentes |
gettatr(objeto, "nombre") |
getattribute__ |
Acceso a los atributos |
Todos los atributos |
setattr__ |
Acceso a los atributos |
settatr(objeto, "nombre") |
delattr__ |
Eliminación de un atributo |
|
getitem__ |
objeto [índice] |
|
setitem__ |
objeto [índice] = valor |
|
delitem__ |
del objeto [índice] |
|
contains__ |
objeto ’in’ objeto |
|
len__ |
Recuento, longitud del objeto |
|
call__ |
Hacer un objeto llamable (callable) |
|
missing__ |
La clave no existe en un diccionario |
|
iter__ |
Debe devolver un objeto iterador |
|
enter__ |
Administrador de contexto: entrada |
With |
exit__ |
Administrador de contexto: salida |
With |
hash__ |
Calcula un valor único |
Si utilización... |
El administrador de contexto (with) __enter__, __exit__
Las funciones especiales permiten hacer muchas cosas interesantes, pero con estas, __enter__ y __exit__, Python nos lleva un paso más allá.
Estas dos funciones permiten crear clases que se pueden usar con la palabra clave ’with’, que es lo que llamamos un administrador de contexto.
La instrucción with asociada con un bloque de código, crea un contexto específico válido para todo el bloque de código.
El típico ejemplo de uso es abrir un archivo.
# ejemplo con la apertura de un archivo (caso típico)
with open(archivo) as f:
<bloque de código>
# al final del bloque de código el archivo
# se cerrará y la variable 'f' que representa al archivo
# se eliminará.
Esto ya existe, pero si quisiéramos escribir lo mismo tendríamos que implementar la función __enter__ y abrir el archivo, que lo cerraríamos en la función __exit__.
Entendiendo cómo funcionan estas dos funciones especiales, una a la entrada de un contexto y la otra a la salida, inmediatamente pensamos en la medida de tiempo que toma un bloque de código.
De ahí la idea de esta clase timing():
#archivo: f_spe/with1.py
import math
import time
class timing(): ...
Los objetos mutables y no mutables
Bien podría decirlo de inmediato, esta sección ciertamente no será su sección favorita. Pero dado que este es un concepto importante, debe abordarse.
En el lenguaje Python, todo es objeto. Hay objetos mutables y objetos no mutables.
Por mutable, debemos entender modificable y sobre todo, diferenciar entre modificación y asignación.
Una variable es una referencia a un objeto.
Cuando asignamos una variable, no cambiamos el valor del objeto sino que cambiamos de objeto.
Cuando modificamos un objeto, modificamos el contenido del objeto.
Cuando escribimos:
a = 1
a = 2
a hace referencia al objeto 1 (el entero 1), luego referencia al objeto 2 (el entero 2), no es el objeto 1 el que ha cambiado, es el valor de ’a’.
Y es fácil entender que no podamos modificar el objeto ’1’ (el entero 1).
Por el contrario, si escribimos:
>>> a = [1] # creación de una lista con el objeto 1
>>> a[0] = 2 # modificación de la lista
>>> a
[2]
modificamos el objeto lista creado, por lo que el objeto lista es modificable.
De ahí la noción importante de objetos mutables y objetos no mutables.
No se vay todavía, pronto entenderá para qué sirve.
1. Los mutables
Las listas, los diccionarios y los conjuntos son objetos mutables, por lo que se pueden modificar. Además, tienen métodos previstos para este propósito.
>>> a = [1,2,3]
>>> id(a)
140647764070880 # Id de a
>>> a.append(4)
>>> a
[1, 2, 3, 4]
>>> id(a)
140647764070880 # Es el mismo objeto
Pero atención, todavía hay algunas trampas:
>>>...
Información adicional sobre las clases en Python
1. Los atributos implícitos
Una clase, incluso vacía de cualquier atributo y método, contiene atributos implícitos para su gestión interna.
Y como siempre con Python, no hay nada oculto, solo necesita saber dónde está.
Sea el siguiente script:
class A():
pass
for i in dir(A):
print("%20s |" % i, "%s" % getattr(A,i))
que devolverá todo lo que se encuentra en una clase.
__class__ | <class 'type'>
__delattr__ | <slot wrapper '__delattr__'
of 'object' objects>
__dict__ | {'__module__': '__main__',
'__dict__': <attribute '__dict__'
of 'A' objects>,
'__weakref__': <attribute '__weakref__'
of 'A' objects>, '__doc__': None}
__dir__ | <method '__dir__' of 'object' objects>
__doc__ | None
__eq__ | <slot wrapper '__eq__' of 'object' objects>
__format__ | <method '__format__' of 'object' objects>
__ge__ | <slot wrapper '__ge__' of 'object' objects>
__getattribute__ | <slot wrapper '__getattribute__'
...
Las docstrings - cadenas de documentación
1. Definición
La docstring, cadena de caracteres de documentación, le permite documentar su código. No es necesario asignarlo, es suficiente con colocar las explicaciones en el lugar correcto, como regla general, al comienzo del objeto que desea documentar.
Es una buena costumbre, especialmente si tiene que trabajar con otras personas en un proyecto.
Escribir docstrings tiene muchas ventajas.
-
Hay una función help() en el intérprete que usa las docstrings.
-
La mayoría de las herramientas con un opción "python" muestran esta documentación.
-
Herramientas que permiten generar un documento en HTML a partir de las docstring.
-
Es un mecanismo de documentación estandarizado en el mundo de Python.
-
El código Python puede usar la docstring para leerlo o mostrarlo.
-
Podemos poner pruebas en las docstring, que luego sirven como ejemplos de uso.
-
En un archivo tenemos el código, la documentación y las pruebas.
-
Dos niveles simples y avanzados.
2. Uso
A continuación se muestra un ejemplo rápido:
>>> def f(arg1, arg2):
... """
... Documentación de la función f()
... """
... pass
...
>>> help(f)
python docstring
Help on function f in module __main__:
f(arg1, arg2)
Documentación de la función f()
(END)
Nada demasiado complicado, solo hay que documentarlo usando comillas triples.
Es posible insertar docstrings al comienzo de un script o módulo, al comienzo de una clase o función.
A continuación se muestra un ejemplo un poco más completo:
# archivo: clase/docstr1.py
# -----------------------
# Un módulo documentado
# -----------------------
"""
Un...
Los decoradores
Python ofrece muchas funcionalidades, módulos, funciones, clases y este libro le permite repasar, con suerte, los conceptos básicos.
Con los decoradores abordamos las nociones avanzadas de programación, pero como es muy práctico y de uso común, hay que saber al menos qué es.
Un decorador en Python no es ni más ni menos que una función que se ejecutará antes que una función.
Esto permite, entre otras cosas, modificar el comportamiento de esta función antes de su ejecución.
La sintaxis es la siguiente:
@decorador
def la_función_a_decorar():
pass
Pero cuidado, no es tan intuitivo como parece.
Primera trampa: el decorador se ejecuta durante la definición de la función; se le da una función y debe devolver una función, que sustituirá a la función "decorada".
Ejemplo de decorador que no hace nada
#archivo: deco/deco0.py
def mi_deco(funcion): o
return funcion
@mi_deco
def funcion1():
print("Soy la función 1 ")
@mi_deco
def funcion2():
print("Soy la función 2 ")
@mi_deco
def funcion3():
print("Soy la función...
Los iteradores, generadores y otras expresiones generadoras
Con el lenguaje Python, es posible crear todos los objetos informáticos imaginables, y más. En el "más" encontramos, entre otras cosas, las nociones de iteradores y generadores.
1. Los iteradores
Un iterador es un objeto que le permite examinar todos los objetos contenidos en otro objeto. Con Python es posible crear sus propios iteradores.
Pero primero hay que estudiar un poco lo que ocurre en un ejemplo más clásico, el bucle ’for’:
for <variable> in <object iterable>:
<bloque de código>
¿Cómo funciona este pilar de la programación?
En Python, esquemáticamente, sucede de esta manera:
Primero, el intérprete comienza invocando la función iter(<object_iterable>), para obtener un objeto iterable a cambio.
Una vez que el objeto ha sido capturado, el bucle ’for’ buscará obtener el siguiente valor llamando a la función next() en el objeto y asignar este valor a la variable prevista para este propósito.
El bucle llamará a la función next() hasta que no haya más valor y la función next() devuelva una excepción.
Para comprender completamente cómo funciona, es posible hacerlo manualmente.
>>> objeto_iterable = iter([1,2,3])
>>> objeto_iterable
<list_iterator object at 0x7fda31b8c870>
>>> next(objeto_iterable)
1
>>> next(objeto_iterable)
2
>>> next(objeto_iterable)
3
>>> next(objeto_iterable)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> dir(objeto_iterable)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__getattribute__', '__gt__', '__hash__',
'__init__', '__init_subclass__', '__iter__', '__le__',
'__length_hint__', '__lt__', '__ne__', '__new__', '__next__',
'__reduce__', '__reduce_ex__'...
Gestionar sus propias excepciones
Es un hecho, todo es objeto con Python y por supuesto, las excepciones también lo son.
Es posible que esto nunca suceda, especialmente con scripts pequeños, pero en proyectos grandes, las excepciones son una forma de comunicarse con los otros programadores y usuarios.
Después de todo, a veces escuchamos que podemos juzgar la calidad de un producto informático, por la cantidad de mensajes de error que puede manejar.
En Python, las excepciones provienen de la clase… Exception.
Y para crear sus propias excepciones, es suficiente con crear una clase que herede de la clase Exception y redefina la función __str__.
A continuación se muestra un script de ejemplo:
# archivo: except/except4.py
class MiExcepcion(Exception):
"""
La documentación es primordial sobre todo para las excepciones
que, recuerdo, no son objetos sin importancia
"""
def __init__(self, message):
self.message = message
def __str__ (self):
return "MiExcepcion msg=%s" % self.message
try:
print("Antes del error")
raise MiExcepcion("MENSAJE...
Las funciones nativas
Python tiene algunas funciones nativas, que se pueden usar directamente. Aquí está la lista, ordenada por temática.
Para obtener una lista alfabética, consulte la documentación oficial: https://docs.python.org/es/3/library/functions.html
1. Las funciones nativas in clasificables
input([prompt]) |
Lee en la entrada estándar. Si el módulo readline está cargado, input() lo usará para manejar un historial. |
open() |
Abra un archivo (consulte el capítulo El lenguaje Python - Las entradas/salidas (archivo y otros)). |
print() |
La función de impresión (ver capítulo El lenguaje Python - La función print()). |
breakpoint() |
Usado con el depurador, establece un punto de interrupción para detener la ejecución. |
compile(code, filename, mode) |
Compila el código fuente de Python. Para ser utilizado de la siguiente manera:
mode puede ser:
|
eval(expression, [globals[, locals]]) |
Evalúa una expresión.
|
exec() |
Ejecuta código Python compilado con compile(). |
help() |
Llama al sistema de ayuda nativo de Python. |
format(valor[,format]) |
Da formato a un valor (ver capítulo El lenguaje Python - La función print() - Print() formateo cadena.format ()). |
globals() |
Devuelve la tabla de las variables globales (ver capítulo El lenguaje Python - Las funciones - Las funciones y el alcance de las variables). |
locals() |
Devuelve la tabla de las variables locales (ver capítulo El lenguaje Python - Las funciones - Las funciones y el alcance de las variables). |
2. Las funciones nativas binarias
Las funciones binarias nativas permiten la manipulación de bytes y tablas de bytes.
bytearray() |
Crea una tabla de bytes. |
bytes() |
Devuelve un nuevo objeto bytes. |
memoryview() |
Permite manejar un objeto como una secuencia de bytes sin hacer una copia. |
Para más detalles visite: https://docs.python.org/es/3/library/stdtypes.html#bytes-methods
3. Las funciones nativas de conversión o creación de tipo
Estas funciones nativas permiten...
Resumen
En este punto, es hora de pasar a un nivel superior y dejar de considerarse un "principiante" en el lenguaje Python. Los tipos de datos, el lenguaje, la POO, la librería estándar, etc. todo esto se ha abordado. Ahora es necesario practicar, con la mayor regularidad posible, el lenguaje Python.
Los siguientes cinco capítulos tratan temas mucho más prácticos, en particular lo que Python puede proporcionar como información sobre un sistema, el manejo de diferentes formatos de archivo o la simulación de actividad en una base de datos, pasando por la generación de informes en PDF.