Herramientas de programación funcional

Python proporciona las herramientas básicas de programación funcional: funciones anónimas, map, reduce, filter, closures, etc. Sin embargo, no hace falta aplicar estrictamente el paradigma funcional para sacar partido de estas carácteristicas.

Funciones anónimas

Son funciones sin nombres (obviamente). Se crean con la palabra reservada lambda y permite crear un objeto invocable temporal, es decir, no existía antes y no existirá una vez acabada la invocación. Requiere una lista de argumentos (que puede ser vacia) y un cuerpo (sin return).


In [6]:
(lambda x: x ** 2)(8)


Out[6]:
64

Aunque en la mayoría de los casos se utilice como un argumento para otra función, también puede ser almacenada, comom cualquier objeto.


In [7]:
f = lambda x: x ** 2
f(7)


Out[7]:
49

In [8]:
g = lambda:7
print(g)
print(g())


<function <lambda> at 0x7f7c0c18e9b0>
7

map()

Dados una función y un iterable, la función map() aplica la función a cada elemento del iterable y crea una lista.


In [9]:
help(map)


Help on built-in function map in module __builtin__:

map(...)
    map(function, sequence[, sequence, ...]) -> list
    
    Return a list of the results of applying the function to the items of
    the argument sequence(s).  If more than one sequence is given, the
    function is called with an argument list consisting of the corresponding
    item of each sequence, substituting None for missing values when not all
    sequences have the same length.  If the function is None, return a list of
    the items of the sequence (or a list of tuples if more than one sequence).


In [10]:
def square(x):
    return x ** 2

numbers = range(20)
print(map(square, numbers))


[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361]

Con una función anónima:


In [11]:
print(map(lambda x: x**2, range(20)))


[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361]

In [12]:
print(map(str.upper, "foo bar fuzz".split()))


['FOO', 'BAR', 'FUZZ']

filter()

Dado un iterable y un predicado (una función que devuelve un booleano) retorna los elementos que cumplen el predicado.


In [13]:
help(filter)


Help on built-in function filter in module __builtin__:

filter(...)
    filter(function or None, sequence) -> list, tuple, or string
    
    Return those items of sequence for which function(item) is true.  If
    function is None, return the items that are true.  If sequence is a tuple
    or string, return the same type, else return a list.


In [14]:
filter(lambda x: not x % 3, range(1, 30))


Out[14]:
[3, 6, 9, 12, 15, 18, 21, 24, 27]

In [15]:
filter(lambda x: x.isupper(), "HolA")


Out[15]:
'HA'

In [16]:
filter(str.isupper, "HolA")


Out[16]:
'HA'

In [17]:
str.isupper("A")


Out[17]:
True

reduce()

Aplica una función a una secuencia de valores tomados dos a dos acumulando el resultado.


In [18]:
help(reduce)


Help on built-in function reduce in module __builtin__:

reduce(...)
    reduce(function, sequence[, initial]) -> value
    
    Apply a function of two arguments cumulatively to the items of a sequence,
    from left to right, so as to reduce the sequence to a single value.
    For example, reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) calculates
    ((((1+2)+3)+4)+5).  If initial is present, it is placed before the items
    of the sequence in the calculation, and serves as a default when the
    sequence is empty.


In [19]:
reduce(lambda x,y:x+y, range(5))


Out[19]:
10

In [20]:
import operator
reduce(operator.add, range(5))


Out[20]:
10

In [21]:
f = lambda x: reduce(operator.mul, range(1, x+1), 1)
f(6)


Out[21]:
720

In [22]:
reduce(operator.add, map(lambda x:x**2, range(5)))


Out[22]:
30

Comprensión de listas

Se trata de una expresión que genera una lista, normalmente aplicando una transformación a partir de otro objeto iterable. Es por tanto equivalente a la función map().


In [23]:
# con map
map_squares = map(lambda x: x**2, range(20))
print(map_squares)

# con LC (list comprenhension)
lc_squares = [x**2 for x in range(20)]
print(lc_squares)


[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361]

In [24]:
[2**x for x in range(16)]


Out[24]:
[1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768]

De hecho es más potente que map(), porque se puede aplicar a varios iteradores.


