En Python todo es un objeto, incluídas las funciones.
Las funciones son un tipo de callable (invocable),
en particular, son del tipo de callable que se define con la
sintáxis def
.
Una función está formada por una signatura y un bloque de código. La signatura es el nombre de la función y la lista de sus parámetros formales, junto con los valores por defecto y anotaciones.
El bloque de código es el conjunto de instrucciones que forman el cuerpo de la función. El cuerpo de una función define un alcance (scope) para las variables definidas en ellas, que quedan aisladas de los bloques exteriores.
El bloque puede comenzar por una cadena de texto que representa la documentación de la función.
-
Considera la siguiente función:
def simple(): """Returns the answer to the life, universe and everything else.""" return 42
Una función debe tener al menos una sentencia. Esto también son funciones:
def stub(): ... def another_stub(): ...
Ambas devuelven
None
:stub() is None another_stub() is None stub is not another_stub
Podemos visualizar la documentación de una función con la función
help()
:help(simple)
-
El nombre de la función es parte del objeto:
print(simple.__name__) simple is not simple.__name__
-
El nombre de la función es, también, una variable que se refiere al objeto:
id(simple) type(simple) simple()
Su alcance (scope) es el del bloque donde fue definida y el cuerpo de la función (lo que permite hacer llamadas recursivas):
def factorial(n): """Recursively calculate the factorial of a number.""" if n == 0: return 1 return n * factorial(n-1) factorial(10)
-
Como cualquier otro objeto, podemos asignarlo a otras variables:
same_function = simple same_function() same_function is simple same_function.__name__ != 'same_function'
-
Como cualquier otro objeto, podemos pasarlo como parámetro:
def print_name(f): """Print the given name of a function.""" print(f.__name__) print_name(simple) print_name(print_name)
-
El resultado de una función no debe ser confundido con la función en sí:
simple() is not simple simple() == 42 type(simple) type(simple())
Las anotaciones son valores arbitrarios que acompañan a los parámetros y a la propia función sin ningún significado por defecto para Python.
-
Esto son anotaciones válidas:
def func(a: 'some text', b: int) -> (lambda x: x): ...
-
Las anotaciones de una función pueden consultarse en el atributo
__annotations__
:func.__annotations__
- El PEP 3107 explica la motivación y la sintaxis de las anotaciones.
Un invocable es un objeto sobre el que podemos usar la sintaxis de llamada, formalmente definida en la documentación de Python.
-
La sintaxis de llamada consiste en acompañar un objeto invocable con unos paréntesis que encierran una lista de valores llamados parámetros.
print_name(simple)
-
Python fuerza que exista una correspondencia entre los parametros formales y los parámetros reales. No puede haber parámetros de menos:
print_name()
Los parámetros formales son aquellos en la definición de la función y los parámetros reales son aquellos encerrados entre los paréntesis de la llamada de la función.
-
Tampoco puede haber parámetros de más:
print_name(simple, 2)
Pero podemos "recoger" los parámetros extra con un parámetro especial en la signatura de la función, el parámetro "estrella" (stararg):
def print_name(f, *others): """Print the given name of a function.""" print(f'others is of type {type(others)} and contains {others}') print(f.__name__) print_name(simple, range, enumerate)
-
También podemos utilizar los valores recolectados:
def print_name(f, *others): """Print the given name several functions.""" print(f'others is of type {type(others)} and contains {others}') print(f.__name__) for g in others: print(g.__name__) print_name(simple, range, enumerate)
El parámetro estrella sirve para implementar funciones variádicas, es decir, con un número variable de argumentos.
def average(*values): """Calculate the average of the arguments.""" return sum(values) / len(values) average(10) average(10, 20) average(10, 20, 30)
La función
print_names
realiza la misma acción sobre todos sus argumentos. ¿Podrías simplificar su signatura y su implementación? -
Como en el caso de las colecciones, podemos desempaquetar secuencias en las llamadas a una función:
values = [1, 2, 3, 4, 5] average(*values)
Nota: fíjate que la sintaxis de desempaquetado y la sintáxis del parámetro estrella es "la misma". Se desambiguan por el contexto. Un parámetro formal precedido de asterisco es un parámetro estrella siempre. Es en la llamada donde la sintáxis se utiliza para desempaquetar.
Observa también que no es necesario que exista un parámetro estrella para poder desempaquetar secuencias en una llamada:
def solve(a, b, c): """Solves a quadratic equation given the coefficients.""" root = (b**2 - 4*a*c) ** 1/2 return (-b + root)/2*a, (-b - root)/2*a coefficients = (4, 2, 1) solve(*coefficients) solve(8, 4, 2)
-
Python permite el paso de parámetros por nombre explícito:
def nroot(value, root): """Calculate the nth root of value.""" return value ** (1/root) root(49, 2) root(value=49, root=2) root(root=2, value=49)
Distinguiremos entra parámetros con nombre (keyword parameters) y parámetros posicionales (positional parameters).
-
Los parámetros con nombre añaden claridad:
nroot(81, root=3)
Pero sólo pueden aparecer después de los parámetros posicionales:
nroot(root=3, 81) nroot(value=81, 3)
-
De nuevo, debe haber una correspondencia entre los parámetros de la llamada y los parámetros en la signatura. No podemos pasarmás parámetros con nombre de los esperados:
nroot(81, root=3, another_parameter=4)
-
Pero podemos utilizar otro parámetro estrella para recolectar los valores sobrantes:
def print_profile(name, surname, **extra_info): """Format a profile of a person.""" print(f'Profile of: {surname}, {name}') if extra_info: fields = sorted(extra_info.keys()) for field in fields: print(f'\t{field}: {extra_info[field]}') print_profile('Salvador', 'de la Puente') print_profile(surname='de la Puente', name='Salvador') print_profile('Salvador', 'de la Puente', company='IBM Research')
El parámetro estrella para recolectar los parámetros con nombre sobrantes se suele llamar kwargs (del inglés keyword arguments).
De nuevo, no se debe confundir con la sintáxis de desempaquetado. En una signatura, un parámetro precedido de dos asteriscos es siempre un parámetro kwargs.
-
De manera similar a las secuencias, podemos desempaquetar diccionarios en las llamadas a función. Por ejemplo:
extra_data = {'company': 'IBM Research', 'age': 33} print_profile('Salvador', 'de la Puente', **extra_data) complete_data = {'name': 'Salvador', 'surname':'de la Puente', **extra_data} print_profile(**complete_data)
Tampoco es necesario que exista un parámetro kwargs para utilizar el desempaquetado de mapas:
def do_get_request(host, username, password): """Perform an authenticated GET request to an endpoint.""" print(f'Faking GET request to {username}:{password}@{host}') credentials = {'username': 'salva', 'password':'1234'} do_get_request('example.com', **credentials)
Python permite el uso de parámetros por defecto.
-
Considera esta nueva versión de la función que calcula raíces:
def nroot(value, root=2): """Calculate the nth root of value. If omitting root, calculate the square root.""" return value ** (1/root) nroot(49) nroot(81) nroot(81, root=3)
-
Los parámetros por defecto se evalúan una sola vez en el momento de la creación del objeto función y no en el momento de la llamada. Es decir:
def shared_default(list_=[]): list_.append('another item') print(len(list_)) shared_default() shared_default() shared_default()
La recomendación es la de sólo usar tipos inmutables como valores por defecto. Si, de todas formas, quisieras lograr el comportamiento "esperado", ¿cómo lo harías?
Una lambda es una función anónima con una sintáxis simplificada que no require nombre, ni paréntesis en la signatura y que sólo admite una expresión como cuerpo de la función, que además debe escribirse a continuación de la signatura y cuyo resultado es el valor de retorno:
-
Una función lambda también es un objeto:
lambda x: 2*x
-
Una función lambda puede pasarse como parámetro:
print_name(lambda x: 2*x)
-
Una función lambda puede asignarse a una variable:
double = lambda x: 2*x
Pero si esto ocurre, se contradice el propósito de la expresión lambda de ser una función anónima.
-
Una función lambda es un invocable porque es una función:
type(lambda x: 2*x) (lambda x: 2*x)(4)
-
Las funciones lambdas suelen ser útiles cuando se usan como parámetros en funciones "de segundo order":
input = [1, 2, 3, 4] doubles = list(map(lambda x: 2*x, input))
Pero puede ser más expresivo utilizar una función al uso:
def double(x): return 2*x input = [1, 2, 3, 4] doubles = list(map(double, input))
Una clausura o closure es un conjunto de valores para las variables libres de una función. Es decir, aquellas variables no definidas en el cuerpo de la función sino en bloques de funciones exteriores.
def greetings_factory(greeting):
"""Return a function for greeting people with a customize expression."""
def greetings(name):
"""Print a greetings to name."""
return f'{greeting} {name}'
return greetings
spanish_greetings = greetings_factory('Hola')
english_greetings = greetings_factory('Hello')
spanish_greetings.__closure__[0].cell_contents
english_greetings.__closure__[0].cell_contents
spanish_greetings('Salva')
english_greetings('Salva')
Todas la funciones tienen una serie de atributos.
-
Considera esta versión más completa de la función anterior:
from typing import Callable def greetings_factory(greeting: str ='Hi') -> Callable[[str], str]: """Return a function for greeting people with a customize expression.""" def greetings(name: str) -> str: """Print a greetings to name.""" return f'{greeting} {name}' return greetings
-
Ahora crea una nueva función de saludo:
spanish_greetings = greetings_factory('Hola')
-
Y comprueba algunos sus atributos:
spanish_greetings.__doc__ spanish_greetings.__name__ spanish_greetings.__qualname__ spanish_greetings.__module__ spanish_greetings.__defaults__ spanish_greetings.__code__ spanish_greetings.__globals__ spanish_greetings.__dict__ spanish_greetings.__closure__ spanish_greetings.__annotations__ spanish_greetings.__kwdefaults__
-
Además, a las funciones definidas por el usuario con
def
, podemos añadir nuevos atributos como si se tratara de metadatos:def factorial(n): accumulator = 1 for v in range(2, n+1): accumulator *= v return accumulator factorial.version = '2.0' factorial(10) print(factorial.version)
Un decorador es un invocable que devuelve otro invocable. Un decorador recibe como parámetro una función definida por el usuario y, de alguna forma, aumenta su funcionalidad. Por ejemplo:
@cached
def fibonacci(n):
"""Calculate the nth fibonacci number."""
if n == 0 or n == 1:
return 1
return fibonacci(n-1) + fibonacci(n-2)
El decorador cached
podría guardar los resultados de las invocaciones de
forma que segundas invocaciones para valores ya calculados fueran más rápidos.
La sintaxis anterior es tan sólo azúcar sintáctico para el siguiente código:
def fibonacci(n):
"""Calculate the nth fibonacci number."""
if n == 0 or n == 1:
return 1
return fibonacci(n-1) + fibonacci(n-2)
fibonacci = cached(fibonacci)
Vamos a implementar el decorador cached
:
-
Empecemos por un decorador que no hace nada:
def leave_the_same(target): return target @leave_the_same def fibonacci(n): """Calculate the nth fibonacci number.""" if n == 0 or n == 1: return 1 return fibonacci(n-1) + fibonacci(n-2)
-
Una implementación alternativa del mismo comportamiento sería:
def leave_the_same(target): def _decorated(*args, **kwargs): return target(*args, **kwargs) return _decorated @leave_the_same def fibonacci(n): """Calculate the nth fibonacci number.""" if n == 0 or n == 1: return 1 return fibonacci(n-1) + fibonacci(n-2)
-
Sin embargo, esta implementación nos da más juego puesto que ahora podemos hacer cosas antes y después de la invocación. Por ejemplo, imprimir los parámetros y el resultado:
def log(target): def _decorated(*args, **kwargs): print(f'Called with {args} and {kwargs}') result = target(*args, **kwargs) print(f'Returned {result}') return result return _decorated @log def solve(a, b, c): """Solves a quadratic equation given the coefficients.""" root = (b**2 - 4*a*c) ** 1/2 return (-b + root)/2*a, (-b - root)/2*a solve(4, 2, 1) solve(16, 25, c=5)
-
Los decoradores son altamente combinables y podemos crear unos a partir de otros más sencillos:
def log_parameters(target): def _decorated(*args, **kwargs): print(f'Called with {args} and {kwargs}') return target(*args, **kwargs) return _decorated def log_return(target): def _decorated(*args, **kwargs): result = target(*args, **kwargs) print(f'Returned {result}') return result return _decorated def log(target): @log_parameters @log_return def _decorated(*args, **kwargs): return target(*args, **kwargs) return _decorated @log def solve(a, b, c): """Solves a quadratic equation given the coefficients.""" root = (b**2 - 4*a*c) ** 1/2 return (-b + root)/2*a, (-b - root)/2*a solve(4, 2, 1) solve(16, 25, c=5)
-
Como ocurría con el primer ejemplo, un decorador no tiene por qué siempre devolver otra función. Un decorador puede añadir metadatos a la función objetivo:
VERSION = '2.0' def version(target): global VERSION target.version = VERSION return target @version def some_function(): return ...
-
Con lo visto, podemos implementar el comportamiento de la caché:
def cached(target): cache = target.__cache__ = {} def _decorated(n): if n not in cache: cache[n] = target(n) return cache[n] return _decorated
Y comparar:
def test_fibonacci(): def fibonacci(n): """Calculate the nth fibonacci number.""" if n == 0 or n == 1: return 1 return fibonacci(n-1) + fibonacci(n-2) fibonacci(100) fibonacci(100) fibonacci(200) def test_cached_fibonacci(): @cached def fibonacci(n): """Calculate the nth fibonacci number.""" if n == 0 or n == 1: return 1 return fibonacci(n-1) + fibonacci(n-2) fibonacci(100) fibonacci(100) fibonacci(200) import timeit timeit.timeit('test_fibonacci()', globals=globals(), number=1) timeit.timeit('test_cached_fibonacci()', globals=globals(), number=1)
También podemos manipular la caché de la función a través del atributo
__cache__
:cached_fibonacci.__cache__ cached_fibonacci.__cache__.clear()
- Python tiene un decorador
@lru_cache
que implementa una versión más sofisticada de nuestro decorador@cached
. - Documentación del módulo
timeit
El problema, al decorar una función, es que en realidad estamos perdiendo algo de información de la función original.
-
Ejecuta el siguiente código para restaurar la función
solve
a su versión sin decorar:def solve(a, b, c): """Solves a quadratic equation given the coefficients.""" root = (b**2 - 4*a*c) ** 1/2 return (-b + root)/2*a, (-b - root)/2*a
Y comprueba la siguiente información:
solve.__doc__ solve.__name__
-
Ahora ejecuta el siguiente código para decorar la función y vuelve a comprobar los mismos valores:
@log def solve(a, b, c): """Solves a quadratic equation given the coefficients.""" root = (b**2 - 4*a*c) ** 1/2 return (-b + root)/2*a, (-b - root)/2*a
¿Qué le ha pasado a
solve.__doc__
y asolve.__name__
? -
Para restaurar el comportamiento, Python provee del decorador
@functools.wraps
que copia los valores de la función objetivo a la función decoradora:from functools import wraps def log(target): @wraps(target) def _decorated(*args, **kwargs): print(f'Called with {args} and {kwargs}') result = target(*args, **kwargs) print(f'Returned {result}') return result return _decorated @log def solve(a, b, c): """Solves a quadratic equation given the coefficients.""" root = (b**2 - 4*a*c) ** 1/2 return (-b + root)/2*a, (-b - root)/2*a
¿Qué pasa ahora con
solve.__doc__
ysolve.__name__
? -
La función original aun está disponible en el atributo
__wrapped__
:solve(1,2,3) solve.__wrapped__(1,2,3)
Corrige los decoradores de ejemplo de este tema para que envuelvan correctamente las funciones objetivo utilizando
@wraps
.
- Documentación del decorador
@wraps
- Documentación de la función
update_wrapper
- Documentaicón del módulo
functools
Un decorador parametrizado toma argumentos. Tiene esta pinta:
@version('2.0')
def solve(a, b, c):
"""Solves a quadratic equation given the coefficients."""
root = (b**2 - 4*a*c) ** 1/2
return (-b + root)/2*a, (-b - root)/2*a
Y, de nuevo, la sintáxis es equivalente a:
def solve(a, b, c):
"""Solves a quadratic equation given the coefficients."""
root = (b**2 - 4*a*c) ** 1/2
return (-b + root)/2*a, (-b - root)/2*a
solve = version('2.0')(solve)
Eso significa que el resultado de version('2.0')
debe ser un invocable.
-
Considera el decorador
@version(version_string)
que añade el atributoversion
a la función objetivo:def version(version_string): def _decorator(target): target.version = version_string return target return _decorator @version('2.0') def solve(a, b, c): """Solves a quadratic equation given the coefficients.""" root = (b**2 - 4*a*c) ** 1/2 return (-b + root)/2*a, (-b - root)/2*a solve.version == '2.0'
-
Considera ahora el decorador
@log(label)
que añade una etiqueta de la forma[label]
antes de imprimir parámetros y resultado:def log(label): def _decorator(target): @wraps(target) def _decorated(*args, **kwargs): print(f'[{label}] Called with {args} and {kwargs}') result = target(*args, **kwargs) print(f'[{label}] Returned {result}') return result return _decorated return _decorator @log('XXX') def solve(a, b, c): """Solves a quadratic equation given the coefficients.""" root = (b**2 - 4*a*c) ** 1/2 return (-b + root)/2*a, (-b - root)/2*a
Este patrón es en realidad una versión de la función factoría que vimos
con greetings_factory
donde el decorador actúa en realidad como una factoría
de decoradores.
Los decoradores son aun más potentes cuando los aplicamos a clases aunque esto ya lo veremos más adelante. También son útiles para implementar el paradigma de la programación orientada a aspectos.
Con decoradores podemos implementar logging automático, funciones espía, validación de tipos en tiempo de ejecución, generación segura de valores por defecto, generación de documentación, sustitución de parámetros...