martes, 23 de marzo de 2021

Las listas en Python



una_lista = ['peras', 'manzanas', 'zanahorias']

Las Listas. 


Usamos las listas para guardar múltiples elementos en una sola variable.

Las listas son uno de los cuatro elementos que se usan para almacenar datos, los otros son las Tuplas, los Conjuntos y los Diccionarios.

Las listas se crean usando corchetes.

nombre_lista = [elemento1, elemento2,....., elemento_n]

Los elementos de una lista están ordenados, se pueden modificar y permiten valores duplicados.

Al estar ordenados podemos acceder a cualquier elemento de la lista indicando su posición en la misma. Asi empezando por la izquierda, el primer elemento tiene el indice [0], el segundo [1] y así sucesivamente. Este orden no cambia de forma que si añadimos nuevos elementos a la lista estos se situarán al final de la misma.

Veamos algunos ejemplos de como acceder a una lista usando una lista con números.

>>> lista = [1,2,3,4,5,6,7,8,9,10]
>>> # Si queremos por ejemplo imprimir el numero 4 (cuyo índice es el 3)
>>> print(lista[3])
4

Si quisiéramos imprimir toda la lista podemos usar:

>>> print(lista)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> # o también [:] lo que quiere decir que imprima desde el primer al ultimo elemento de la lista >>> print(lista[:]) [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Si quisiéramos imprimir o usar solo una parte o porción de la lista (Slicing o rebanadas en python), usamos la expresión:

lista[desde_posicion, hasta posición sin incluir ese elemento, salto]

>>> # Imprimir los 5 primeros números
>>> print(lista[0:5])
[1, 2, 3, 4, 5]
>>> # Imprimir desde el 6 al final.
>>> print(lista[5:10])
[6, 7, 8, 9]
>>> print(lista[5:])
[6, 7, 8, 9, 10]
>>> # Imprimir cada dos números desde el primer al ultimo elemento
>>> print(lista[::2])
[1, 3, 5, 7, 9]


También podemos indicar el índice del elemento empezando a contar desde la izquierda, en ese caso utilizaremos el índice con el signo menos y empezando por el -1 y no por el cero como hicimos antes. Podemos combinar ambas formas de indicar el índice de los elementos.

>>> # Imprimir los 5 primeros números
>>> print(lista[-10:-5])
[1, 2, 3, 4, 5]
>>> # Imprimir desde el 6 al final.
>>> print(lista[-5:10])
[6, 7, 8, 9]
>>> print(lista[-5:])
[6, 7, 8, 9, 10]
>>> # Imprimir cada dos números desde el último al primero
>>> print(lista[::-2])
[10, 8, 6, 4, 2]
>>> # Invertir el orden de los elementos de una lista.
>>> print(lista[::-1])
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]


Longitud de una lista.


Para saber cuantos elementos tiene una lista usaremos la función len()

Ejemplo

Imprimimos el tamaño o número de elementos de una lista:

>>> lista = ["manzana""platano""pera"]
>>> print(len(lista))
3


Tipos de datos que pueden formar una lista.


Los elementos que se pueden almacenar en una lista pueden ser.....CUALQUIERA. Pueden ser números, strings o cadenas de texto, booleanos o incluso una combinación de todos ellos.

lista_string = ["manzana""platano""pera"]
lista_numeros = [15793]
lista_booleanos = [TrueFalseFalse]
lista_combinada = [True, 5, "manzana"]

También puede haber una lista con otra u otras listas dentro:

>>> lista = ["manzana", "platano", "pera", [1, 2, 3, 4]]
>>> print(lista[len(lista)])
4

Como ves la lista tiene 4 elementos, los tres primeros son frutas y el cuarto es otra lista. Si queremos acceder a un elemento de esta última lista es muy sencillo. Primero indicamos su índice en la lista principal y luego otra vez el elemento al que queremos acceder dentro de la sub-lista de números. Por ejemplo para imprimir el nº 3 de la lista de números:

>>> lista = ["manzana", "platano", "pera", [1, 2, 3, 4]]
>>> print(lista[3][2])
3
# [Elemento con indice 3 de la lista principal] y [2 de la sublista]


Desde la perspectiva de Python las listas son definidas como objetos cuyo tipo de datos es "list"

>>> lista = ["manzana", "platano", "pera", [1, 2, 3, 4]]
>>> print(type(lista))
<class 'list'>


y también es posible usar el constructor "list()" para crear una lista a partir de otro tipo de objeto:

>>> palabra ="Bienvenido"
>>> print(list(palabra))
['b', 'i', 'e', 'n', 'v', 'e', 'n', 'i', 'd', 'o']


Métodos que podemos usar para trabajar con listas.


*)     nombre_lista.append(elemento)        Añade un elemento al final de la lista.

>>> lista = ['uno','dos','tres']
>>> item = 'cuatro'
>>> lista.append(item)
>>> print(lista)
['uno', 'dos', 'tres', 'cuatro']
Otra forma usando slicing de listas es la siguiente:

>>> lista = ['uno','dos','tres']
>>> item = ['cuatro']
>>> lista[len(lista):]=item
>>> print(lista)
['uno', 'dos', 'tres', 'cuatro']

Aunque quizá la forma más fácil de fusionar o añadir una lista a continuación de otra es usar las suma "+", vamos sumando ambas listas:

>>> lista = ['uno','dos','tres']
>>> item = ['cuatro']
>>> lista = lista + item
>>> print(lista)
['uno', 'dos', 'tres', 'cuatro']

y ya que estamos con los signos matemáticos si usas el signo de multiplicar con una lista la multiplicas n veces:

>>> lista = ['uno','dos'] * 3
>>> print(lista)
['uno', 'dos', 'uno', 'dos', 'uno', 'dos']


*)     nombre_lista.insert(i, item)                 Añade el item en la posición i desplazando al resto hacia la derecha.

>>> # añadimos el cuatro donde estaba el dos, es decir en la posición 1.
>>> lista = ['uno','dos','tres']
>>> item = 'cuatro'
>>> lista.insert(1,item)
>>> print(lista)
['uno', 'cuatro', 'dos', 'tres']


*)     nombre_lista.extend(otra_lista)            Fusiona o añade una lista a la anterior.

>>> lista = ['uno','dos','tres']
>>> items = ['cuatro','cinco']
>>> lista.extend(items)
>>> print(lista)
['uno', 'dos', 'tres', 'cuatro', 'cinco']

otro ejemplo:

>>> lista = [1,2,3,4]
>>> lista.extend(range(5,11))
>>> print(lista)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]


En el ejemplo he utilizado la función range() que lo que hace es crear una lista de números secuenciales.

Su estructura es:

 range(nº inicio, nº final sin incluir, salto)


*)       nombre_lista.index(elemento)                 Nos devuelve el índice o posición del primer elemento de la lista que coincida con el elemento dado.

>>> lista = ["manzana", "platano", "pera", "platano"]
>>> print(lista.index("manzana"))
0
>>> print(lista.index("platano"))
1
>>> print(lista.index("pera"))
2


*)     nombre_lista.pop(<indice_elemento>)       Modifica la lista original, eliminando el elemento de la misma que esté en la posición indicada y lo asigna a una variable si es el caso. El argumento del método es opcional y si no pones nada se entiende que es el último elemento de la lista.

>>> lista_original = ["manzana", "platano", "pera", "platano"]
>>> lista_original.pop() # quita el ultimo elemento de la lista modificándola.
>>> print(lista_original)
['manzana', 'platano', 'pera']
x= lista_original.pop(1) # quitamos el elemento de la posición 1 y lo asignamos a x.
>>> print(x)
platano
>>> print(lista_original) # Como queda la lista después de las modificaciones.
['manzana', 'pera']


Con esto podemos utilizar cosas que se encuentran en otros lenguajes de programación como son las pilas y colas de datos. 

En las pilas se sigue el método LIFO (Last In First Out), es decir, los últimos datos que entran en las listas son los primeros en salir. Utilizaremos los métodos append() y pop(). Veámoslo con este ejemplo. Tenemos un almacén  nos traen la mercancía en cajas. Y la vamos vendiendo primero las últimas que nos traen.

>>> lista_cajas = [5,2,6,7]
>>> # Nos traen otras dos cajas y las metemos en el stock
>>> lista_cajas.append(2)
>>> print(lista_cajas)
[5, 2, 6, 7, 2]
>>> # Ahora sacamos 9 del almacén utilizando el método de la pila. (7, 2)
>>> lista_cajas.pop()
>>> list_cajas.pop()
>>> print(lista_cajas)
[5, 2, 6]


En las colas pasa lo contrario. Va saliendo de la variable primero lo que entro antes. Utilizaremos los métodos insert(0, item) y pop(). Veámoslo con un ejemplo.

>>> lista_cajas = [5,2,6,7]
>>> # Nos traen otras dos cajas y las metemos en el stock
>>> lista_cajas.insert(0, 2)
>>> print(lista_cajas)
[2, 5, 2, 6, 7]
>>> # Ahora sacamos 7 del almacén utilizando el método de la pila. (7)
>>> lista_cajas.pop()
>>> print(lista_cajas)
[2, 5, 2, 6]


*)   nombre_lista.count(item)           Cuenta las veces que un objeto está en una lista.

>>> lista=[2, 5, 2, 6, 7, 2]
>>> print(lista.count(2))
3


*) nombre_lista.remove(item)            Elimina el primer elemento que coincide de una lista.     

>>> lista=[2, 5, 2, 6, 7, 2]
>>> lista.remove(2)
>>> print(lista)
[5, 2, 6, 7, 2]


Para eliminar un elemento de una lista también podemos usar su posición con la instrucción:

del lista[indice_elemento]

>>> lista=[2, 5, 2, 6, 7, 2]
>>> # Borramos el primer 2 de la lista que ocupa la posición 0.
>>> del lista[0]
>>> print(lista)
[5, 2, 6, 7, 2]


 *)  nombre_lista.clear()            Vacía la lista de elementos.

>>> lista=[2, 5, 2, 6, 7, 2]
>>> # Vaciamos todos los elementos de la lista.
>>> lista.clear()
>>> print(lista)
[]     


Podemos conseguir lo mismo también con las siguientes instrucciones:

>>> lista=[2, 5, 2, 6, 7, 2]
>>> # Vaciamos todos los elementos de la lista.
>>> lista=[]
>>> print(lista)
[]
# O también podemos vaciarla entera o solo una parte usando
# del y usando slicing de listas.
>>> lista=[2, 5, 2, 6, 7, 2]
>>> del lista[:]
>>> print(lista)
[]


*)     nombre_lista.reverse()        Devuelve la misma lista pero al revés.

>>> lista=[2, 5, 2, 6, 7, 2]
>>> lista.reverse()
>>> print(lista)
[2, 7, 6, 2, 5, 2]
# lo mismo lo podríamos conseguir con lista[::-1]


*)     nombre_lista.sort()            Ordena solamente los elementos de una lista. Por defecto modifica la lista orignal y lo hace en orden creciente, a no ser que como argumento usemos reverse=True que lo haría en orden decreciente.

>>> lista=[2, 5, 2, 6, 7, 2]
>>> lista.sort() # Orden creciente
>>> print(lista)
[2, 2, 2, 5, 6, 7]
>>> lista.sort(reverse=True) # Orden decreciente
[7, 6, 5, 2, 2, 2]


Opcionalmente también podríamos usar la función sorted(), que no solo puede ordenar listas sino cualquier otro iterable. Además sorted() no modifica la lista original, sino que devuelve la lista ordenada. 

>>> lista=[2, 5, 2, 6, 7, 2]
>>> x = sorted(lista)
>>> print(x)
[2, 2, 2, 5, 6, 7]
>>> print(lista)
[2, 5, 2, 6, 7, 2]


*)     nombre_lista.copy()         Devuelve una copia de esa lista. Si simplemente asignamos una lista a una variable no estamos copiando la lista en otra independiente sino solo haciendo una referencia a la misma. Por tanto si lo que queremos es copiar una lista tenemos que hacer uso del método .copy()

>>> lista=[2, 5, 2, 6, 7, 2]
>>> x = lista # Esto no copia la lista en una independiente, solo hacemos una referencia
a la misma.
>>> x.append(9)
>>> print(lista)
[2, 5, 2, 6, 7, 2, 9]
>>> z = lista.copy() # Crea una verdadera copia independiente de la lista.
>>> print(z)
[2, 5, 2, 6, 7, 2, 9]


Otras funciones y otras cosas que se pueden aplicar a una lista.


*)     sum(lista)            Devuelve la suma de los elementos de la lista.

>>> lista = [2, 5, 2, 6, 7, 2, 9]
>>> print(sum(lista)) 
33


*)     max(lista)            Devuelve el valor máximo de una lista.

>>> lista = [2, 5, 2, 6, 7, 2, 9]
>>> print(max(lista)) 
9


