Curso Python: Clase 3

Temas:

  • Listas como pilas y colas
  • Listas por comprensión
  • Extensiones funcionales
  • Argumentos (args + kwargs)
  • Cadena con formato
  • Pruebas unitarias

Concepto de Pila

El comportamiento de una pila se puede describir con la frase "Lo último que se apiló es lo primero que se usa". Es decir, su estructura es LIFO (Last in, First out)

Podríamos pensar en una pila de platos de donde se puede sacar el primer plato para lavar, pero no los del medio.

Operaciones

  • Crear: crea una pila vacía
  • Apilar o Push: agrega un elemento al tope de la pila
  • Desapilar o Pop: elimina el primer elemnto del tope de la pila
  • Preguntar si está vacía: devuelve True si la pila está vacía

Listas como Pila

¿Como podríamos utilizar las operaciones de una lista para representar una pila?

Si tomamos la convención de que el tope de la pila se encuentra al final de la lista tenemos:

  • Operación crear: crear una pila vacía
In [1]:
stack = []
stack
Out[1]:
[]
  • Operación push (apilar), podemos utiliza append() de las listas para agregar al final
In [2]:
stack.append(1)
stack.append(2)
stack.append(3)
stack.append(4)
stack
Out[2]:
[1, 2, 3, 4]
  • Operación pop (desapilar), se usa la función pop() de las listas para eliminar el ultimo elemento
In [3]:
stack.pop()
Out[3]:
4
In [4]:
stack
Out[4]:
[1, 2, 3]
  • Operación está vacia: básicamente una pila está vacía si no tiene elementos
In [5]:
def esta_vacia(pila):
    return len(pila) is 0
In [7]:
def esta_vacia(pila):
    return not len(pila)
In [8]:
def esta_vacia(pila):
    return not pila
In [9]:
print esta_vacia(stack)
print stack.pop() 
print stack.pop()
print stack.pop()
print esta_vacia(stack)
False
3
2
1
True
In [10]:
#Si hacemos pop() de una lista vacía obtenemos un error
stack.pop()
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-10-505a7268d9a8> in <module>()
      1 #Si hacemos pop() de una lista vacía obtenemos un error
----> 2 stack.pop()

IndexError: pop from empty list
In [12]:
#Debemos tener en cuenta
if not esta_vacia(stack):
    stack.pop()

Concepto de Cola

El comportamiento de una cola se puede describir con la frase "Lo primero que se encoló es lo primero que se usa". Es decir, su estructura es FIFO (First in, First out)

Un ejemplo básico podría ser la cola en un cajero, donde la primera persona que llegue, es la primera en utilizar el cajero.

Operaciones

  • Crear: crea una cola vacía
  • Encolar o Enqueue: agrega un elemento al final de la cola
  • Desencolar o Dequeue: elimina el primer elemnto del principio de la cola
  • Preguntar si está vacía: devuelve True si la cola está vacía

Suponiendo que implementamos una cola usando una lista. ¿Cómo se podría implementar? ¿Cuál sería el costo?

Opción 1:

  • enqueue encola al principio de la lista
  • dequeue desencola del final de la lista

Opción 2:

  • enqueue encola al final de la lista
  • dequeue desencola del principio de la lista

Problema: En el primer caso encolar y en el segundo caso desencolar del principio implica desplazar todo el contenido de la lista (en un sentido u otro). Esta operación es costosa, imaginense una lista muy grande!

Deque como Cola

  • Deque: diseñado para appends y pops eficientes en ambos extremos
  • Operación enqueue (encolar): se usa la función append()
In [13]:
from collections import deque

queue = deque(["Alicia", "Bernardo", "Carlos"])
queue.append("Daniel")
queue
Out[13]:
deque(['Alicia', 'Bernardo', 'Carlos', 'Daniel'])
In [14]:
type(queue)
Out[14]:
collections.deque
  • Operacion dequeue (desencolar): se usa la función popleft()
In [15]:
queue.popleft()
Out[15]:
'Alicia'

Ejercicio 3.0

Programar la función invertir_pila que reciba una pila y devuelva otra nueva con sus elementos en orden inverso.

Usar solamente las funciones:

  • append()
  • pop()
  • esta_vacia()
In [19]:
from collections import deque

def invertir_pila(pila):
    pila_nueva = []
    cola = deque()
    
    while not esta_vacia(pila):
        elem = pila.pop()
        cola.append(elem)
        
    while not esta_vacia(cola):
        elem = cola.popleft()
        pila_nueva.append(elem)
        
    return pila_nueva
     
