Intro

  • Lenguaje creado por Guido van Rossum a principios de los años 90
  • Semi interpretado (se genera un fichero de pseudocódigo máquina o bytecode con extensión .pyc que es lo que realmente se ejecuta)
  • Multiplataforma
  • Open source
  • Orientado a objetos
  • Permite programación imperativa, funcional y orientada a aspectos.

Hola mundo

El hola mundo es tan sencillo como escribir una línea:

In [1]:
print('Hola Mundo')
Hola Mundo

Variables

Python cuenta con las siguientes características destacables referentes a las variables:

  • Tiene tipado dinámico; no hay que declarar el tipo de dato que va a contener una variable (se determina en tiempo de ejecución dependiendo del valor asignado). Además puede cambiarse con otra simple asignación de valor.
  • Es fuertemente tipado; no se permite tratar a una variable como si fuera de un tipo distinto al que tiene, es necesario convertirla de forma explícita.
  • No hay que declarar las variables antes de asignarlas. Y si se intentan usar sin haberse asignado saltará una excepción.
  • Las variables son simplemente nombres que referencian a objetos

Por convención las variables se nombran en minúsculas y con guiones bajos para separar palabras.

In [2]:
variable_zero = 0

Existen 3 tipos básicos de variables:

  1. Númericos
    • Enteros
      • int (sin límite de tamaño)
    • Reales (Decimales)
      • float. P.ej. 0.26, 0.1e-3,…
    • Complejos
      • complex. P.ej. 5+7j
  2. Secuencias (iterables)
    • str
    • bytes
    • bytearray
    • list
    • tuple
    • range
  3. Booleanos
    • bool: True, False

También hay otros tipos predefinidos:

  • Mapeos (dict)
  • Ficheros
  • Clases
  • Instancias
  • Módulos
  • Excepciones
  • ...

Tipos numéricos

In [3]:
# A continuación unos ejemplos. Ah! esto es un comentario :) 
i = 2
i2 = 0xf
f = 2.339
c = 1+2j

# Para obtener el tipo de una variable cualquiera siempre podemos recurrir a type()
print(i, type(i))
print(i2, type(i2))
print(f, type(f))
print(c, type(c))
2 <class 'int'>
15 <class 'int'>
2.339 <class 'float'>
(1+2j) <class 'complex'>

Operaciones con tipos numéricos