*)    min(lista)            Devuelve el valor mínimo de una lista.

>>> lista = [2, 5, 2, 6, 7, 2, 9]
>>> print(min(lista)) 
2


Cuando estamos asignando elementos de un iterable a unas variables, si a una de estas variables le anteponemos un *, Python se encarga de asignarle todos los valores del iterable que le dejen la asignación de las otras variables.

>>> lista = [2, 5, 2, 6, 7, 2, 9]
>>> a, *b, c = lista
>>> print(a)
2
>>> print(b)
[5, 2, 6, 7, 2]
>>> print(c)
9


*) Operador especial "in" aplicado a una lista.         Nos dice si un determinado elemento esta en la lista.

Devuelve True si el elemento esta en la lista y False en caso contrario.

>>> lista = ['angel','andres','maria']
>>> print('maria' in lista)
True


Compresión de listas (list comprehensions).

Nos sirve para crear una nueva lista, excluyendo o trasformando sus elementos, pero de una forma más rápida y que se lea mejor que utilizando el código normal y en una sola línea de código.  

Toda compresión de listas puede ser representada con un bucle for, pero no todo bucle for, puede ser representado como una compresión de listas.

La síntesis de una compresión de listas es la siguiente:

[expression for item in lista condicional]

Esto se puede usar con cualquier iterable, no solo con las listas.

Lo mejor es verlo con ejemplos:

Vamos a obtener una nueva lista, de una lista que ya tenemos, con los números que sean mayores que 10.

>>> lista = [4,22,58,4,1,20,8]
>>> lista_nueva = [x for x in lista if x>10]
>>> print(lista_nueva)
[22, 58, 20]

Esto mismo se podría haber hecho de forma, digamos tradicional usando el siguiente código:

>>> lista = [4,22,58,4,1,20,8]
>>> lista_nueva = []
>>> for x in lista:
>>>   if x > 10:
>>>    lista_nueva.append(x)
print(lista_nueva)
[22, 58, 20]


Otro ejemplo. Multiplicar por 2 los elementos de una lista.

>>> lista = [4,22,58,4,1,20,8]
>>> lista_nueva = [x * 2 for x in lista]
>>> print(lista_nueva)
[8, 44, 116, 8, 2, 40, 16]

 

O por ejemplo también podemos usar  bucles anidados para ciertas cosas, por ejemplo crear puntos de coordenadas:

>>> points = [(x, y) for y in range(0, 3) for x in range(0, 5)]
>>> print(points)
[(0, 0), (1, 0), (2, 0), (3, 0), (4, 0), 
 (0, 1), (1, 1), (2, 1), (3, 1), (4, 1), 
 (0, 2), (1, 2), (2, 2), (3, 2), (4, 2)]
que de forma tradicional llevarían más trabajo:

  1. points = []
  2. for y in range(0, 3):
  3. for x in range(0, 5):
  4. points.append((x, y))
  5. print(points)

e incluso podríamos haber utilizado una condición para mostrar solo los puntos en los que x == y.

  1. points = [(x, y) for x in range(0, 5) for y in range(0, 5)
  2. if x == y]
Para finalizar, si alguna vez necesitamos evaluar una lista, es útil usar los comandos ALL y ANY. (todos o alguno en español)

Por ejemplo si tenemos una serie de números en una lista y queremos saber si son mayores que uno dado podemos usar lo siguiente para abreviar código.

> numeros = [55, 44, 33, 22, 7]
> # ¿Son todos mayores que 5?
> if all([i>5 for i in numeros]):
>	print("Todos los números son mayores que 5.")
Todos los números son mayores que 5.

O si tenemos una lista de números y queremos saber si alguno de ellos es par podemos usar:

> numeros = [55, 44, 33, 22, 7]
> # ¿Alguno de ellos es par?
> if any([i%2==0 for i in numeros]):
     print("al menos un número es par")
> else:
     print("ninguno es par")    

Puedes encontrar más información sobre listas, tuplas y secuencias en la documentación oficial.

sábado, 20 de marzo de 2021

Condicionales en Python



Los condicionales son sentencias de código que solo se ejecutarán si se cumple la condición establecida (operadores lógicos).

Por ejemplo, si tenemos un número almacenado en la variable x, podemos hacer el siguiente condicional:

1 x = 30

2 if  x < 20:

3     print(f"{x} es menor que 20")

4 elif x < 25:

    print(f'{x} es mayor que 20 pero menor que 25')

6 else:

    print(f"{x} es mayor que 25")


En este ejemplo el valor almacenado en la variable x es 30. El programa lee la instrucción de la línea 2 if (si) que se podría traducir como si x es menor que 20 entonces ejecuta el código que está indentado en la linea 3. Como esto no es verdad, no se ejecuta el código y se pasa a la siguiente línea, a la instrucción elif.

La palabra clave elif (de lo contrario Si) es la forma que tiene Python de decir "Si las condiciones anteriores no son verdaderas entonces prueba esta condición". En el ejemplo como x no es menor que 25 la condición es falsa, no se ejecuta el código identado y se pasa a la siguiente instrucción.

La instrucción final es else (de lo contrario), que captura cualquier cosa que no sea detectada por las condiciones anteriores, es decir que si todo lo anterior no se cumple (else = Si no) se ejecute el código identado en la línea 7.


Cuando utilizamos un condicional sin más (if Expresión:) sin poner ninguna condición, lo que se quiere decir en realidad es que si algo es True se ejecute el código de la condición. Por ejemplo:

>>> x = 30
>>> if x:
>>>     print("x es True")
True        

La expresión de la sentencia if se evalúa a False cuando en la expresión a evaluar se dan alguna de las siguientes circunstancias:

- Cualquier número igual a cero.

- Cualquier contenedor que este vacío. (lista, tupla, conjunto, diccionario)

- La expresión False o None.


Otros operadores que podemos usar son:


Operadores de identidad en Python.

Se usan para comparar los objetos, no si son iguales,  sino si en realidad son el mismo objeto, con la misma ubicación en memoria.

Operador is:

Significa una prueba de identidad, ambos lados de la expresión condicional deben ser el mismo objeto.

a = 1
b = 1
if a is b:
  print("a es idéntico que b")

SALIDA:
a es idéntico que b 

Operador is not:

Devuelve True si ambas variables no son el mismo objeto.

x = ["manzana", "plátano"]
y = ["manzana", "plátano"]
z = x

print(x is not z)

# devuelve False porque z es el mismo objeto que x