In [18]:
pila = [1,2,3,4,5,6,7,8,9]
invertir_pila(pila)
Out[18]:
[9, 8, 7, 6, 5, 4, 3, 2, 1]

Listas por Comprensión

  • Es una herramienta que permite generar listas de forma concisa
  • Guarda una notación similar a la matemática para definir conjuntos.

    Ejemplo:

    **A = {3x / x ∈ N: x < 10}**
    Equivalente a:
    **A = {3, 6, 9, 12, ... , 30}**

¿Cómo podemos obtener una lista de los n primeros numeros pares?

In [1]:
def n_pares(n):
    lista = []
    for x in xrange(1,n+1):
        lista.append(x*2)
    return lista

n_pares(10)
Out[1]:
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
In [2]:
def n_pares(n):
    return [2*x for x in xrange(1,n+1)]

n_pares(10)
Out[2]:
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

¿Cómo podemos obtener una lista de los numeros mayores a n de otra lista?

In [3]:
def mayores_a_n(lista, n):
    nueva_lista = []
    for x in lista:
        if x>n:
            nueva_lista.append(x)
    return nueva_lista

mayores_a_n([3,1,4,6,2,3,2],2)
Out[3]:
[3, 4, 6, 3]
In [4]:
def mayores_a_n(lista, n):
    return [x for x in lista if x>n]

mayores_a_n([3,1,4,6,2,3,2],2)
Out[4]:
[3, 4, 6, 3]

Estructura:

[<expresion> for <item> in <secuencia> if <condicion>]

Ejercicio 3.1

Implementar las funciones map y filter con listas por comprensión

Ayuda:

  • map: Recibe una función y una lista. Aplica la función a cada uno de los elementos de la lista recibida, y devuelve una lista con los resultados.
  • filter: Recibe una función (booleana) y una lista. Aplica la función a cada uno de los elementos de la lista recibida, y devuelve una lista con todos los elementos que devolvieron True

Solución 3.1

In [6]:
def _map(funcion, lista):
    return [funcion(x) for x in lista]

def cuadrado(x):
    return x**2

lista = [1,2,3,4,5]
_map(cuadrado, lista)
Out[6]:
[1, 4, 9, 16, 25]
In [9]:
def _filter(funcion, lista):
    return [x for x in lista if funcion(x)]

def mayor_que_5(x):
    return x > 5

lista2 = [1,6,3,5,3,4,8,5]
_filter(mayor_que_5, lista2)
Out[9]:
[6, 8]

Y podemos devolver listas o tuplas también!

In [10]:
vec = [1,2,3]
[(x,x**2) for x in vec]
Out[10]:
[(1, 1), (2, 4), (3, 9)]

Y anidar varios for

In [12]:
vec1 = [1,2,3]
vec2 = ["a","b","c"]
[(x,y) for x in vec1 for y in vec2]
Out[12]:
[(1, 'a'),
 (1, 'b'),
 (1, 'c'),
 (2, 'a'),
 (2, 'b'),
 (2, 'c'),
 (3, 'a'),
 (3, 'b'),
 (3, 'c')]

Y también podemos iterar matrices

In [16]:
mat = [[1,2,3],
       [4,5,6],
       [7,8,9]
      ]

print [[fila[i] for fila in mat] for i in [0, 1, 2]]
[[1, 4, 7], [2, 5, 8], [3, 6, 9]]

Diccionarios por comprensión

In [17]:
{x: x**2 for x in (2, 4, 6)}
Out[17]:
{2: 4, 4: 16, 6: 36}

¿Qué genera el siguiente código?

In [18]:
uno = [1,2,3]
dos = ["a","b","c"]
{x:y for x in dos for y in uno}
Out[18]:
{'a': 3, 'b': 3, 'c': 3}

Ejercicio 3.2

Implementar una función que recibe una lista y devuelve la lista invertida, usando listas por comprensión

Solución 3.2

In [31]:
def invertir(lista):
    return [lista[i] for i in range(len(lista) - 1, -1, -1)]

invertir([1,2,3,4])
Out[31]:
[4, 3, 2, 1]
In [26]:
def invertir(lista):
    return [lista[-(i+1)] for i in xrange(len(lista))]

invertir([1,2,3,4])
Out[26]:
[4, 3, 2, 1]