In [25]:
print( [x+y+z for x in 'abc' for y in '12' for z in 'XYZ'] )


['a1X', 'a1Y', 'a1Z', 'a2X', 'a2Y', 'a2Z', 'b1X', 'b1Y', 'b1Z', 'b2X', 'b2Y', 'b2Z', 'c1X', 'c1Y', 'c1Z', 'c2X', 'c2Y', 'c2Z']

Y uno de los iteradores puede depender del otro:


In [26]:
[x for y in [1, 2, 3] for x in range(y+1)]


Out[26]:
[0, 1, 0, 1, 2, 0, 1, 2, 3]

Y es anidable (para crear estructuras anidadas):


In [27]:
[[x+y for x in 'abc'] for y in '12']


Out[27]:
[['a1', 'b1', 'c1'], ['a2', 'b2', 'c2']]

Permite además que la inserción en la lista resultante sea condicionada. Por tanto, más potente que filter():


In [28]:
filter_3_multiples = filter(lambda x: not x % 3, range(1, 30))
print(filter_3_multiples)

lc_3_multiples = [x for x in range(1, 30) if not x % 3]
print(lc_3_multiples)


[3, 6, 9, 12, 15, 18, 21, 24, 27]
[3, 6, 9, 12, 15, 18, 21, 24, 27]

Comprensión de conjuntos

Equivalente a la comprensión de listas para generar conjuntos. En lugar de corchetes "[]" se utilizan llaves "{}":


In [29]:
{ord(x) for x in "foo bar fuzz"}


Out[29]:
{32, 97, 98, 102, 111, 114, 117, 122}

Comprensión de diccionarios

También se especifican con llaves, pero se contruyen pares "clave:valor"


In [30]:
grades ={"john":3, "mary":5}
{key.upper():value for key,value in grades.items()}


Out[30]:
{'JOHN': 3, 'MARY': 5}

In [31]:
grades.items()


Out[31]:
[('john', 3), ('mary', 5)]

In [32]:
{x:ord(x) for x in "hola"}


Out[32]:
{'a': 97, 'h': 104, 'l': 108, 'o': 111}

In [33]:
{category:set() for category in ["vowels", "consonants"]}


Out[33]:
{'consonants': set(), 'vowels': set()}

Decoradores

Un decorador es una función que se utiliza para cambiar el comportamiento de otra, es decir, toma una función como argumento y devuelve otra:


In [34]:
def bob_say(text):
    print("Bob says: " + text)
    
bob_say("hello")


Bob says: hello

Escribamos un decorador que genere una traza de las invocaciones.


In [35]:
def trace(func):
    def deco(text):
        print("-- function '{}' invoked".format(func.__name__))
        func(text)
        print("-- function '{}' ends".format(func.__name__))
    return deco
        
deco_bob_say = trace(bob_say)
deco_bob_say("bye")


-- function 'bob_say' invoked
Bob says: bye
-- function 'bob_say' ends

Pero lo interesante es que los decoradores se pueden aplicar se una forma más compacta en la propia definición de la función:


In [36]:
@trace
def bob_say(text):
    print("Bob says: " + text)
    
bob_say("hello")


-- function 'bob_say' invoked
Bob says: hello
-- function 'bob_say' ends

Una función thread safe:


In [37]:
import threading
lock = threading.Lock()

def syncronized(func):
    def deco():
        try:
            lock.acquire()
            func()
        finally:
            lock.release()
    return deco

@syncronized
def say():
    print("hello")
    
say()


hello

Decoradores con parámetros


In [38]:
import threading

def syncronized(lock):
    def with_lock(func):
        def deco():
            try:
                lock.acquire()
                func()
            finally:
                lock.release()
        return deco
    return with_lock


lock = threading.Lock()
@syncronized(lock)
def say():
    print("hello")
    
say()


hello

Funciones parcialmente especificadas

functools.partial es un functor que permite crear una función a partir de otra, adelantando algunos de los parámetros.


In [39]:
from functools import partial
print(partial.__doc__)


partial(func, *args, **keywords) - new function with partial application
    of the given arguments and keywords.


In [40]:
def my_pow(x, y):
    return x ** y

square = partial(my_pow, y=2)
print(square(8))

cube = partial(my_pow, y=3)
print(cube(3))


64
27