print(x is not y)

# devuelve True porque x no es el mismo objeto que y, aunque ambas tengan el mismo contenido.

print(x != y)

# para que veas la diferencia entre "is not" and "!=": Esta comparación devuelve False 
# porque x tiene el mismo contenido que y


Operadores de pertenencia en Python:

Operador in:

El operador in, significa, que para cualquier colección (lista, tupla etc) el elemento del lado izquierdo está en el lado derecho del operador.

b = [1,2,3]
if 2 in b:
   print("2 esta en la lista b")

SALIDA:
2 esta en la lista b

Operador not in:

Es el contrario del anterior, y devuelve True cuando el elemento no esta en una secuencia de datos.

b = [1,2,3]
if 5 not in b:
  print("5 no esta en la lista b")
else: 
  print("5 está en la lista b")

SALIDA:
5 no esta en la lista b


El operador not (lo contrario) devuelve True si las declaraciones no son verdaderas y False si lo son.

x = 5

print(not(x > 3 and x < 10))

SALIDA:
False
# devuelve False porque not se utiliza para dar la vuelta al resultado.


También se pueden unir varias condiciones usando los operadores lógicos:


and  (Traducido como "y")

a = 200
b = 33
c = 500
if a > b and c > a:
  print("Todas las condiciones son verdaderas")

Salida:
Todas las condiciones son verdaderas


Or (Traducido como "o")

a = 200
b = 33
c = 500
if a > b or a > c:
  print("Al menos una de las condiciones es verdadera")

Salida:
Al menos una de las condiciones es verdadera


Podemos asignar un valor a una variable de forma abreviada usando una condición es lo que se conoce como Ternary Operator u Operador Ternario ( de 3 elementos: variable, valor True y valor False). Si la condición es cierta se le asignará un valor a la variable y si es Falsa otro distinto. 

a = 7
b = 5 if a > 6 else 2
print(b)

Salida:
5
# La variable valdría 2 si la variable a no fuera mayor que 6


Los operadores de comparación que se pueden utilizar tanto en condicionales como en bucles son:

Igual: a==b
No igual: a!=b
Menor que: a < b
Menor que o igual que: a <= b
Más grande que: a > b
Más grande o igual que: a >= b


Puedes encontrar más información sobre operadores de Python en esta página de w3schools.


Uso de la declaración Switch de otros lenguajes en Python.


Hasta la versión 3.10, Python nunca tuvo una función que implementara lo que hace la instrucción switch en otros lenguajes de programación.

Entonces, si querías ejecutar varias declaraciones condicionales, tendrías que usar la palabra clave elif de esta manera:

edad = 120

if edad > 90:
    print("Ya tienes una edad considerable.")
elif edad < 0:
    print("Aun no has ni nacido")
elif edad >= 18:
    print("¿Ya puedes ir de fiesta?")
else: 
    print("Aun te queda mucho por delante.")

# Salida: Ya tienes una edad considerable.

Desde la versión 3.10 en adelante, Python ha implementado esta característica con las palabras clave "match" y "case

Para escribir declaraciones de este tipo se puede seguir la siguiente estructura:

match término:
    case patrón-1:
         acción-1
    case patrón-2:
         acción-2
    case patron-3:
         acción-3
    case _:
        acción_por_defecto

Ten en cuenta que el símbolo de subrayado es lo que se usa para definir un caso por defecto en Python.

A continuación vamos a ver un ejemplo con esta nueva funcionalidad.

vocal = input("Introduzca una vocal: ")

match vocal:
    case 'a':
        print("la vocal es la a")
    case 'e':
        print("la vocal es la e")
    case 'i':
        print("la vocal es la i")
    case 'o':
        print("la vocal es la o")
    case _:
        print("la vocal es la u u otra cosa")



martes, 9 de marzo de 2021

Las Funciones en Python



Hay una serie de funciones en Python que ya están incluidas en el lenguaje y que están siempre disponibles y otras que podemos crear nosotros.

En este enlace puedes encontrar todas las funciones incluidas en Python (Built-in) ordenadas por orden alfabético.

Entre las funciones para analizar como son las diferentes variables y objetos encontramos:

id() 

Retorna la identidad de un objeto. Recuerda que en Python (variables, listas, clases, funciones etc) todo son objetos. Nos va a dar un numero entero que es único y constante para ese objeto durante toda su existencia. Esto nos va a ser de utilidad para saber si un objeto es único o es una copia del mismo.

x=[1,2,3]
z=x
print(id(x))
139737522015552
print(id(z))
139737522015552
# z es la misma lista que x. Cualquier modificación que hagamos en z afectara a x. No son 
# dos listas distintas. Una es una referencia a la otra.


type()

Nos indica de que tipo es una determinada variable y objeto.

>>> x=[1,2,3]
>>> z="hola"
>>> y=None
>>> print(type(x))
<class 'list'>
>>> print(type(z))
<class 'str'>
>>> print(type(y))
<class 'NoneType'>


dir()

Nos indica todos los métodos y atributos del objeto o de la variable. Esto es muy útil para saber por ejemplo todos los métodos que se les pueden aplicar. Veamos todos los métodos que se pueden utilizar con una lista.

>>> x=[1,2,3]
>>> print(dir(x))
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', 
'__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', 
'__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', 
'__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', 
'__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', 
'__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 
'remove', 'reverse', 'sort']

Como "x" es una lista, si queremos información sobre la misma podemos ver una sencilla documentación utilizando el atributo __doc__

>>> x=[1,2,3]
>>> print(x.__doc__)
Built-in mutable sequence.

If no argument is given, the constructor creates a new empty list.
The argument must be an iterable if specified.


Otra función útil es help() que nos facilita ayuda sobre el objeto que le pasamos. Por ejemplo:


>>> help(print)
Help on built-in function print in module builtins:

print(...)
    print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=
False)
    
    Prints the values to a stream, or to sys.stdout by default.
    Optional keyword arguments:
    file:  a file-like object (stream); defaults to the current 
sys.stdout.
    sep:   string inserted between values, default a space.
    end:   string appended after the last value, default a newli
ne.
    flush: whether to forcibly flush the stream.


O queremos saber información sobre cualquier modulo, basta con impórtalo y aplicar la función help(), la cual nos mostrará la ayuda disponible. Por ejemplo vamos a ver la ayuda sobre el módulo math.


>>> import math
>>> help(math)
Help on built-in module math:

NAME
    math

DESCRIPTION
    This module provides access to the mathematical functions
    defined by the C standard.