Extensiones funcionales

  • Los problemas se descomponen en un conjunto de funciones que toman entradas, y producen salidas, sin almacenar ningún estado interno

    • Programas más modularizados
    • Testing más fácil
    • Debugging más fácil
    • Mayor reusabilidad de las funciones
  • Herramientas funcionales en Python:

    • Lambda
    • Map
    • Filter
    • Reduce
    • Iterators
    • Generators
    • Enumerate
    • Zip
  • A partir de Python 3.x algunas hay que importarlas

Expresiones Lambda

  • Vienen del concepto de cálculo lambda
  • Funciones “anónimas”
  • Pueden reemplazar a funciones privadas
  • Permiten una forma abreviada de definir funciones simples
  • Estructura:
    ``` lambda argumentos: expresion ```

 Algunos ejemplos...

In [32]:
doble = lambda x: x*2
doble(2)
Out[32]:
4
In [33]:
suma = lambda x,y: x+y
suma(1,2)
Out[33]:
3
In [34]:
maximo = lambda x,y: x if x > y else y
maximo(3,7)
Out[34]:
7

Map

  • Aplica una función a una o muchas listas

A una lista

In [35]:
lista = [1, 2, 3, 4, 5]
map(lambda x: x*2, lista)
Out[35]:
[2, 4, 6, 8, 10]

A dos listas

In [37]:
lista_uno = ["Hola", "como", "soy"]
lista_dos = ["Juan", "estas?", "Pedro"]
map(lambda x,y: x + " " + y, lista_uno, lista_dos)
Out[37]:
['Hola Juan', 'como estas?', 'soy Pedro']

Filter

In [38]:
filter(lambda x: x % 2 == 0, range(11))
Out[38]:
[0, 2, 4, 6, 8, 10]

Sin función

In [41]:
lista = [True, 0, False, "cadena", 4, "", [], (5,2), {}, None, 3.14]
filter(None, lista)
Out[41]:
[True, 'cadena', 4, (5, 2), 3.14]

Reduce

  • Aplica una función de dos parámetros, de manera acumulativa a los items de una secuencia, de izquierda a derecha, reduciendo la secuencia a un único valor.
  • En Python 3.x: import functools
  • Limitado a operadores asociativos y poco legible, según GVR
In [42]:
# Ejemplo sumatoria
suma = lambda x, y: x + y
print reduce(suma, [1, 2, 3, 4, 5])
# Ejemplo máximo
print reduce(max, [1,4,2,6,9,3,4,5])
15
9

Ejercicio 3.3

Implementar una función que devuelve la cantidad de elementos en una lista que verifican una condición dada.

Solución 3.3

In [49]:
def cuantos_cumplen(condicion, lista):
    return reduce(lambda x,y:x+y, map(condicion, lista))

def cuantos_cumplen_legible(condicion, lista):
    parcial = map(condicion, lista)
    suma = lambda x,y: x+y
    return reduce(suma, parcial)

def cuantos_cumplen2(condicion, lista):
    return len(filter(condicion,lista))

condicion = lambda x: x>5
lista = [1,2,4,5,7,8,9,7]
cuantos_cumplen_legible(condicion, lista)
Out[49]:
4

Iteradores

  • Permiten usar la sintaxis for
  • Llaman al método iter() sobre el objeto a iterar. Devuelve un iterador con el método __next__()
  • El método __next__() me permite acceder a cada elemento del contenedor, uno a la vez.
  • Cuando llegó al final de la iteración lanza una excepción: StopIteration

Generators

  • Herramienta simple para crear iteradores
  • Resuelven el problema de generar en memoria la lista a iterar
  • Usan la sintaxis de una función
  • Los métodos __iter__() y __next__() se crean automáticamente
  • yield devuelve datos en cada iteración
  • Si quiero volver a iterarlos, debo crear uno nuevo
  • Se pueden crear con la sintaxis de listas por comprensión

¿Cómo haríamos una función que genere los primeros n cuadrados?

In [11]:
def primeros_n_cuadrados(n):
    num = 0
    nums = []
    while num < n:
        nums.append(num**2)
        num += 1
    return nums