In [4]:
print('Suma: 1+1 =', 1+1)
print('Resta: 1-1 =', 1-1)
print('Multiplicación: 2*2 =', 2*2)
print("División: 5/2 =", 5/2) # En v2 el resultado no es el esperado
print("División: 4.0/2 =", 4.0/2) # El resultado es siempre un float si dividendo o divisor son float
print("División entera: 5//2 =", 5//2)
print("Módulo o resto entero: 5%2 =", 5%2)
print('Potencia: 2**3 =', 2**3) # Ojo, el _^_ es un XOR a nivel de bit
print("Valor absoluto: abs(-1) =", abs(-1))
Suma: 1+1 = 2
Resta: 1-1 = 0
Multiplicación: 2*2 = 4
División: 5/2 = 2.5
División: 4.0/2 = 2.0
División entera: 5//2 = 2
Módulo o resto entero: 5%2 = 1
Potencia: 2**3 = 8
Valor absoluto: abs(-1) = 1
In [5]:
import math
print("Truncado de decimales: math.trunc(f) =", math.trunc(f))
print("Redondeo con n decimales: round(f, 2) =", round(f, 2))
print("Redondeo a número entero por abajo: math.floor(f) =", math.floor(f))
print("Redondeo a número entero por arriba: math.ceil(f) =", math.ceil(f))
Truncado de decimales: math.trunc(f) = 2
Redondeo con n decimales: round(f, 2) = 2.34
Redondeo a número entero por abajo: math.floor(f) = 2
Redondeo a número entero por arriba: math.ceil(f) = 3
In [6]:
print("Comparación 1 < 2 <= 2 =", 1 < 2 <= 2)
Comparación 1 < 2 <= 2 = True
In [7]:
print("AND a nivel de bit: 5&1 =", 5&1)
print("AND a nivel de bit: 5|2 =", 5|2)
print("XOR a nivel de bit: 5^1 =", 5^1)
print("NOT a nivel de bit: ~1 =", ~1)
print("Desplazamiento dcho: 5>>1 =", 5>>1)
print("Desplazamiento izdo: 2<<1 =", 2<<1)
AND a nivel de bit: 5&1 = 1
AND a nivel de bit: 5|2 = 7
XOR a nivel de bit: 5^1 = 4
NOT a nivel de bit: ~1 = -2
Desplazamiento dcho: 5>>1 = 2
Desplazamiento izdo: 2<<1 = 4

Cadenas de caracteres

Las cadenas de caracteres son un tipo de secuencia con texto definido entre comillas simples o dobles. Se pueden incluir caracteres especiales escapándolos con \. Se trata de objetos inmutables e iterables.

In [8]:
# Distintos tipos de cadenas de caracteres
s = "string"
unicode = u"äóè"
raw = r"\n"
multiline = """primera linea 
            y ésto se verá en otra"""

print(s, type(s))
print(unicode, type(unicode))
print(raw, type(raw))
print(multiline, type(multiline))
string <class 'str'>
äóè <class 'str'>
\n <class 'str'>
primera linea 
            y ésto se verá en otra <class 'str'>

Operaciones con cadenas de caracteres

In [9]:
print("Longitud: len(s) =", len(s))
print("Concatenación: s + raw =", s + raw) # mejor evitarlo
print("Repetición: raw*3 =", raw*3)
print("Búsqueda: 'tt' not in s =", "tt" not in s)
print("Elemento en posición i: s[2] =", s[2]) # los strings pueden ser tratados como listas
print("Elemento en posición -i: s[-1] =", s[-1])
print("Subcadena: s[0:2] =", s[0:2])
print("Subcadena con salto: s[0:6:2] =", s[0:6:2])
Longitud: len(s) = 6
Concatenación: s + raw = string\n
Repetición: raw*3 = \n\n\n
Búsqueda: 'tt' not in s = True
Elemento en posición i: s[2] = r
Elemento en posición -i: s[-1] = g
Subcadena: s[0:2] = st
Subcadena con salto: s[0:6:2] = srn
In [10]:
print("Primera aparición de subcadena:", s.index("ing"))
print("Número de apariciones de subcadena:", s.count("ing"))
print("Elemento mínimo: min(s) =", min(s))
print("Elemento máximo: max(s) =", max(s))
Primera aparición de subcadena: 3
Número de apariciones de subcadena: 1
Elemento mínimo: min(s) = g
Elemento máximo: max(s) = t
In [11]:
print("Cadena en minúsculas:", s.lower())
print("Cadena en mayúsculas:", s.upper())
print("La cadena empieza por s?:", s.startswith(s))
print("La cadena acaba por s?:", s.endswith(s))
print("La cadena contiene la subcadena 'raw' en la posición:", s.find('raw'))
Cadena en minúsculas: string
Cadena en mayúsculas: STRING
La cadena empieza por s?: True
La cadena acaba por s?: True
La cadena contiene la subcadena 'raw' en la posición: -1
In [12]:
print("Cadena reemplazando s por sss:", s.replace('s', 'sss'))
print("Cadena dividida con split:", s.split('r'))
print("Cadena re-unida con join:", '-'.join(s))
print("Lista convertida en cadena con join:", ' '.join(['a', 'b', 'c']))
print("Cadena sin espacios en los extremos:", '    espacios everywhere   '.strip())
Cadena reemplazando s por sss: ssstring
Cadena dividida con split: ['st', 'ing']
Cadena re-unida con join: s-t-r-i-n-g
Lista convertida en cadena con join: a b c
Cadena sin espacios en los extremos: espacios everywhere

Formateo de cadenas de caracteres

In [13]:
print("{} no es {}".format('aquí', 'allí'))
print("{0} no es {1}, sólo es {0}".format('aquí', 'allí'))
print("{aqui} no es {alli}; es {aqui}".format(aqui='Móstoles', alli='Madrid'))
aquí no es allí
aquí no es allí, sólo es aquí
Móstoles no es Madrid; es Móstoles

Formateo mejorado usando f-strings, con evaluación de expresiones (>= Py3.6):

In [14]:
aqui = 'Móstoles'
f'Me encanta {aqui.upper()} desde {1980+1}'
Out[14]:
'Me encanta MÓSTOLES desde 1981'

Booleanos

  • Valores True, False
  • El tipo bool es subclase del tipo int
  • Operadores lógicos: and, or, not (textuales)
  • Operadores comparativos: ==, !=, <, >, <=, >=
In [15]:
b = True
print(b, type(b))
True <class 'bool'>

Operaciones con booleanos

In [16]:
print("not True:", not True)
print("True and not True or False:", True and not True or False)
print("True != False:", True != False)
not True: False
True and not True or False: False
True != False: True
In [17]:
print("True + True:", True + True)
print("2 == True:", 2 == True)
True + True: 2
2 == True: False

Se considera False lo siguiente...

In [18]:
print(bool(None)) # None (pasado a booleano) equivale a False
print(0 == True)  # El 0 de cualquier tipo numérico equivale a False
print(bool(""))   # La cadena vacía equivale a False
print(bool({}))   # El diccionario vacío equivale a False
print(bool([]))   # La secuencia vacía equivale a False
False
False
False
False
False

Ojo con esto:

In [19]:
print(bool(-2))
True

Manipulación de variables

La constante None equivale a null en otros lenguajes

In [20]:
print(None, type(None))
print(0 is None) # Para comparar con None se usa is en lugar de ==
None <class 'NoneType'>
False

Asignación de valores múltiple:

In [21]:
x, y = 4, 5

Intercambio de valores sencillo:

In [22]:
x, y = y, x
print(x, y)
5 4

Conversión entre tipos explícita:

In [23]:
print('float(2) =', float(2))
print('int(2.5) =', int(2.5))
print('2*str(2) =', 2*str(2))
float(2) = 2.0
int(2.5) = 2
2*str(2) = 22

Colecciones

Existen muchos tipos de colecciones en Python (por ejemplo las cadenas de caracteres son secuencias, que a su vez son colecciones). Las colecciones más usadas para albergar datos de cualquier tipo son:

  • Listas
  • Tuplas
  • Diccionarios
  • Conjuntos (sets)

Listas

Una lista (objeto list) es una colección de elementos mutable, ordenada e iterable; el equivalente a arrays o vectores en otros lenguajes. Puede contener varios tipos de datos distintos a la vez, o incluso listas anidadas.

Creación:

In [24]:
l = [9, True, "texto", [1, 2]] # se pueden crear con corchetes o con la función list()
print("l =", l)
l = [9, True, 'texto', [1, 2]]

Para acceder a sus elementos usaremos los típicos corchetes. Como particularidades:

  • Podemos usar índices negativos, referidos al final de la lista.
  • Podemos hacer slicing; seleccionar una parte de la lista usando [inicio:fin] o [inicio:fin:salto]. Si se omite el inicio o el fin se cogerá desde/hasta el extremo. También se puede usar esto para modificar una parte de la lista.

NOTA: La selección y el slicing aplica también al resto de cadenas en Python; tuplas y cadenas de caracteres.

In [25]:
print("l[0] =", l[0])
print("l[-1][-2] =", l[-1][-2])
print("l[:2] =", l[:2])
print("l[::2] =", l[::2])
print("l[::-1] =", l[::-1]) # inversa
l[0] = 9
l[-1][-2] = 1
l[:2] = [9, True]
l[::2] = [9, 'texto']
l[::-1] = [[1, 2], 'texto', True, 9]

También podemos crear listas con range. En Python 3 usamos list(range()) para obtener una lista porque range es un generador (en Python 2 no lo era).

In [26]:
print(list(range(5))) # rango de 0 a 5 (5 no incluido)
print(list(range(1,5))) # rango de 1 a 5 (5 no incluido)
print(list(range(1,5,3))) # rango de 1 a 5, cogiendo 1 de cada 3 (5 no incluido)
[0, 1, 2, 3, 4]
[1, 2, 3, 4]
[1, 4]

Manipulaciones más comunes sobre listas (recordemos que son mutables):

In [27]:
l = [9, True, 'texto', [1, 2]]
print('Lista:', l)
Lista: [9, True, 'texto', [1, 2]]
In [28]:
# Reemplazamos elementos
l[0:3] = [0, 1, 2]
print('Lista después de reemplazos:', l)
print('Elementos de la lista:', *l) # Con *<lista> obtenemos los elementos
Lista después de reemplazos: [0, 1, 2, [1, 2]]
Elementos de la lista: 0 1 2 [1, 2]
In [29]:
# Añadimos elementos al final
l.append(123)
l.append(456)
l.append(789)
print('Lista después de appends:', l)
Lista después de appends: [0, 1, 2, [1, 2], 123, 456, 789]
In [30]:
# Añadimos un elemento en una posición concreta desplazando el resto
l.insert(0, 111)
print('Lista después de insert:', l)
Lista después de insert: [111, 0, 1, 2, [1, 2], 123, 456, 789]
In [31]:
# Eliminamos el último elemento y el primero.
print('l.pop() =>', l.pop())
print('l.pop(0) =>', l.pop(0))
print('Lista después de los pop():', l)
l.pop() => 789
l.pop(0) => 111
Lista después de los pop(): [0, 1, 2, [1, 2], 123, 456]
In [32]:
# También podemos eliminar elemento con del (admite l[:])
del l[len(l)-1]
print('Lista después de del:', l)
Lista después de del: [0, 1, 2, [1, 2], 123]
In [33]:
# Eliminamos la primera aparición de un elemento concreto  
l.remove(123)
print('Lista después de remove:', l)
Lista después de remove: [0, 1, 2, [1, 2]]
In [34]:
# Podemos concatenar listas
print('Resultado de concatenación:', l + [9, 8])
print('Lista l después de concatenación:', l)
Resultado de concatenación: [0, 1, 2, [1, 2], 9, 8]
Lista l después de concatenación: [0, 1, 2, [1, 2]]
In [35]:
# O extender una lista con otra (se modifica la original). Es más rápido que concatenar
l.extend(l)
print('Lista l después de extend consigo misma:', l)
Lista l después de extend consigo misma: [0, 1, 2, [1, 2], 0, 1, 2, [1, 2]]
In [36]:
# Comprobamos existencia de un elemento
print('  0 in l?:', 0 in l)
print('  Count(0):', l.count(0))
print('  index(1):', l.index(1))

# Longitud de la lista
print('  len(l):', len(l))
  0 in l?: True
  Count(0): 2
  index(1): 1
  len(l): 8

Podemos ordenar listas de cadenas o numéricas, usando list.sort() (ordena el original) o sorted(list) (devuelve copia ordenada)

NOTA: sort() es un poco más rápido y consume menos memoria; aunque sólo vale para listas, mientras que sorted() acepta cualquier iterable.

In [37]:
l_str = ['ab', 'aa']
l_str.sort()
print('Lista de cadenas ordenada: ', l_str)

l_num = [2, 3, 1]
print('Lista numérica ordenada: ', sorted(l_num))
Lista de cadenas ordenada:  ['aa', 'ab']
Lista numérica ordenada:  [1, 2, 3]

Tuplas

Las tuplas (objetos tuple) son exactamente como las listas pero inmutables; una vez creadas no se pueden modificar. Proporcionan mayor eficiencia para usos más básicos.

Para definirlas usaremos paréntesis (o nada) en lugar de corchetes. Para tuplas de 1 elemento es necesario poner una coma al final, para diferenciarlo de un elemento básico. Todo lo explicado para lectura de listas es también aplicable para tuplas; para acceder a sus elementos usaremos también corchetes.

In [38]:
t1 = (1, 2, True, "python")
print(t1)
(1, 2, True, 'python')

Podemos definir tuplas con 1 sólo elemento de dos maneras:

In [39]:
t2 = (1, )
t3 = 1,
print(t2, type(t2))
print(t3, type(t3))
(1,) <class 'tuple'>
(1,) <class 'tuple'>

Podemos extraer o desempaquetar los valores de la tupla a variables con una asignación múltiple:

In [40]:
t1a, t1b, t1c, t1d = t1
print(t1a, t1b, t1c, t1d)
print(*t1) # desempaquetado automático como argumento de una función
1 2 True python
1 2 True python
In [41]:
t1a, *t1b, t1c = t1
print(t1a, t1b, t1d)
1 [2, True] python

Existe una versión ampliada de las tuplas: los objetos namedtuple. Nos permiten asignar nombres a los elementos, para su posterior acceso. Se pueden usar como clases sencillas inmutables:

In [42]:
from collections import namedtuple
Persona = namedtuple('Persona', ['name', 'age', 'height'])
persona = Persona('Andrew', 45, 174)
persona.name
Out[42]:
'Andrew'

Diccionarios

Los diccionarios (dict) son colecciones mutables y sin orden de tipo mapeado que relacionan claves y valores; lo que se conoce en otros lenguajes como mapa. Se declara con llaves y una sucesión de pares clave : valor. La clave puede ser de cualquier tipo inmutable (incluidas las tuplas) pero única; mientras que el valor puede ser de cualquier tipo.

A los valores se accede por medio de las claves. En este caso no se puede hacer slicing.

In [43]:
# Creación:
d = {"Love Actually": "Richard Curtis", "Kill Bill": "Tarantino"}
In [44]:
# Introducción / Modificación de elemento:
d["Kill Bill"] = "Quentin Tarantino"
print('type(d)', type(d))

# Recorremos todos los elementos e imprimimos los pares clave-valor con formato
for movie, director in d.items():
    print('Movie "{}" from director: {}'.format(movie, director))
type(d) <class 'dict'>
Movie "Love Actually" from director: Richard Curtis
Movie "Kill Bill" from director: Quentin Tarantino
In [45]:
# Obtención de claves, valores e items
print("Keys:", list(d.keys()))
print("Values:", list(d.values()))
print("Items:", list(d.items()))
Keys: ['Love Actually', 'Kill Bill']
Values: ['Richard Curtis', 'Quentin Tarantino']
Items: [('Love Actually', 'Richard Curtis'), ('Kill Bill', 'Quentin Tarantino')]
In [46]:
# Comprobación de la existencia de una clave
print("Love Actually in d =", "Love Actually" in d)
Love Actually in d = True
In [47]:
# Obtener el valor perteneciente a una clave. EVITAR! si no existe la clave tendremos un KeyError
print("d['Love Actually'] =", d["Love Actually"])
# Obtener el valor perteneciente a una clave evitando excepción si no se encuentra.
print('d.get("Love Actual") = ', d.get("Love Actual"))
# Permite definir valor por defecto para ese caso
print('d.get("Love Actual", "No encontrado") = ', d.get("Love Actual", "No encontrado"))
d['Love Actually'] = Richard Curtis
d.get("Love Actual") =  None
d.get("Love Actual", "No encontrado") =  No encontrado
In [48]:
# Introducción de un nuevo elemento, sólo si la clave no existía con anterioridad
d.setdefault("Pulp Fiction", "Quentin Tarantino")
print('d.get("Pulp Fiction") after first setdefault() = ', d.get("Pulp Fiction"))

d.setdefault("Pulp Fiction", "Quentin Tarantinoak")
print('d.get("Pulp Fiction") after second setdefault() = ', d.get("Pulp Fiction"))
d.get("Pulp Fiction") after first setdefault() =  Quentin Tarantino
d.get("Pulp Fiction") after second setdefault() =  Quentin Tarantino
In [49]:
# Eliminar una entrada
del d["Pulp Fiction"]
print('d.get("Pulp Fiction") after del = ', d.get("Pulp Fiction"))
d.get("Pulp Fiction") after del =  None
In [50]:
d = {1:10, 2:13, 3:12}
print(d)

# Obtener el elemento de un diccionario con max key
print('max(d) =', max(d))

# Obtener el elemento de un diccionario con max value
print('max(d, key=d.get) =', max(d, key=d.get))
{1: 10, 2: 13, 3: 12}
max(d) = 3
max(d, key=d.get) = 2

Conjuntos

Los conjuntos (set) son exactamente como las listas pero sin orden. Se crean con el método set() o usando llaves en lugar de corchetes.

Tienen operaciones propias de los conjuntos que conocemos del mundo real: intersección, unión, diferencia, ...

In [51]:
# Dos formas de crearlos
set1 = {1, 1, 2, 3, "abc"}
set2 = set([0, 1, 2])
print('Set 1:', set1, type(set1))
print('Set 2:', set2, type(set2))

# Ojo! con llaves y vacío sería un diccionario, no un conjunto
dict = {}
print('Dict:', dict, type(dict))
Set 1: {1, 2, 3, 'abc'} <class 'set'>
Set 2: {0, 1, 2} <class 'set'>
Dict: {} <class 'dict'>
In [52]:
# Añadir elementos
set1.add(4)
set1.add(4) # sin error!
print('Set 1 después de add:', set1)

# Borrar elementos
set1.remove(4)
print('Set 1 después de remove:', set1)

# Borrar elementos sólo si existen (recomendado)
set1.discard(4)
print('Set 1 después de discard:', set1)
Set 1 después de add: {1, 2, 3, 'abc', 4}
Set 1 después de remove: {1, 2, 3, 'abc'}
Set 1 después de discard: {1, 2, 3, 'abc'}
In [53]:
# Intersección de conjuntos con &
print("set1 & set2:", set1 & set2)

# Unión con |
print("set1 | set2:", set1 | set2)

# Diferencia con -
print("set1 - set2:", set1 - set2)
print("set2 - set1:", set2 - set1)

# Diferencia simétrica con ^
print("set1 ^ set2:", set1 ^ set2)
set1 & set2: {1, 2}
set1 | set2: {0, 1, 2, 3, 'abc'}
set1 - set2: {'abc', 3}
set2 - set1: {0}
set1 ^ set2: {0, 'abc', 3}

Control de Flujo

Por un lado tenemos las sentencias condicionales, que se reducen a dos (el switch en Python se puede emular con un diccionario). Por otro lado están los bucles.

Condicionales

if... elif... else

La forma más simple de crear un condicional es con un if seguido de la condición a evaluar, dos puntos (:) y en la siguiente línea e indentado, el código a ejecutar en caso de que se cumpla dicha condición.

In [54]:
numero = 1

print("{0}:".format(numero))
if numero < 0: 
    print("Negativo")
elif numero > 0: 
    print("Positivo")
else: 
    print("Cero")
1:
Positivo

Asignación condicional

Es el equivalente al operador ternario ? en otros lenguajes.

In [55]:
var = "par" if (numero % 2 == 0) else "impar"
print(var)
impar

Bucles

while

Porción de código que se ejecuta mientras se cumpla una condición.

La instrucción break nos sirve para salir del bucle. La instrucción continue nos llevará a la siguiente ejecución del bucle.

In [56]:
edad = 15
while edad < 18: 
    edad += 1
    print("Felicidades, tienes " + str(edad))
Felicidades, tienes 16
Felicidades, tienes 17
Felicidades, tienes 18

for... in

Para iterar sobre una secuencia (cadenas, iterables como range(), d.keys(), iterators, etc). Lo bueno es que itera sobre los elementos, no sobre las posiciones.

In [57]:
# Iterando sobre una secuencia
secuencia = ['uno', 'dos', 'tres']
for elemento in secuencia: 
    print('Elemento {} leído'.format(elemento))
Elemento uno leído
Elemento dos leído
Elemento tres leído
In [58]:
# Iterando sobre un iterable
for elemento in d.keys():
    print(elemento)
type(d.keys())
1
2
3
Out[58]:
dict_keys

Podemos iterar con enumerate para acceder a la vez al elemento y al índice:

In [59]:
for i, elemento in enumerate(secuencia): 
    print('Elemento {} leído: {}'.format(i, elemento))
Elemento 0 leído: uno
Elemento 1 leído: dos
Elemento 2 leído: tres

Iteradores

Un iterador es un iterable sobre el que se puede aplicar la función __next__() para obtener el siguiente elemento, guardando el estado.

In [60]:
# El iterador se crea a partir de un iterable
iterator = iter(d.keys())
print(type(iterator))
<class 'dict_keyiterator'>
In [61]:
# Podemos acceder al siguiente elemento con iterator.__next__() o next(iterator)
print(iterator.__next__())
print(next(iterator))
1
2
In [62]:
# O podemos usarlo directamente en un bucle for, porque un iterador es un iterable
for elemento in iterator:
    print("Elemento en for: ", elemento)
Elemento en for:  3

OJO: obtendremos una excepción si intentamos obtener el siguiente elemento y no hay más.

Borrado iterativo

Para manipular los elementos de una secuencia con operaciones como puede ser el borrado, recorreremos una copia del original, de tal forma que al borrar un elemento del original seguirá estando en la copia y el bucle no se saltará nada. Para la copia real podemos hacer copia = secuencia[:] o copia = <collection>(secuencia)

In [63]:
secuencia = [1, 2, 3]
print(secuencia)
for elemento in secuencia[:]:
    secuencia.remove(elemento) # borrado sobre el original
    print(secuencia)
[1, 2, 3]
[2, 3]
[3]
[]

¿Qué ocurre si no trabajamos con una copia? al borrar el 1º, el 2º pasa a ser el 1º, por lo que no procesamos el 2º

In [64]:
secuencia = [1, 2, 3]
print(secuencia)
for elemento in secuencia:
    secuencia.remove(elemento)
    print(secuencia)
[1, 2, 3]
[2, 3]
[2]

Excepciones

En python se usa una construcción try-except-else-finally para capturar y tratar las excepciones.

In [65]:
# Ejemplo:
try:
    num = int("3a")
    print("Hecho!")
except (NameError, ValueError) as e:
    print("La variable no es correcta")
except:
    print("Error")
else:
    print("Esto se ejecuta cuando no hay excepciones")
finally:
    print("Esto se ejecuta siempre")
La variable no es correcta
Esto se ejecuta siempre

Más adelante veremos cómo crear nuestras propias excepciones.

Podemos usar assert si queremos testear algo y en caso de que no se cumpla automáticamente se lance una excepción de tipo AssertionError.

In [66]:
assert len(l) > 0, "La secuencia debe contener elementos" # Raises exception if empty

Para lanzar una excepción de forma manual usaremos raise():

In [67]:
try:
    raise(Exception)
except:
    print("Excepción!")
Excepción!

Funciones

Se conoce así a los fragmentos de código con nombre que devuelven un resultado. Se usa def para definirlas y return para devolver valores o tuplas. Si no especificamos un valor de retorno, la función devolverá por defecto None (el equivalente en Python para null).

In [68]:
# Definimos una función de primer orden, con un valor por defecto para el 2º parámetro
def imprimir1(texto, veces=1):
    """Esta funcion imprime los dos valores pasados 
    como parametros""" # Docstring: lo que imprime el operador ? de Python o la función help
    
    print(texto*veces)
In [69]:
# Ejecutamos la función de varias formas posibles
imprimir1("hola")
imprimir1("hola ", 2)
res = imprimir1(veces = 2, texto = "hola ")
print(res)
hola
hola hola 
hola hola 
None

Podemos crear una función con un número variable de parámetros posicionales o sin clave, precediendo el último de un asterisco (*args). Eso rellenará una tupla con los valores pasados:

In [70]:
def imprimir2(texto, veces=1, *otros):
    print(texto*veces, *otros)
    
imprimir2("hola", 2, "mario", "luis")
holahola mario luis

También existe la opción de poner dos asteriscos (**kwargs) para recibir un número indeterminado de parámetros con clave. Esto rellenará un diccionario en lugar de una tupla.

In [71]:
def imprimir3(texto, veces=1, **otros):
    print(texto*veces, *list(otros.values()))
    
imprimir3("hola", 2, invitado1 = "mario", invitado2 = "luis")
holahola mario luis

Podemos combinar ambas opciones. Pondremos primero los parámetros posicionales y luego los que van con clave.

In [72]:
def imprimir4(*args, **kwargs):
    print(*args, *list(kwargs.values()))

args1 = (1, 2, 3)
args2 = {'a': 11, 'b': 12}
imprimir4(*args1, **args2)
1 2 3 11 12

De forma análoga podemos detallar los argumentos y desempaquetarlos de una tupla o un diccionario al llamar a la función.

In [73]:
def func(a, b):
    print(a, b)

t = (0, 1)
func(*t)

d = {'a': 1, 'b': 2}
func(**d)
0 1
1 2

En Python a veces se dice que las variables mutables se pasan a las funciones como referencia, y las inmutables como valor; basándose en que las primeras se pueden modificar dentro de la función teniendo efecto fuera, y las segundas no. Esto puede llevar a confusión. En Python no existe el paso como referencia per se. Lo probamos con una función que intente intercambiar 2 variables:

In [74]:
def swap(a, b):
    (a, b) = (b, a) # swap sencillo en Python
    return (a, b)
In [75]:
# Probamos con variables inmutables
a, b = 1, 2
print(swap(a, b))
print(a, b)
(2, 1)
1 2
In [76]:
# Ahora probamos con variables mutables (listas)
a, b = [1], [2]
print(swap(a, b))
print(a, b)
([2], [1])
[1] [2]

Como podemos comprobar no funciona en ningún caso (las variables se mantienen igual fuera de la función). Pero si queremos conseguir el efecto del paso de argumentos como referencia, tenemos varias opciones:

  1. devolver una tupla y reasignarla a las variables originales
  2. usar variables globales (no recomendado)
  3. pasar como parámetro una variable que referencie a un objeto mutable. Podremos cambiar su contenido (si no cometemos el error de reasignar la variable a otro objeto):
In [77]:
def append_0(l):
    l.append(0)
    return l

l=[]
print(append_0(l))
print(append_0(l))
print(l)
[0]
[0, 0]
[0, 0]

Más adelante veremos funciones de orden superior.

Orientación a objetos

Partimos de que en Python todo es un objeto.

Comparación de objetos

Para comparar el valor de 2 objetos usamos el operador ==, mientras que para comparar su identidad usaremos is

In [78]:
# Variables inmutables
a = 257
b = 257
print('a == b: ', a == b)
print('a is b: ', a is b)
a == b:  True
a is b:  False
In [79]:
# Excepción: enteros pequeños (se cachean)
a = 256
b = 256
print('a == b: ', a == b)
print('a is b: ', a is b)
a == b:  True
a is b:  True
In [80]:
# Variables mutables
a = [1]
b = [1]
print('a == b: ', a == b)
print('a is b: ', a is b)
a == b:  True
a is b:  False

Copia de objetos

Cuando hacemos una copia de un objeto inmutable, obtenemos una copia real o "profunda" del mismo.

In [81]:
a = 1
b = a
a += 1
b
Out[81]:
1

Cuando hacemos una copia de un objeto mutable en Python, obtenemos una copia de la referencia a su espacio en memoria. Esto se conoce como copia "superficial".

In [82]:
ids = [1, 2, 3]
ids2 = ids
ids.append(4)

print(ids2)
[1, 2, 3, 4]
In [83]:
# Ejemplo de cómo crear una copia real o "profunda" de un objeto mutable
ids = [1, 2, 3]
ids2 = ids.copy() # o ids[:] por ser una lista
ids.append(4)

print(ids2)
[1, 2, 3]

Clases e instancias

Las clases en Python se declaran como en el siguiente ejemplo. Distinguiremos entre atributos de clase y variables de instancia, y presentamos tres tipos de métodos (de instancia, de clase, estáticos):

In [84]:
class Coche: 
    """Abstraccion de los objetos coche.""" # docstring
    
    ruedas = 4  # atributo público de clase, compartido por todas sus instancias
    
    # Método especial (constructor). Función que se ejecuta al instanciar un nuevo objeto de la clase.
    def __init__(self, marca):
        self.marca = marca # argumento del objeto -> atributo de la instancia
    
    # Métodos de instancia (self,). Acceden a cosas de la instancia por medio de self.( )
    def get_marca(self): 
        return self.marca
        
    def pintar(self, color): 
        print("Pintar de ", color)
      
    # Método de clase (cls,). Compartido con todas las instancias. Acceden a cosas de la clase por medio de cls.( )
    @classmethod
    def get_ruedas(cls):
        return cls.ruedas
        
    # Método estático (). No tiene acceso a la instancia o el objeto.
    @staticmethod
    def cerrar():
        print("Cerrado")

El primer parámetro de todos los métodos de instancia será self, aunque no hay que escribirlo al hacer las llamadas, ya que lo pone Python automáticamente. Los atributos de la clase serán accedidos desde la propia clase como self.variable y se pueden modificar dentro de cualquier función.

In [85]:
# Creación de un objeto, instancia de la clase
mi_coche = Coche('Seat')

# Acceso a atributo de clase
print('Ruedas de mi_coche:', mi_coche.ruedas)
print('Ruedas de Coche:', Coche.ruedas)

# Acceso a método de clase
print('Ruedas de mi_coche:', mi_coche.get_ruedas())
print('Ruedas de Coche:', Coche.get_ruedas())

# Acceso a variable de instancia
print("Marca de mi_coche:", mi_coche.get_marca()) # Coche.get_marca() => error
print("Marca de mi_coche:", mi_coche.marca)

# Acceso a método estático
mi_coche.cerrar()
Coche.cerrar()

print(type(Coche)) #Object
print(type(mi_coche)) #Coche
Ruedas de mi_coche: 4
Ruedas de Coche: 4
Ruedas de mi_coche: 4
Ruedas de Coche: 4
Marca de mi_coche: Seat
Marca de mi_coche: Seat
Cerrado
Cerrado
<class 'type'>
<class '__main__.Coche'>

Los métodos especiales son aquellos métodos privados que Python nos proporciona para cada objeto, y cuyo nombre empieza y acaba por __. Los podemos usar de forma interna. A continuación una lista de los métodos especiales disponibles en una clase de nueva creación.

In [86]:
class NuevaClase:
    pass

# Podemos imprimir las funciones de cualquier objeto con dir()
print(dir(NuevaClase))
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

Para evitar inicializaciones y liberar memoria en una tarea típica (por ejemplo leer de un fichero) se usa la orden with. Python ejecutará el método __enter__() del objeto obtenido antes del bloque a continuación, y __exit__() al acabar dicho bloque. Un ejemplo con el objeto file que ya tiene esos métodos implementados:

In [87]:
try:    
    with open('file.txt', 'r') as f:
        for line in f:
            print(line)
except FileNotFoundError:
    pass

Herencia

Para indicar que una clase hereda de otra se coloca el nombre de la clase padre entre paréntesis cuando declaramos la clase hija.

En Python lo más destacado es que se permite la herencia múltiple, por lo que no son necesarias las interfaces como en otros lenguajes.

La clase hija hereda los atributos y las funciones de las clases padre referenciadas (pudiendo sobrescribirlos), y si hay alguna función cuyo nombre se repita en varias clases padre, tendrá preferencia la primera clase que aparece en la declaración.

Si la clase hija no define un método __init__(), se llamará automáticamente al de la clase padre; aunque lo adecuado es definirlo y llamarlo explícitamente.

In [88]:
# Clase padre
class Instrumento:
    def __init__(self, nombre):
        self.nombre = nombre
    def get_nombre(self):
        return self.nombre

# Clase hija
class Bateria(Instrumento):
    def __init__(self, nombre, platillos):
        super().__init__(nombre)              # si sólo hay una clase padre
        # Instrumento.__init__(self, nombre)  # si hay más de una clase padre
        self.platillos = platillos
        
mi_bateria = Bateria("batera", 13)
mi_bateria.get_nombre()
Out[88]:
'batera'

Podremos comprobar en cualquier momento si un objeto pertenece realmente a la clase hija o a la padre usando type:

In [89]:
if isinstance(mi_bateria, Instrumento):
    print('isinstance: Instrumento')
if isinstance(mi_bateria, Bateria):
    print('isinstance: Bateria')
if type(mi_bateria) is Instrumento:
    print('type: Instrumento')
if type(mi_bateria) is Bateria:
    print('type: Bateria')
isinstance: Instrumento
isinstance: Bateria
type: Bateria

Notas sobre polimorfismo y encapsulamiento

No existe sobrecarga de métodos en la misma clase debido al tipado dinámico de Python. De intentarse usar como en otros lenguajes, el último método definido sobrescribiría los anteriores. Pero podemos conseguir el mismo efecto jugando con parámetros de longitud variable, valores por defecto de los mismos y decoradores.

No existen los modificadores de acceso. Lo que hace Python es considerar privada toda aquella función o variable que empiece por __ (siempre que no acabe igual, siendo entonces una función especial), o protegida en caso de ir precedida por un único _. En el resto de casos la función o variable será pública.

Excepciones propias

En Python podemos crear (y lanzar) nuestras propias excepciones. Basta con crear una clase que herede de Exception o cualquiera de sus hijas. Las excepciones como ya hemos visto antes se crean con raise y se recogen con except:

In [90]:
class MiError(Exception):
    def __init__(self, valor):
        self.valor = valor
    def __str__(self):
        return "Error " + str(self.valor)

try:
    if 22 > 20:
        raise MiError(33)
except MiError as e:
    print(e) # o por ejemplo pass si no queremos tratarla
Error 33

Metaclases

¿Qué es una metaclase? Pues es una clase cuyas instancias son clases en lugar de objetos. Es decir; si para construir un objeto se usa una clase, para construir una clase se utiliza una metaclase (por defecto type).

In [91]:
 mi_coche = type('Coche',(),{'gasolina':3})
 print(mi_coche.gasolina)
 print(type(mi_coche))
3
<class 'type'>

Módulos y paquetes

Módulos

Los módulos son entidades que permiten organizar y dividir lógicamente nuestro código cuando tenemos programas demasiado grandes. Los ficheros son su equivalente en el mundo físico.

Para usar la funcionalidad definida en un módulo, tendremos que importarlo con import + nombre del módulo sin extensión de fichero. Esto no sólo deja la funcionalidad disponible, sino que ejecuta dicho módulo. Podemos escribir varios módulos separados por comas en la instrucción import.

Para usar funciones de los módulos importados, habrá que antecederlas del nombre del módulo y un punto. O podemos usar from [module] import [function] para importar el objeto al espacio de nombres actual y así ahorrarnos escribir el nombre del módulo. También es posible usar from [module] import * pero se desaconseja.

El atributo __doc__ nos sirve para documentar el módulo.

Opciones de importación

from math import *          # NO!!!!! importa todas las funciones del módulo
import math                 # importa el módulo; ejecución como math.sqrt()
import math as M            # importa el módulo usando alias; ejecución como M.sqrt()
from math import sqrt, cos  # SI!!!!! importa funciones concretas del módulo

Para importar módulos en otro directorio distinto al nuestro, deberemos tenerlos disponibles en la variable PYTHONPATH. Podemos consultar el contenido del path en python ejecutando lo siguiente:

In [92]:
import sys
print(sys.path)
['C:\\Users\\yago_\\Dropbox\\DEV\\projects\\notebooks\\python3-101', 'C:\\Users\\yago_\\Miniconda3\\envs\\py37\\python37.zip', 'C:\\Users\\yago_\\Miniconda3\\envs\\py37\\DLLs', 'C:\\Users\\yago_\\Miniconda3\\envs\\py37\\lib', 'C:\\Users\\yago_\\Miniconda3\\envs\\py37', '', 'C:\\Users\\yago_\\Miniconda3\\envs\\py37\\lib\\site-packages', 'C:\\Users\\yago_\\Miniconda3\\envs\\py37\\lib\\site-packages\\win32', 'C:\\Users\\yago_\\Miniconda3\\envs\\py37\\lib\\site-packages\\win32\\lib', 'C:\\Users\\yago_\\Miniconda3\\envs\\py37\\lib\\site-packages\\Pythonwin', 'C:\\Users\\yago_\\Miniconda3\\envs\\py37\\lib\\site-packages\\IPython\\extensions', 'C:\\Users\\yago_\\.ipython']

Podemos imprimir las funciones y atributos de un módulo usando dir()

In [93]:
print(dir(math))
['__doc__', '__loader__', '__name__', '__package__', '__spec__', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'ceil', 'copysign', 'cos', 'cosh', 'degrees', 'e', 'erf', 'erfc', 'exp', 'expm1', 'fabs', 'factorial', 'floor', 'fmod', 'frexp', 'fsum', 'gamma', 'gcd', 'hypot', 'inf', 'isclose', 'isfinite', 'isinf', 'isnan', 'ldexp', 'lgamma', 'log', 'log10', 'log1p', 'log2', 'modf', 'nan', 'pi', 'pow', 'radians', 'remainder', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'tau', 'trunc']

Los módulos (ficheros) son también objetos, por lo que pueden tener sus atributos y sus métodos. El atributo __name__ se usa de forma habitual en un módulo para comprobar si se está ejecutando el módulo como importado, o bien como programa principal (en cuyo caso __name__ será igual a "__main__", pudiendo ejecutar condicionalmente una parte del código):

In [94]:
print("Ésto se imprime siempre")

if __name__ == "__main__":
    print("Ésto se imprime sólo cuando la ejecución no es mediante import")
Ésto se imprime siempre
Ésto se imprime sólo cuando la ejecución no es mediante import

Paquetes

Los paquetes sirven para organizar los módulos. En realidad ambos son tipos especiales de módulos (module). Los paquetes se representan físicamente como directorios.

Para hacer que python trate un directorio como un paquete es necesario crear un fichero __init__.py dentro del mismo. En dicho fichero se definen elementos que pertenezcan al paquete, aunque basta con meter un módulo en el directorio para que esté disponible.

Como se trata de módulos, para tenerlos disponibles se usa import:

In [95]:
# import paquete.subpaquete.modulo

# modulo.func()

Programación funcional

La programación funcional es un paradigma en el que el código se basa casi en su totalidad en funciones, entendiendo el concepto de función según su definición matemática, y no como los simples subprogramas de los lenguajes imperativos que podamos haber visto hasta ahora. El concepto de variables desaparece, y las funciones no tienen efectos colaterales; el resultado de ejecutar una función 2 veces con la misma entrada será el mismo, con todas las ventajas que eso supone.

Python cuenta con varias características de este paradigma.

Funciones de orden superior

Las funciones en Python son de primera clase o de orden superior. Como todo en Python las funciones son objetos: pueden asignarse a una variable o guardarse en una estructura, y pueden pasarse como parámetro a otras funciones, o devolverse como resultado de las mismas.

In [96]:
# Ejemplo
def crear_suma(x):
    def suma(y):
        return x + y
    return suma

suma_10 = crear_suma(10)

print(suma_10(3))
13
In [97]:
# Ejemplo más práctico
def saludar(lang):
  
    def saludar_es():
        print("Hola")
    def saludar_en():
        print("Hello")
    def saludar_it():
        print("Ciao")

    lang_func = {"es": saludar_es,
                 "en": saludar_en,
                 "it": saludar_it}
    
    return lang_func[lang]
  
f = saludar("es") # devuelve una función
f() # ejecutamos la función

# Podríamos simplificar escribiendo: saludar("es")()
Hola

Iteraciones de orden superior sobre listas

Podemos pasar nuestras funciones de orden superior como argumentos a las funciones del core map y filter, u otras como reduce. Estas funciones nos permiten precindir de los bucles típicos de otros lenguajes.

map(function, iterable[, iterable, ...])

Devuelve una secuencia (objeto map) con el resultado de aplicar una función a cada elemento de un iterable (o varios iterables, uno a uno). Si se pasan como parámetros n iterables, la función tendrá que aceptar n argumentos. Si alguna de las secuencias es más pequeña que las demás, el valor que le llega a la función para posiciones mayores que el tamaño de dicha secuencia será None.

In [98]:
# Ejemplo
def cuadrado(n):
    return n ** 2
  
l = [1, 2, 3]
l2 = map(cuadrado, l)

print(l2)
print(list(l2))
<map object at 0x000001C5160FF108>
[1, 4, 9]
In [99]:
# Ejemplo con 2 iterables
def concat_zip(a, b):
  return a + b

x = map(concat_zip, ('apple', 'banana', 'cherry'), ('orange', 'lemon', 'pineapple')) 
print(list(x))
['appleorange', 'bananalemon', 'cherrypineapple']
In [100]:
# Ejemplo: convertir lista de números en lista de caracteres
print(list(map(str, l)))
['1', '2', '3']

filter(function, iterable)

Devuelve una secuencia con los elementos del iterable para los que la función devuelve True.

In [101]:
# Ejemplo
def es_par(n):
    return (n % 2.0 == 0)

l = [1, 2, 3, 4, 5, 6, 7]
f = filter(es_par, l)
print(f)
print(list(f))
<filter object at 0x000001C516110E88>
[2, 4, 6]

reduce(function, iterable[, initial])

Devuelve el resultado (un valor) de ir aplicando una función a pares de elementos de un iterable. La función aceptará 2 parámetros; el primero es el valor acumulado de la ejecución anterior (initial si es la primera) y el segundo es el elemento actual del iterable. En Python 3 forma parte de functools

In [102]:
from functools import reduce

def multiplica(x, y):
    return x * y

print(reduce(multiplica, [1, 2, 3, 4, 5]))
120

Funciones lambda

Las funciones lambda son funciones anónimas o temporales, que no podrán ser referenciadas más adelante.

Se construyen mediante el operador lambda, los parámetros de la función separados por comas (SIN paréntesis), dos puntos (:) y el código de la función.

In [103]:
# Ejemplo simple
print((lambda x: x % 2)(5))
1
In [104]:
# Ejemplo con filter
lista = [1, 2, 3]
print(list(filter(lambda n: n % 2.0 == 0, lista)))

# La función lambda equivale a:
def lambda_function(n):
    return n % 2.0 == 0
[2]
In [105]:
# Ejemplo con sort (función que ordena un diccionario atendiendo a una clave):
points = [{"x": 2, "y": 3}, {"x": 4, "y": 1}]
points.sort(key=lambda i: i["y"]) # [{"y":1, ..}, {"y":3, ..}]
print(points)
[{'x': 4, 'y': 1}, {'x': 2, 'y': 3}]

Comprensión de listas

Construcción que permite crear listas a partir de otras listas. También es aplicable a otros iterables, aunque su uso más habitual es con listas.

Su estructura puede seguir dos patrones:

 [function(x) for x in iterable [if condition]]

 [function(x) if condition [else operation2] for x in iterable]

Cada una de estas construcciones consta de una expresión que determina cómo modificar el elemento de la lista original, seguida de una o varias cláusulas for y opcionalmente una o varias cláusulas if.

Ejemplos

In [106]:
print(l)
[1, 2, 3, 4, 5, 6, 7]
In [107]:
# Ejemplo equivalente a map
print([n ** 2 for n in l])
[1, 4, 9, 16, 25, 36, 49]
In [108]:
# Ejemplo equivalente a filter
print([n for n in l if n % 2.0 == 0])
[2, 4, 6]
In [109]:
# Ejemplo con if-else
print([n if n % 2 == 0 else 0 for n in l]) # cambia los impares por 0
[0, 2, 0, 4, 0, 6, 0]
In [110]:
# Ejemplo con doble for para obtener las combinaciones que suman 10
print(sum([1 if l[i] + l[j] == 10 else 0 for i in range(len(l)) for j in range(i+1,len(l))]))
2

Comprensión de conjuntos

In [111]:
nombres = ['jaime', 'yago', 'iago', 'tiago', 'diego', 'jacobo', 'iacobus', 'santiago']
longitudes = {len(nombre) for nombre in nombres}
print(longitudes)
{4, 5, 6, 7, 8}

Comprensión de diccionarios

In [112]:
name_lengths = {nombre: len(nombre) for nombre in nombres}
print(name_lengths)
{'jaime': 5, 'yago': 4, 'iago': 4, 'tiago': 5, 'diego': 5, 'jacobo': 6, 'iacobus': 7, 'santiago': 8}

Generadores

Los generadores son una herramienta de programación perezosa, con una expresión similar a la de la comprensión de listas (de hecho se escriben igual que éstas pero usando paréntesis en lugar de corchetes). La diferencia es que no devuelven una lista, sino un generador.

Un generador es un tipo especial de función que genera bajo demanda valores sobre los que iterar. Para devolver el siguiente valor sobre el que iterar se usa la palabra clave yield en lugar de return. Para iterar sobre el generador se usa por ejemplo un for...in

Como no se llega a crear una lista en memoria, sino que se generan valores y se consumen, estamos ahorrando recursos; algo que notaremos con grandes cantidades de datos. No obstante podemos crear una lista a partir de un generador gracias a la función list().

In [113]:
# Ejemplo
def mi_generador(n, m, s):
    while(n <= m):
        yield n
        n += s
        
for n in mi_generador(0, 7, 2):
    print(n)
0
2
4
6

Clausuras

Una clausura (closure) es un mecanismo para llamar a una función interna que tiene acceso al scope de su función contenedora

In [114]:
# Ejemplo de función para calcular la media de una serie
def construir_calculadora_media():
    series = [] # variable local en el ámbito de la función outer, accesible desde la inner
    def calcular_media(valor):
        series.append(valor)
        return sum(series)/len(series)
    return calcular_media

calcular_media = construir_calculadora_media() # clausura de la función inner
print(calcular_media(10))
print(calcular_media(15))
print(calcular_media(20))
10.0
12.5
15.0

Decoradores

Un decorador es una función que recibe otra función como parámetro y extiende el comportamiento de aquella sin modificarla. Devuelve una función interna como resultado.

Puede verse como un recubrimiento para funciones; útil por ejemplo para debugging, ejecución con reintentos, tratamiento de excepciones, etc.

In [115]:
# Ejemplo
def mi_decorador(funcion):
    def nueva(*args):
        print("Llamada a la función", funcion.__name__)
        return funcion(*args)
    return nueva
  
def imprimir5(texto):
    print(texto)

mi_decorador(imprimir5)("hola!")
Llamada a la función imprimir5
hola!

Si queremos algo más limpio y que el decorador se aplique siempre a la función, lo escribiremos como una anotación delante:

In [116]:
@mi_decorador
def imprimir6(texto):
    print(texto)

imprimir6("hola!")
Llamada a la función imprimir6
hola!