FUNCTIONS
    acos(x, /)
        Return the arc cosine (measured in radians) of x.
    
    acosh(x, /)
        Return the inverse hyperbolic cosine of x.
    
    asin(x, /)
        Return the arc sine (measured in radians) of x.
    
    asinh(x, /)
        Return the inverse hyperbolic sine of x.
    
    atan(x, /)
        Return the arc tangent (measured in radians) of x.......etc


Si lo ejecutamos en la consola de Python y no le pasamos ningún argumento nos aparecerá una consola de ayuda interactiva en la que podremos buscar ayuda sobre el módulo, función, clase, método, palabra o documentación que nos interese. Para salir de la consola hay que teclear "quit" y pulsar intro.


>>> help()

Welcome to Python 3.8's help utility!

If this is your first time using Python, you should definitely check out
the tutorial on the Internet at https://docs.python.org/3.8/tutorial/.

Enter the name of any module, keyword, or topic to get help on writing
Python programs and using Python modules.  To quit this help utility and
return to the interpreter, just type "quit".

To get a list of available modules, keywords, symbols, or topics, type
"modules", "keywords", "symbols", or "topics".  Each module also comes
with a one-line summary of what it does; to list the modules whose name
or summary contain a given string such as "spam", type "modules spam".
help >


- Funciones numéricas útiles.

min()

Nos devuelve cual es el item con el valor más bajo o mínimo.

>>> print(min(56,32,6,67))
6
>>> print(min('vehiculo'))
c
# Puesto que las letras están ordenadas alfabéticamente si lo aplicamos a una cadena 
# nos dev7uelve la letra con el valor más bajo.


max ()

Nos devuelve el item con el valor más alto.


abs()

Devuelve el valor absoluto de un número.


sum()

Devuelve la suma de los iterables.

>>> print(sum([23,45,65,3]))
136


Función Input()


Se utiliza para introducir texto escrito a través del teclado. Al llegar a la función el programa se detiene esperando que se escriba algo y se pulse la tecla intro. Si no lo asignas a ninguna variable se puede utilizar para pausar el programa hasta que el usuario pulse la tecla intro.

Si la entrada del teclado (que es texto siempre, tipo string) hay que utilizarla como número, en el programa hay que convertirlo. Veamos un ejemplo.

# Suma de dos números
x=int(input("Introduzca el primer número > "))
y=int(input("introduzca el segundo número > "))
input("Pulsa Intro para saber el resultado")
print(f'La suma de {x} + {y} = {x+y}')

Salida:
Introduzca el primer número > 5
introduzca el segundo número > 7
Pulsa Intro para saber el resultado
La suma de 5 + 7 = 12
# Si no hubiésemos convertido x e y a números con int() nos diría que la suma de 
x + y es 57. Prueba a quitar int() y lo verás.


Funciones Creadas por nosotros:


Una función es un bloque de código que solo se ejecuta cuando se le llama. Podemos pasarlas datos, lo que se llaman parámetros. Nos devolverá un dato como resultado.

En Python para crear una función usaremos la palabra clave def y para llamarla usaremos el nombre de la función seguido por paréntesis.

>>> def mi_funcion():
  print("Hola imprimido desde una función.")

>>> mi_funcion()
Hola imprimido desde una función.


Podemos pasar información a la función a través de los argumentos. Estos irán después del nombre de la función y dentro de los paréntesis. Podemos pasar tantos argumentos como queramos seguido de comas.

>>> def mi_funcion(nombre):
      print("Hola " + nombre )

>>> mi_funcion("Emilio")
Hola Emilio

>>> mi_funcion("Tobias")
Hola Tobias
>>> mi_funcion("Lucas")
Hola Lucas


Por defecto, una función debe ser llamada con el número correcto de argumentos. Esto significa que si la función espera dos argumentos, tenemos que pasarle dos argumentos, ni más ni menos.

>>> def mi_funcion(nombre, apellido):

        print("Hola" , nombre , apellido )

>>> mi_funcion("Emilio","Hurtado")
Hola Emilio Hurtado
>>> mi_funcion("Tobias","Palomo")
Hola Tobias Palomo
>>> mi_funcion("Lucas","Escoté")
Hola Lucas Escoté


Si en el ejemplo anterior le pasamos uno o tres argumentos obtendremos un error: 


>>> def mi_funcion(nombre, apellido):
       print("Hola" , nombre , apellido )

>>> mi_funcion("Emilio")
Traceback (most recent call last):
  File "./prog.py", line 5, in <module>
TypeError: mi_funcion() missing 1 required positional argument: 'apellido'


Argumentos Arbitrarios *args

Si a priori desconoces el número de argumentos que le pasarás a la función, utiliza un asterisco *, antes del nombre del argumento. De esta forma la función recibirá una tupla como argumento y puedes acceder a sus elementos como accederías en cualquier tupla. Aunque puede utilizar el nombre del argumento que quieras después del asterisco, por convención se suele utilizar *args

Veamos un ejemplo:

>>> def mi_funcion(*args):
       print("Mi hijo más joven es "+ args[1])

>>> mi_funcion("Emilio","Antonio","juan")
Mi hijo más joven es Antonio


Hasta ahora los parámetros los hemos pasado por posición f(a, b, c) = f(1, 2 ,7) pero también los podemos pasar con la estructura Clave = Valor. De esta forma el orden de los argumentos no importa.

>>> def mi_funcion(chico3, chico2, chico1):
       print("Mi hijo más joven es "+ chico3)

>>> mi_funcion(chico1="Emilio", chico2="Antonio", chico3="Juan")
Mi hijo más joven es Juan

Ahora bien, al utilizar una estructura de la forma f(*arg, a, b) puedes pasar cualquier número de argumentos por posición que tu quieras, pero fuerzas a que obligatoriamente tengas que pasar los argumentos a y b, y lo hagas con la estructura clave=valor. Si no pones a o b o lo haces sin utilizar la clave=valor tendrás un bonito error.

def mi_funcion(*arg, a, b):
  """ Solamente imprime un texto por pantalla que se le pase
  como argumento."""
  print(arg, a, b)
  

mi_funcion('mercurio', a="venus", b="tierra")
Salida:
('mercurio',) venus tierra


Argumentos de palabras clave arbitrarias **kwargs


Si no sabes cuantos argumentos de palabras claves se pasarán a la función, podemos agregar dos asteriscos ** antes del nombre del argumento en la definición de la función. Puedes usar el nombre del argumento que quieras, aunque por convección se suele utilizar **kwargs