primeros_n_cuadrados(100)
Out[11]:
[0,
 1,
 4,
 9,
 16,
 25,
 36,
 49,
 64,
 81,
 100,
 121,
 144,
 169,
 196,
 225,
 256,
 289,
 324,
 361,
 400,
 441,
 484,
 529,
 576,
 625,
 676,
 729,
 784,
 841,
 900,
 961,
 1024,
 1089,
 1156,
 1225,
 1296,
 1369,
 1444,
 1521,
 1600,
 1681,
 1764,
 1849,
 1936,
 2025,
 2116,
 2209,
 2304,
 2401,
 2500,
 2601,
 2704,
 2809,
 2916,
 3025,
 3136,
 3249,
 3364,
 3481,
 3600,
 3721,
 3844,
 3969,
 4096,
 4225,
 4356,
 4489,
 4624,
 4761,
 4900,
 5041,
 5184,
 5329,
 5476,
 5625,
 5776,
 5929,
 6084,
 6241,
 6400,
 6561,
 6724,
 6889,
 7056,
 7225,
 7396,
 7569,
 7744,
 7921,
 8100,
 8281,
 8464,
 8649,
 8836,
 9025,
 9216,
 9409,
 9604,
 9801]

Pero si n es muy grande, estaríamos almacenando una lista muy grande en memoria (cómo pasaba con range) ...

In [30]:
def primeros_n_cuadrados(n):
    num = 0
    while num < n:
        yield num**2
        yield num*2
        num += 1

ret1 = primeros_n_cuadrados(10)
print ret1
print list(ret1)
<generator object primeros_n_cuadrados at 0x10437edc0>
[0, 0, 1, 2, 4, 4, 9, 6, 16, 8, 25, 10, 36, 12, 49, 14, 64, 16, 81, 18]
In [33]:
def primeros_n_cuadrados(n):
    # Generador estilo list comprehension
    return (x**2 for x in xrange(n))

ret = primeros_n_cuadrados(10)
print type(ret)
print list(ret)
<type 'generator'>
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Ejercicio 3.4

Implementar un generator que me permita obtener los primeros n valores de la secuencia de fibonacci

fibo(0) = 0
fibo(1) = 1
fibo(2) = 1
fibo(3) = 2
.
.
.
fibo(n) = fibo(n-1) + fibo(n-2)

Solución 3.4

In [ ]:
def fiboGenerator(n):
    a, b = 0, 1
    contador = 0
    while contador < n:
        yield a
        a, b = b, a + b
        contador += 1
    return

Ejercicio 3.5

Dadas dos listas de enteros del mismo largo, implementar una función que devuelva una lista en la que cada elemento es el producto de los elementos de mismo indice de las listas originales. Usar listas por comprensión.

Ayuda: zip()

Solución 3.5

In [37]:
def producto_de_listas(vecA, vecB):
    return [x * y for x,y in zip(vecA, vecB)]
In [38]:
lista1 = [1,2,3]
lista2 = [4,5,6]
producto_de_listas(lista1, lista2)
Out[38]:
[4, 10, 18]

args y kwargs

  • Distintas formas de empaquetar y desempaquetar argumentos
  • Se pueden combinar de muchas maneras
  • YAGNI = You Aren't Gonna Need It'

Parámetros por default

In [39]:
def imprime(saludo="Hola", persona="Juan"):
    print saludo, persona
    
imprime()
imprime("Chau")
imprime("Como andas?", "Pedro")
Hola Juan
Chau Juan
Como andas? Pedro

Parámetros por nombre

In [40]:
imprime(persona="Miguel")
imprime(persona="Mati", saludo="Adios")
Hola Miguel
Adios Mati

args: lista de parámetros

In [43]:
def imprime_de_todo(*elementos):
    for elemento in elementos:
        print elemento
        
imprime_de_todo("Hola")
imprime_de_todo("Hola", "Como", "Andas")
Hola
Hola
Como
Andas

args: desempaquetado

In [44]:
def recibe_tres_parametros(pUno, pDos, pTres):
    print("a:", pUno, ", b:", pDos, ", c:",pTres)
    
recibe_tres_parametros(1,2,3)
recibe_tres_parametros(*(1,2,3))
recibe_tres_parametros(*[1,2,3])
recibe_tres_parametros(*[1,2,3,4])
('a:', 1, ', b:', 2, ', c:', 3)
('a:', 1, ', b:', 2, ', c:', 3)
('a:', 1, ', b:', 2, ', c:', 3)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-44-7e4904c806b5> in <module>()
      5 recibe_tres_parametros(*(1,2,3))
      6 recibe_tres_parametros(*[1,2,3])
----> 7 recibe_tres_parametros(*[1,2,3,4])

TypeError: recibe_tres_parametros() takes exactly 3 arguments (4 given)

kwargs: dict de parámetros

In [45]:
def imprime_elementos(**elementos):
    for clave, valor in elementos.items():
        print(clave, ":", valor)