De esta forma la función recibirá un diccionario como argumento y puedes acceder a sus datos como lo harías en cualquier diccionario.


>>> def mi_funcion(**kwargs):
      print("Mi hijo más joven es "+ kwargs['chico3'])

>>> mi_funcion(chico1="Emilio", chico2="Antonio", chico3="Juan")
Mi hijo más joven es Juan

Valores de parámetros por defecto.

En el siguiente ejemplo veremos como utilizar un valor de parámetro por defecto. Si cuando llamas a la función no le pasas un argumento la función utilizará el parámetro que le hayas especificado por defecto.


def saludo(nombre="Manolo"):
  return print("Bienvenido", nombre)

# Si no le pasamos argumento la función no da error ya que 
# utiliza el parámetro por defecto.
saludo()

# Si le pasamos argumento funciona normalmente.
saludo("Izan")

Salida:
Bienvenido Manolo
Bienvenido Izan


Pasando una lista como argumento.

Puedes pasar cualquier tipo de datos como argumentos de una función (cadenas, números, diccionarios, listas etc.) y serán tratados como tal dentro de la función:

def mi_funcion(comida):
  for x in comida:
    print(x)

frutas = ["manzana", "naranja", "limon"]

Salida:
manzana
naranja
limon


Retornando Valores.

Normalmente en una función, después de que le pasemos unos valores o argumentos, esta realizará el código que contenga y nos devolverá o retornará algo.

Por ejemplo, una función que nos multiplique dos números y nos retorne el valor podría ser:

>>> def multiplicar(x, y):
>>>  return x * y

>>> print(multiplicar(3,5))
15


La instrucción pass.

Cuando definimos una función hay ocasiones en las que no hemos escrito el código de la misma aún, pero necesitamos saber que se llama correctamente desde el programa. Sin embargo una función no se puede dejar vacía ya que nos daría un error. Es estos casos se utiliza, pass. Realmente no sucede nada cuando se ejecuta, como su nombre indica la secuencia de ejecución del programa "pasa" por el.

def mifuncion():
  "Aqui ira el código de la funcion"
  pass

mifuncion()

Salida:
>
#Llama a la función pero no da error, puedes quitar la instrucción pass y ver
#que ocurre.


Recursión.

Las funciones pueden asignarse a una variable o pueden también ser argumentos de otras funciones. Pueden llamar a una función distinta o incluso una función puede llamarse y ser argumento de si misma, lo cual se denomina recursión.

 Aunque en términos de eficiencia y velocidad de cálculo no es buena idea usar la recursión, hay ocasiones en que esto puede ser útil. Por ejemplo para calcular el factorial de un número.

Factorial de 3 => 3! = 3*2*1 = 6

def factorial(n):
    if n == 0:
        return 1
    else:
        return n * factorial(n - 1)

print(factorial(3))
6

Cuando llamamos a la función le pasamos el número 3. Puesto que 3 es distinto de 0 la función devuelve 3 por el resultado de llamarse a si misma pasando como argumento (3-1). En esta segunda llamada a la función el argumento que se le pasa es 2, mientras la primera llamada sigue esperando el valor que le retorne la segunda llamada.  Como 2 es distinto de 0 en esta llamada se devuelve 2 por lo que resulte de llamarse a si misma una tercera vez con el argumento de (2-1), mientras tanto la llamada primera y la segunda sigue esperando. Esto se repite hasta que el argumento que se le pase es 0 y entonces se romperá el bucle y nos devolverá un 1 y se hará todo el calculo. 

El problema de las funciones recursivas es que como te confundas y no pongas una condición de finalización, crearas un bucle infinito. (Bueno es finito porque acabarás con la memoria libre de la computadora y te dará un error)


Variables locales y globales.

Dentro de una función se pueden usar variables que se hayan declarado en alguna parte de nuestro código pero fuera de ella.


>>> nombre = "Angela"

>>> def mifuncion():
>>>  print(nombre)

>>> mifuncion()
Angela

En el ejemplo anterior la variable nombre tiene asignado el valor de "Angela", y al llamar a la función se imprime Angela, a pesar de que cuando declaramos la función dicha variable no está dentro de su código. La razón de esto, es que cuando se ejecuta la función, la variable ya está declarada previamente y tiene un valor, razón por la cual la función la muestra. 

Este tipo de variables, declaradas fuera de la función pero ejecutadas dentro, reciben el nombre de variables globales.

Al revés. Si declaramos una variable dentro de una función, esta no podrá usarse fuera de ella. Por ejemplo:

>>> def mifuncion():
>>>  nombre="Angela"

>>> mifuncion()
>>> print(nombre2)
Traceback (most recent call last):
  File "main.py", line 5, in <module>
    print(nombre)
NameError: name 'nombre' is not defined

Recibimos el error "name 'nombre' is not defined". Estas variables declaradas dentro de la función, que no están disponibles fuera de la misma, se denominan variables locales.

¿Y si cambiamos el valor de una variable global dentro de una función?

>>> nombre="Pepe"
>>> def mifuncion():
>>>  nombre="Angela"
>>>  print(nombre)

>>> mifuncion()
Angela
>>> print(nombre)
Pepe

El programa imprimirá los nombres "Angela" y "Pepe". A pesar de que el valor de la variable nombre cambio dentro de la función, ¡Fuera de la función sigue siendo el mismo!. Esto se hace para proteger a las variables globales frente a cambio inadvertidos que pudieran ocurrir dentro de funciones. Por lo tanto, si se modifica una variable global dentro de la función, está se trasformará en una variable local y su modificación no afectará a una variable global que tenga el mismo nombre.

Más formalmente, dentro de una función la variable no puede ser en unos momentos global y en otros local. Si hay una asignación, aunque sea posterior a su uso como variable global, la variable será considerada local y se producirá un error:

>>> def mifuncion():
>>>  nombre1="Angela"
>>>  print(nombre2)
>>>  nombre2="Pepe"

>>> nombre1="Antonio"
>>> nombre2="Jeremias"
>>> mifuncion()
Traceback (most recent call last):
  File "main.py", line 8, in <module>
    mifuncion()
  File "main.py", line 3, in mifuncion
    print(nombre2)
UnboundLocalError: local variable 'nombre2' referenced before assignment

La variable nombre2 ha sido referenciada antes de ser asignada lo que está prohibido en Python.

Si en algún momento queremos que una función pueda cambiar alguna variable global, es decir que se haya declarado fuera de ella, debemos declarar esa variable en la función usando la palabra clave global.

>>> def mifuncion():
>>>   global nombre
>>>   nombre="Juan"
>>>   print(nombre)

>>> nombre="Antonio"
>>> mifuncion()
Juan
>>> print(nombre)
Juan


Un pequeño, pero importante matiz. Todo lo que hemos visto anteriormente sobre variables globales y locales es válido cuando las variables que se pasan son de tipo inmutable. Perooooo aquellas variables mutables (listas, diccionarios..) pasadas como argumentos en funciones, sufren las modificaciones que se realicen en el interior de las funciones.

Veamos que pasa si pasamos como argumento una lista que es un dato mutable.

>>> lista_global=["Antonio","pepe"]

>>> def mi_funcion():
>>>  print(lista_global)
>>>  lista_global[0]="Maria"

>>> mi_funcion()
['Antonio', 'pepe']
>>> print(lista_global)
['Maria', 'pepe']


Documentación de una función.

La documentación de una función en Python se realiza inmediatamente después de definir la función poniendo un sangrado y situando el texto explicativo entre comillas triples incluso cuando la documentación solo conste de una línea.

Para consultarla podemos usar la función integrada de Python "help" o bien imprimir el nombre de la función seguido del método .__doc__

>>> def mi_funcion(texto):
>>>   """ Solamente imprime un texto por pantalla que se le pase
>>>   como argumento."""
>>>   print(texto)

>>> mi_funcion('Hola mundo')
Hola mundo
>> help(mi_funcion)
Help on function mi_funcion in module __main__:

mi_funcion(texto)
    Solamente imprime un texto por pantalla que se le pase
    como argumento.
>>> print(mi_funcion.__doc__)
 Solamente imprime un texto por pantalla que se le pase
  como argumento.


- Funciones anónima o lambda.

Se utilizan cuando se trata de funciones o expresiones sencillas que podamos definir en una sola expresión. Nunca incluyen ni bucles ni condicionales. No hace falta declararlas previamente y su estructura típica sería:

nombre función = lambda <parámetros>:<expresión>

En lo que queramos que haga, ósea en la expresión, no hace falta poner un return, ya que este tipo de funciones nos devuelve lo que esté después de los dos puntos.

Veamos algunos ejemplos de lo útiles que pueden llegar a ser siempre que queramos que realicen cosas sencillas que se puedan realizar en una única expresión.

Por ejemplo si queremos cálcular el area de un triángulo de la forma tradicional se podría hacer con una función:

>>> # Area de un triángulo
>>> def area_triangulo(base, altura):
        return (base*altura)/2
>>> # Y llamamos a la función.
>>> print(area_triangulo(5,7)

Ahora bien de una forma simple y sencilla usando una función lambda:

>>> area_triangulo = lambda base, altura: (base*altura)/2
>>> print(area_triangulo(5,7))

o también se puede escribir así:
>>> print((lambda base, altura: (base*altura)/2)(5, 7))
en donde pasamos directamente los valores de los parámetros.


Más ejemplos de funciones lambda.

>>> # Suma de dos números
>>> valor = lambda x,y: x+y

>>> print(valor(5,2))
7
>>> print(valor(7,9))
16

o

>>> # Comprobar si un numero es par
>>> par = lambda num: num%2 == 0

>>> print(par(5))
False
>>> print(par(12))
True

o

>>> # Dar la vuelta a una cadena de texto utilizando slicing.
>>> revertir = lambda cadena: cadena[::-1]

>>> print(revertir("Hola"))
aloH

Como ves las posibilidades que nos ofrecen son enormes.

Y si las utilizamos junto con las funciones Filter y Map, sus posibilidades se amplían aun más.


Función Filter

Esta función verifica que los elementos de una secuencia cumplen una condición, devolviéndonos un iterador con los elementos que cumplen dicha condición.

Su estructura es:

filter(Función a llamar, lista de objetos)

La función a llamar puede ser una función normal o una función lambda. 

Para entenderlo mejor veamos un ejemplo. Si tengo esta lista de números:

lista = [3,45,22,7,18,15,14,2,31] y quiero obtener otra lista pero solo con los números pares, haría lo siguiente:

>>> lista = [3,45,22,7,18,15,14,2,31]
>>> # par si el resto de dividir el numero entre 2 es igual a cero.
>>> print(filter(lambda numero:numero%2==0, lista))
<filter object at 0x7f3f783f6b20>

Aunque parece que no ha hecho nada en realidad si. Ha aplicado el filtro de la función a la lista y nos ha devuelto un objeto. Sin embargo nosotros lo que queremos no es el objeto sino la lista, con lo que lo único que tenemos que hacer es convertirlo en lista con la función list e imprimirlo:

>>> print(filter(lambda numero:numero%2==0, lista))
<filter object at 0x7f3f783f6b20>
>>> print(list(filter(lambda numero:numero%2==0, lista)))
[22, 18, 14, 2]

Si quieres usarla con una función normal haríamos lo siguiente:

>>> def numero_par(num):
>>>     if num%2 == 0:
>>>        return True
>>> print(list(numero_par, lista))


Función Map. 

La función map aplica una función a cada elemento de una secuencia y devuelve un nuevo iterable (objeto) con la función aplicada a cada argumento.

Su estructura es:

map(función a aplicar, iterable)

Por ejemplo. Queremos sumar 5 a todos los números que tenemos en una lista. Pues lo hacemos de una forma parecida a filter:

>>> lista = [3,45,22,7,18,15,14,2,31]
>>> # sumamos 5 a cada elemento de la lista.
>>> print(list(map(lambda numero:numero+5, lista)))
[8, 50, 27, 12, 23, 20, 19, 7, 36]


Decoradores.

Un decorador en Python es una función que toma otra función como argumento y devuelve, ¡sii! otra función. Sirven para agregar acciones o nuevas funcionalidades a funciones que ya tenemos, sin modificar su código. Si has visto alguna vez @ delante de un nombre, ya has visto un decorador.

Tienen todas la siguiente estructura:

def funcion_decorador(funcion_argumento):
    def funcion_interna():
        # Código de la función interna.
    return funcion_interna

Para entender como funcionan los decoradores tenemos que entender que se basan en tres puntos.

1) Se pueden ejecutar funciones dentro de otras funciones.

Considera el siguiente ejemplo:

def funcion_externa():
    print("Ejecutando la función externa.")

    def funcion_interior():
        print("Ejecutando la función interior.")
    
    funcion_interior()

Si llamamos a la función externa obtenemos el siguiente resultado:

 >>> funcion_externa()