In [46]:
imprime_elementos(uno = 1)
('uno', ':', 1)
In [48]:
imprime_elementos(uno = 1, letra_a = "a")
('letra_a', ':', 'a')
('uno', ':', 1)
In [49]:
imprime_elementos(uno = 1, letra_a = "a", una_lista = [])
('una_lista', ':', [])
('letra_a', ':', 'a')
('uno', ':', 1)

kwargs: desempaquetado

In [ ]:
def recibe_tres_parametros(pUno, pDos, pTres):
    print("a:", pUno, ", b:", pDos, ", c:",pTres)
In [50]:
dicc = {"pDos": "que", "pUno": "Hola", "pTres": "tal?"}
recibe_tres_parametros(**dicc)
('a:', 'Hola', ', b:', 'que', ', c:', 'tal?')
In [51]:
dicc2 = {"pDos": 22, "pUno": 11, "pTres": 33}
recibe_tres_parametros(**dicc2)
('a:', 11, ', b:', 22, ', c:', 33)
In [52]:
dicc2 = {"pDos": 22, "pUno": 11, "pTre": 33}
recibe_tres_parametros(**dicc2)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-52-ef3358a4dc18> in <module>()
      1 dicc2 = {"pDos": 22, "pUno": 11, "pTre": 33}
----> 2 recibe_tres_parametros(**dicc2)

TypeError: recibe_tres_parametros() got an unexpected keyword argument 'pTre'

Cadenas con formato

Por posición

In [53]:
'{0}, {1}, {2}'.format('a', 'b', 'c')
Out[53]:
'a, b, c'
In [54]:
'{2}, {1}, {0}'.format('a', 'b', 'c')
Out[54]:
'c, b, a'
In [59]:
'{2}, {1}, {0}'.format(*'abc')
Out[59]:
'c, b, a'
In [60]:
'{0}{1}{0}'.format('abra', 'cad')
Out[60]:
'abracadabra'

Por nombre

In [61]:
'Coordinates: {latitude},{longitude}'.format(latitude='37.24N', longitude='-115.81W')
Out[61]:
'Coordinates: 37.24N,-115.81W'
In [62]:
coord = {'latitude': '37.24N', 'longitude': '-115.81W'}
'Coordinates: {latitude},{longitude}'.format(**coord)
Out[62]:
'Coordinates: 37.24N,-115.81W'

Por atributos

In [63]:
c = 3 + 5j
'Numero: {0}. Parte real: {0.real}.Parte imaginaria {0.imag}.'.format(c)
Out[63]:
'Numero: (3+5j). Parte real: 3.0.Parte imaginaria 5.0.'

Por notación indexada

In [64]:
coord = (3, 5)
'X: {0[0]}; Y:{0[1]}'.format(coord)
Out[64]:
'X: 3; Y:5'

Unit testing

  • unittest es una versión en Python de JUnit. También llamado PyUnit
  • Automatización de test con test runner

Soporta:

  Test case: unidad de prueba
  Test suite: grupo de pruebas que se corren en conjunto
  Test fixture: preparación necesaria para correr pruebas 
  • class TestMiModulo(unittest.TestCase):

  • Convención: la clase comienza con “Test”. Hereda de TestCase, definido en el módulo unittest

  • SetUp: Método que se llama para preparar el test, justo antes que llamar al método del test. Si se lanza alguna excepción (que no sea AssertionError o SkipTest) se toma como un error y no como que falló el test. La implementacion por default es no hacer nada.
  • tearDown: si SetUp() se llama exitosamente esta se llamará sí o sí, sin importar el resultado de la prueba. La implementacion por default es no hacer nada.
  • Cada test debe comenzar con la palabra “test”
  • Correr los test con:
         python TestModulo.py
         python TestModulo.py -v
Método Chequea que Nuevo en
assertEqual(a,b) a==b
assertNotEqual(a,b) a!=b
assertTrue(x) bool(x) is True
assertFalse(x) bool(x) is False
assertIs(a,b) a is b 2.7
assertIsNot(a,b) a is not b 2.7
assertIsNone(x) x is None 2.7
assertIsNotNone(x) x is not None 2.7
assertIn(a,b) a in b 2.7
assertNotIn(a,b) a not in b 2.7
assertIsInstance(a,b) isinstance(a,b) 2.7
assertIsNotInstance(a,b) not isinstance(a,b) 2.7

Próxima Clase

  • Orientación a objetos
  • Excepciones
  • Aserciones