Ejecutando la función externa.
Ejecutando la función interior.

En otras palabras, podemos ver como se ejecuta la función interior porque llamamos a la función externa dentro de la cual la hemos definido. Si directamente intentamos llamarla nos saldrá un error ya que esta función interna es local a la función externa porque está dentro de la misma. Vamos a demostrarlo:

>>> funcion_interior()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'funcion_interior' is not defined


2) Una función puede retornar otra función.

En el punto anterior hemos visto que la función interior solo se puede ejecutar si llamamos a la función externa. ¿Pero que pasaría si la función externa retornara la función interna? Pues vamos a probar y lo vemos. Modificamos un poco la función interna para que retorne la función interior.

def funcion_externa():
    print("Ejecutando la función externa.")

    def funcion_interior():
        print("Ejecutando la función interior.")
    
    return funcion_interior

Además observa que en la última línea no usamos parentesis en "funcion_interior" porque no queremos que la función se ejecute si no que sea retornada. Ahora asignaremos esta función a una variable que denominamos "mi_funcion":

>>> mi_funcion = funcion_externa()
Ejecutando la función externa.

>>> mi_funcion()
Ejecutando la función interior.

Lo que observamos es que al haber asignado la función externa a la variable mi_función, esta se ha ejecutado y ha retornado y guardado en la variable, la función interior. De esta forma podemos ejecutar siempre que queramos a la función interna llamando a mi_funcion()


3) Una función puede ser el argumento de otra función.

Este punto vamos a verlo con el siguiente ejemplo.

def una_funcion():
    return "Este es el código de una función."

def otra_funcion(argumento):
    print("Este es el código de otra función.")
    print(argumento())

Tenemos dos funciones muy simples. La primera, "una_funcion" devuelve un string, un texto. La segunda "otra_funcion" imprime tambien otro texto y a continuación el resultado retornado por la función que se le pasa como argumento. Ahora vamos a ver que ocurre cuando ejecutamos "otra_funcion" y le pasamos la primera como argumento.

 >>> otra_funcion(una_funcion)
Este es el código de otra función.
Este es el código de una función.


Visto lo anterior podemos decir que un decorador se basa en las tres ideas que acabamos de ver. Es decir acepta una función como argumento y define una función interna, la cual retorna. Además dentro de la función interna ejecuta la función que se le ha pasado como argumento.


Ejemplos Prácticos:

1)

def suma(a, b):
    return a + b

>>> print(suma(5, 4))
>>> 10

Como ves la función suma toma dos números y los suma. 

Ahora imaginemos que la función suma pudiera tomar una lista con tuplas de dos elementos como argumento, por ejemplo [(5, 4), (3 ,2), (7, 4)] y nos devuelva otra lista con la suma de los valores dentro de la tupla, es decir [9, 5, 11]. Pues para esto podemos usar un decorador.

En primer lugar damos un nombre al decorador con el que se intuya lo que va a hacer. Aquí el decorador llamado suma_lista es simplemente una función que toma a otra como argumento.

Dentro de la función decorada, definimos una función local llamada funcion_interna. Esta función va a heredar el argumento que se le pase a suma_lista, en nuestro caso toma una lista de tuplas de dos elementos como argumento. Esta función interna recorre esta lista de tuplas de dos elementos y para cada tupla aplica la función original, suma, siendo la posición 0 de la tupla el argumento a de la función suma y la posición de índice 1 el argumento b. La función interna devolverá una lista de estos valores resumido. La función suma_lista devuelve finalmente la función interna.

Para aplicar el decorador usamos la sintexis @, seguida del nombre de la función decorador, @suma_lista. 

La salida estándar a la consola muestra que ahora hemos ampliado la funcionalidad de la función suma original, de modo que ahora puede tomar una lista de tuplas.

def suma_lista(func_argumento):
    def funcion_interna(lista_de_tuplas): # Esta seria la función interna
        return [func_argumento(val[0], val[1]) for val in lista_de_tuplas]
    return funcion_interna

@suma_lista
def suma(a, b):
    return a + b

print(suma([(5, 4), (3 ,2), (7, 4)]))

Salida:

[9, 5, 11]

Con esta forma de definir el decorador cada vez que llamemos a suma() en cualquier parte del programa se va a ejecutar también el decorador. Existe otra forma de crear el decorador que nos permite también usar la función original sin decorar. Lo podemos hacer de la siguiente forma:

def suma_lista(func_argumento):
    def funcion_interna(lista_de_tuplas): # Esta seria la función interna
        return [func_argumento(val[0], val[1]) for val in lista_de_tuplas]
    return funcion_interna

def suma(a, b):
    return a + b

funcion_decorada = suma_lista(suma)

# Usando el decorador
print(funcion_decorada([(5, 4), (3 ,2), (7, 4)]))

# Usando la función suma original
print(suma(5, 4))

Salida:

[9, 5, 11]
9


2) 

Como has visto la función interna hereda los argumentos de la función que se pase a la función externa o decorador como argumento, Si le pasamos una de un argumento, la función interna heredara ese argumento. Si le pasamos una función con dos argumentos la función interna heredara dos y así sucesivamente. Y lo mismo con el tipo.  Para poder usar cualquier tipo y número de argumentos podemos ver el siguiente ejemplo.

En el se utiliza un decorador que crea un fichero de texto, utilizado a modo de registro, para ver que parámetros se han pasado a la función que decora. Se utiliza *args, *kwargs como argumentos de la función interna o envolvente.

def mi_logger(funcion_original):
    import logging
    logging.basicConfig(filename=f"{format(funcion_original.__name__)}.log", level=logging.INFO)

    def funcion_interior(*args, **kwargs):
        logging.info(f"Ejecutado con args: {args}, y kwargs: {kwargs}")
        funcion_original(*args, **kwargs)
    return funcion_interior

@mi_logger
def mostrar_info(name, age):
    print(f"{name} tiene {age} años")
mostrar_info("Tutankamon", 3000)

Lo importante es ver como se usa *args y **kwargs para pasar cualquier número de parámetros al decorador. Si te interesa ver para que sirve el módulo logger puedes seguir el enlace.

mostrar_info.log

Tutankamon tiene 3000 años

Para terminar comentar que normalmente nosotros no desarrollaremos decoradores pero si que podemos encontrarlos en muchas librerías como Flask y Django para crear aplicaciones web. 

También que los decoradores no solamente se pueden aplicar a funciones si no tambien a clases que es un concepto que veremos en otro capítulo.