Funciones¶
(1)
El concepto de función es básico en prácticamente cualquier lenguaje de programación. Se trata de una estructura que nos permite agrupar código. Persigue dos objetivos claros:
- No repetir fragmentos de código en un programa.
- Reutilizar el código en distintos escenarios.
Una función viene definida por su nombre, sus parámetros y su valor de retorno. Esta parametrización de las funciones las convierten en una poderosa herramienta ajustable a distintas circunstancias. Al invocarla estaremos solicitando su ejecución y obtendremos unos resultados.
Definir una función¶
Para definir (define) una función utilizamos la palabra reservada def
seguida del nombre de la función. A continuación aparecerán 0 o más parámetros separados por comas (entre paréntesis), finalizando la línea con dos puntos :
En la siguiente línea empezaría el cuerpo de la función que puede contener 1 o más sentencias, incluyendo (o no) una sentencia de retorno con el resultado mediante return
.
Dos puntos
Prestar especial atención a los dos puntos :
porque suelen olvidarse en la _definición _ de la función.
Veamos una primera función muy sencilla:
-
- Los nombres de las funciones siguen las mismas reglas que las variables.
- Como regla general, se suelen utilizar verbos en infinitivo para su definicón:
load_data
,store_vaues
,reset_card
,filter_results
,block_request
, ...
- Nótese la indentación del cuerpo de la función.
Invocar una función¶
Para invocar (o «llamar») a una función sólo tendremos que escribir su nombre seguido de paréntesis.
En el caso de la función sencilla (vista anteriormente) sería así:
- La invocación (o llamada) a la función desencadena la ejecución de su código. Por ello obtenemos la salida esperada.
Es importante entender que la definición de la función debe ser previa a su llamada, de lo contrario recibiremos un error:
>>> say_hello()
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[1], line 1
----> 1 say_hello()
NameError: name 'say_hello' is not defined
Retornar un valor¶
Las funciones pueden retornar (o «devolver») un valor. Veamos un ejemplo muy sencillo:
-
- No confundir
return
conprint()
- El valor de retorno de una función nos permite usarlo fuera de su contexto.
- El hecho de añadir
print()
al cuerpo de una función es algo «coyuntural» y no modifica el resultado de la lógica interna.
- No confundir
Pero no sólo podemos invocar a la función directamente, también la podemos asignar a variables y utilizarla:
También la podemos integrar en otras expresiones, por ejemplo en condicionales:
Si una función no incluye un return
de forma explícita, devolverá None
de forma implícita:
Explícito es mejor que implícito
En general, usar return
implícito no se considera una buena práctica salvo que sepamos lo que estamos haciendo. Si la función debe devolver None
es preferible ser explícito y utilizar return None
.
Retornando múltiples valores¶
Una función puede retornar más de un valor. El «secreto» es hacerlo mediante una tupla.
A continuación se muestra un ejemplo muy sencillo de función que retorna dos valores:
0, 1
es una tupla!
Veamos qué ocurre si invocamos esta función:
Por tanto, también podremos aplicar desempaquetado de tuplas sobre el valor retornado por la función:
Funciones auxiliares¶
Es muy probable que en un programa utilicemos distintas funciones para resolver un determinado problema.
En este sentido, hay que entender que podemos definir y utilizar todas aquellas funciones que consideremos necesarias (funciones auxiliares) para conseguir el fin deseado.
Veamos un ejemplo muy sencillo:
flowchart LR
S((Start)) --> run["<tt>run()</tt>"]
run --> f1["<tt>func1()</tt>"]
run --> f2["<tt>func2()</tt>"]
f1 -.->|1| run
f2 -.->|2| run
run ---->|3| E(((End)))
Orden de las funciones
Es importante definir las funciones antes de utilizarlas.
Parámetros y argumentos¶
Si una función no dispusiera de valores de entrada, su comportamiento quedaría muy limitado. Es por ello que los parámetros nos permiten variar los datos que consume una función para obtener distintos resultados.
Vamos a empezar a crear funciones que reciben parámetros. En este primer ejemplo se define una función que recibe un valor numérico y devuelve su raíz cuadrada:
value
es un parámetro.4
es un argumento.
Cuando llamamos a una función, los valores que pasamos se denominan argumentos y se copian en los respectivos parámetros de la función:
Veamos otro ejemplo de función pero ahora con dos parámetros y algo más de lógica de negocio1:
- Esta sentencia
else
es prescindible ya que la sentencia anterior es unreturn
.
Múltiples sentencias de retorno
Nótese que la sentencia return
puede aparecer varias veces en el cuerpo de una función (y no siempre al final). Esta técnica puede ser beneficiosa en determinados escenarios.2
Ejercicio
pypas sum-squares
Argumentos posicionales¶
Los argumentos posicionales son aquellos argumentos que se copian en sus correspondientes parámetros por orden de escritura.
Vamos a mostrar un ejemplo definiendo una función que «construye» una CPU a partir de tres parámetros:
>>> def build_cpu(vendor, num_cores, freq):
... return dict(
... vendor=vendor,
... num_cores=num_cores,
... freq=freq
... )
...
Una posible llamada a esta función —con argumentos posicionales— sería la siguiente:
- Mapeo entre argumentos y parámetros:
AMD
vendor
8
num_cores
2.7
freq
En el uso de argumentos posicionales hay un inconveniente: recordar el orden. Un error en la posición de los argumentos puede dar lugar a resultados indeseados:
Argumentos nominales¶
En esta aproximación los argumentos no son copiados en un orden específico sino que se asignan por nombre a cada parámetro. Esto nos permite evitar el problema de conocer cuál es el orden de los parámetros en la definición de la función. Para utilizarlo, basta con realizar una asignación de cada argumento en la propia llamada a la función.
Veamos la misma llamada que hemos hecho en el ejemplo de «construcción» de la CPU pero ahora utilizando paso de argumentos nominales:
>>> build_cpu(vendor='AMD', num_cores=8, freq=2.7)#(1)!
{'vendor': 'AMD', 'num_cores': 8, 'freq': 2.7}
- Mapeo entre argumentos y parámetros (por su nombre).
Se puede comprobar que el orden de los argumentos no influye en el resultado final:
Argumentos posicionales y nominales¶
Python permite mezclar argumentos posicionales y nominales en la llamada a una función:
Pero hay que tener en cuenta que, en este escenario, los argumentos posicionales siempre deben ir antes que los argumentos nominales. Esto tiene mucho sentido ya que, de no hacerlo así, Python no tendría forma de discernir a qué parámetro corresponde cada argumento:
>>> build_cpu(num_cores=4, 'INTEL', freq=3.1)
Cell In[1], line 1
build_cpu(num_cores=4, 'INTEL', freq=3.1)
^
SyntaxError: positional argument follows keyword argument
Argumentos mutables e inmutables¶
Cuando realizamos modificaciones a los argumentos de una función es importante tener en cuenta si son mutables (listas, diccionarios, conjuntos, ...) o inmutables (tuplas, enteros, flotantes, cadenas de texto, ...) ya que podríamos obtener efectos colaterales no deseados.
Supongamos un ejemplo en el que nos piden escribir una función que reciba una lista y que devuelva sus valores elevados al cuadrado. Una primera aproximación...
>>> values = [2, 3, 4]
>>> def square_it(values):
... for index in range(len(values)):
... values[index] **= 2#(1)!
... return values
>>> square_it(values)
[4, 9, 16]
>>> values#(2)!
[4, 9, 16]
- Aquí estamos modificando la lista de entrada.
- Efectivamente la lista se ha modificado.
Modificaciones controladas
Es totalmente válido implementar una solución como la que hemos visto, pero como norma general no se recomienda que las funciones modifiquen sus argumentos. Habitualmente se suele retornar un resultado con los nuevos valores.
Parámetros con valores por defecto¶
Es posible especificar valores por defecto en los parámetros de una función. En el caso de que no se proporcione un valor al argumento en la llamada a la función, el parámetro correspondiente tomará el valor definido por defecto.
Siguiendo con el ejemplo de la CPU, podemos asignar 2.0GHz como frecuencia por defecto. La definición de la función cambiaría ligeramente:
>>> def build_cpu(vendor, num_cores, freq=2.0):
... return dict(
... vendor=vendor,
... num_cores=num_cores,
... freq=freq
... )
...
Llamada a la función sin especificar frecuencia de CPU:
Llamada a la función indicando una frecuencia concreta de CPU:
Es importante tener presente que los valores por defecto en los parámetros se calculan cuando se define la función, no cuando se ejecuta. Veamos un ejemplo siguiendo con el caso anterior:
>>> DEFAULT_FREQ = 2.0
>>> def build_cpu(vendor, num_cores, freq=DEFAULT_FREQ):
... return dict(
... vendor=vendor,
... num_cores=num_cores,
... freq=freq
... )
...
>>> build_cpu('AMD', 4)
{'vendor': 'AMD', 'num_cores': 4, 'freq': 2.0}
>>> DEFAULT_FREQ = 3.5#(1)!
>>> build_cpu('AMD', 4)
{'vendor': 'AMD', 'num_cores': 4, 'freq': 2.0}
- El hecho de modificar esta «constante» no afecta a la función ya que se definió previamente.
Ejercicio
pypas factorial
Número variable de argumentos¶
Si nos quedamos en lo visto hasta ahora, una función podría recibir un gran número de argumentos, pero siempre sería un número fijo. Sin embargo hay ocasiones en las que necesitamos que una función reciba un número variable (indeterminado) de argumentos.
Python nos ofrece la posibilidad de empaquetar y desempaquetar argumentos cuando estamos invocando a una función, tanto para argumentos posicionales como para argumentos nominales, lo que facilita la gestión de número variable de argumentos.
Supongamos un ejemplo de función que «construye» una hamburguesa :
>>> def make_burguer(*ingredients):#(1)!
... print('Making burguer with...', end=' ')
... print(', '.join(ingredients))#(2)!
...
>>> make_burguer('chicken', 'tomato', 'cheese')
Making burguer with... chicken, tomato, cheese
>>> make_burguer('chicken', 'tomato', 'cheese', 'bacon', 'mayonnaise')
Making burguer with... chicken, tomato, cheese, bacon, mayonnaise
- Se usa el asterisco para indicar un número variable de argumentos posicionales.
ingredients
es una tupla que contiene (empaqueta) los argumentos pasados a la función.
También podríamos llamar a la función desempaquetando argumentos:
>>> ingredients = ['chicken', 'tomato', 'cheese']
>>> make_burguer(*ingredients)#(1)!
Making burguer with... chicken, tomato, cheese
- En este contexto, el asterisco separa los argumentos al llamar a la función.
Supongamos un ejemplo de función que «construye» una hamburguesa pero donde no sólo queremos indicar los ingredientes sino las cantidades de cada ingrediente:
>>> def make_burguer(**ingredients):#(1)!
... print('Making burguer with...', end=' ')
... print(', '.join(f'{qty}g of {ing}' for ing, qty in ingredients.items()))#(2)!
...
>>> make_burguer(chicken=200, tomato=20, cheese=75)
Making burguer with... 200g of chicken, 20g of tomato, 75g of cheese
>>> make_burguer(chicken=200, tomato=20, cheese=75, bacon=80, mayonnaise=15)
Making burguer with... 200g of chicken, 20g of tomato, 75g of cheese, 80g of bacon, 15g of mayonnaise
- Se usa el doble asterisco para indicar un número variable de argumentos nominales.
ingredients
es un diccionario que contiene (empaqueta) los argumentos pasados a la función.
También podríamos llamar a la función desempaquetando argumentos:
>>> ingredients = {'chicken': 200, 'tomato': 20, 'cheese': 75}
>>> make_burguer(**ingredients)#(1)!
Making burguer with... 200g of chicken, 20g of tomato, 75g of cheese
- En este contexto, el doble asterisco separa los argumentos al llamar a la función.
Convenciones¶
Es habitual encontrar definición de funciones genéricas con un número variable de argumentos posicionales y/o nominales:
El hecho de llamar args
a los argumentos posicionales y kwargs
a los argumentos nominales («keyword») sólo es una convención.
Forzando modo de paso de argumentos¶
Si bien Python nos da flexibilidad para pasar argumentos a nuestras funciones en modo nominal o posicional, existen opciones para forzar que dicho paso sea obligatorio para una determinada modalidad.
A partir de Python 3.0 se ofrece la posibilidad de obligar a que determinados parámetros de la función sean pasados sólo por nombre.
Para ello, en la definición de los parámetros de la función, tendremos que incluir un parámetro especial asterisco *
que delimitará el tipo de parámetros. Así, todos los parámetros a la derecha del asterisco estarán obligados a ser nominales:
Veamos un sencillo ejemplo con una función que construye un nombre:
>>> def fullname(name, surname, *, to_upper=False):
... result = f'{name} {surname}'
... if to_upper:
... result = result.upper()
... return result
...
>>> fullname('John', 'Romita')
'John Romita'
>>> fullname(name='John', surname='Romita')
'John Romita'
>>> fullname('John', 'Romita', to_upper=True)
'JOHN ROMITA'
>>> fullname('John', 'Romita', True)#(1)!
Traceback (most recent call last):
Cell In[5], line 1
fullname('John', 'Romita', True)
TypeError: fullname() takes 2 positional arguments but 3 were given
- Esta llamada no está permitida ya que el parámetro
to_upper
debe pasarse como nominal.
A partir de Python 3.8 se ofrece la posibilidad de obligar a que determinados parámetros de la función sean pasados sólo por posición.
Para ello, en la definición de los parámetros de la función, tendremos que incluir un parámetro especial barra /
que delimitará el tipo de parámetros. Así, todos los parámetros a la izquierda del delimitador estarán obligados a ser posicionales:
Veamos un sencillo ejemplo con una función que construye un nombre:
>>> def fullname(name, surname, /, to_upper=False):
... result = f'{name} {surname}'
... if to_upper:
... result = result.upper()
... return result
...
>>> fullname('John', 'Romita')
'John Romita'
>>> fullname('John', 'Romita', True)
'JOHN ROMITA'
>>> fullname('John', 'Romita', to_upper=True)
'JOHN ROMITA'
>>> fullname(name='John', surname='Romita', to_upper=True)#(1)!
Traceback (most recent call last):
Cell In[5], line 1
fullname(name='John', surname='Romita', to_upper=True)
TypeError: fullname() got some positional-only arguments passed as keyword arguments: 'name, surname'
- Esta llamada no está permitida ya que los parámetros
name
ysurname
deben pasarse como posicionales.
Si mezclamos las dos estrategias anteriores podemos forzar a que una función reciba argumentos de un único modo.
Veamos esta aproximación aplicada al ejemplo de la función que construye nombres:
>>> def fullname(name, surname, /, *, to_upper=False):
... result = f'{name} {surname}'
... if to_upper:
... result = result.upper()
... return result
...
>>> fullname('John', 'Romita')
'John Romita'
>>> fullname('John', 'Romita', to_upper=True)
'JOHN ROMITA'
>>> fullname('John', 'Romita', True)
Traceback (most recent call last):
Cell In[4], line 1
fullname('John', 'Romita', True)
TypeError: fullname() takes 2 positional arguments but 3 were given
>>> fullname(name='John', surname='Romita', to_upper=True)
Traceback (most recent call last):
Cell In[42], line 1
fullname(name='John', surname='Romita', to_upper=True)
TypeError: fullname() got some positional-only arguments passed as keyword arguments: 'name, surname'
Ejercicio
pypas consecutive-freqs
Funciones como parámetros¶
Las funciones se pueden utilizar en cualquier contexto de nuestro programa. Son objetos que pueden ser asignados a variables, usados en expresiones, devueltos como valores de retorno o pasados como argumentos a otras funciones.
Veamos un primer ejemplo en el que pasamos una función como argumento:
>>> def success():
... print('Yeah!')
...
>>> type(success)
<class 'function'>
>>> def doit(func):#(1)!
... func()#(2)!
...
>>> type(doit)
<class 'function'>
>>> doit(success)#(3)!
Yeah!
- En este contexto
func
es un parámetro de la funcióndoit()
. - Dado que
func
es una función, podemos invocarla. - Pasamos la función
success
como argumento.
Veamos un segundo ejemplo en el que pasamos, no sólo una función como argumento, sino los valores con los que debe operar:
>>> def success(msg):
... print(f'{msg}. Yeah!')
...
>>> type(success)
<class 'function'>
>>> def doit(func, func_arg):#(1)!
... func(func_arg)#(2)!
...
>>> type(doit)
<class 'function'>
>>> doit(success, 'Functions as params')#(3)!
Functions as params. Yeah!
- En este contexto
func_arg
es un argumento de la funcióndoit()
pero en realidad será el argumento de la funciónfunc()
. - Dado que
func
es una función, podemos invocarla (con sus argumentos). - Pasamos la función
success
y el mensaje'Functions as params'
como argumentos.
Documentación¶
Ya hemos visto que en Python podemos incluir comentarios para explicar mejor determinadas zonas de nuestro código.
Del mismo modo podemos (y en muchos casos debemos) adjuntar documentación a la definición de una función incluyendo una cadena de texto («docstring») al comienzo de su cuerpo.
Empecemos por un primer ejemplo de documentación en una función:
>>> def closest_int(value):
... 'Returns the closest integer to the given value'
... floor = int(value)
... if value - floor < 0.5:
... return floor
... return floor + 1
...
>>> closest_int(3.1)
3
>>> closest_int(3.7)
4
Sin embargo la forma más ortodoxa de escribir un «docstring» es utilizando triples comillas:
>>> def closest_int(value):
... """Returns the closest integer to the given value.
... The operation is:
... 1. Compute distance to floor.
... 2. If distance less than a half, return floor.
... Otherwise, return ceil.
... """
... floor = int(value)
... if value - floor < 0.5:
... return floor
... else:
... return floor + 1
...
Si utilizamos help
sobre una función, Python nos devolverá su «docstring» correspondiente:
>>> help(closest_int)#(1)!
Help on function closest_int in module __main__:
closest_int(value)
Returns the closest integer to the given value.
The operation is:
1. Compute distance to floor.
2. If distance less than a half, return floor.
Otherwise, return ceil.
-
- Otra forma de «pedir ayuda» es:
- Esto no sólo se aplica a funciones propias, sino a cualquier otra función definida en el lenguaje.
- Si queremos ver el «docstring» de una función «en crudo» (sin formatear), podemos usar
func.__doc__
Descripción de parámetros¶
Como ya se ha visto, es posible documentar una función utilizando un «docstring». Pero la redacción y el formato de esta cadena de texto puede ser muy variada.
Existen distintas formas de documentar una función (u otros objetos)3:
Tipo de formato | Descripción | Soporta Sphinx |
---|---|---|
reStructuredText | Formato de documentación recomendado por Python. | |
Formato de documentación utilizado por Google. | ||
NumPy | Formato de documentación utilizado por NumPy4. | |
Epytext | Formato de documentación utilizado por Epydoc5. |
Aunque cada uno tienes sus particularidades, todos comparten una misma estructura:
- Una primera línea de descripción de la función.
- A continuación especificamos las características de los parámetros (incluyendo sus tipos).
- Por último, indicamos si la función retorna un valor y sus características.
Aunque todos los formatos son válidos, nos centraremos en reStructuredText por ser el estándar propuesto por Python para la documentación.
Sphinx¶
Sphinx es una herramienta para generar documentación usando el lenguaje reStructuredText (RST). Incluye un módulo «built-in» denominado autodoc
el cual permite la autogeneración de documentación a partir de los «docstrings» definidos en el código.
Veamos un ejemplo de aplicación de este formato de documentación sobre la función definida previamente:
def power(x, n):
"""Calculates powers of numbers.
:param x: number representing the base of the operation
:type x: int
:param n: number representing the exponent of the operation
:type n: int
:return: x raised to the power of n
:rtype: int
"""
result = 1
for _ in range(n):
result *= x
return result
Si preparamos un proyecto sobre Sphinx y generamos la documentación, obtendríamos algo similar a la siguiente página:
Read the Docs
La plataforma Read the Docs aloja la documentación de gran cantidad de proyectos. En muchos de los casos se han usado «docstrings» con el formato Sphinx visto anteriormente. Un ejemplo de esta documentación es la popular librería de Python requests.
Anotación de tipos¶
Las anotaciones de tipos (o «type-hints») se introdujeron en Python 3.5 y permiten indicar tipos para los parámetros de una función y/o para su valor de retorno (aunque también funcionan en creación de variables).
Veamos un ejemplo en el que creamos una función para dividir una cadena de texto por la posición especificada en el parámetro:
>>> def ssplit(text: str, split_pos: int) -> tuple:#(1)!
... 'Split text at the index given by split_post'
... return text[:split_pos], text[split_pos:]
...
>>> ssplit('Always remember us this way', 15)
('Always remember', ' us this way')
-
- Cada parámetro incluye dos puntos
:
y el tipo de dato que «debería» recibir. - Para el valor de retorno utilizamos una flecha
->
- Cada parámetro incluye dos puntos
Probemos ahora el siguiente código:
¿Cómo ha podido funcionar si ssplit()
espera ver una cadena de texto y estamos pasando una lista de enteros?
Esto ocurre porque lo que hemos definido es simplemente una anotación de tipo, no una declaración de tipo6. Lo podríamos ver como una forma más de documentar la función.
Valores por defecto¶
Al igual que ocurre en la definición ordinaria de funciones, cuando usamos anotaciones de tipos también podemos indicar un valor por defecto para los parámetros.
Veamos la forma de hacerlo continuando con el ejemplo anterior:
>>> def ssplit(text: str, split_pos: int = None) -> tuple:#(1)!
... """
... Split text at the index given by split_post.
... If split_pos is not defined, text will be splitted by half.
... """
... if split_pos is None:
... split_pos = len(text) // 2
... return text[:split_pos], text[split_pos:]
...
>>> ssplit('Always remember us this way')
('Always rememb', 'er us this way')
- Simplemente se añade el valor por defecto después del tipo.
Tipos compuestos¶
Hay escenarios en los que necesitamos más expresividad de cara a la anotación de tipos. Por ejemplo ¿qué ocurre si queremos indicar una lista de cadenas de texto o un conjunto de enteros?
En la siguiente tabla se muestran distintos métodos para anotaciones de tipos compuestos:
Anotación | Ejemplo |
---|---|
list[str] |
['A', 'B', 'C'] |
set[int] |
{4, 3, 9} |
dict[str, float] |
{'x': 3.786, 'y': 2.198, 'z': 4.954} |
tuple[str, int] |
('Hello', 10) |
tuple[float, ...] |
(7.11,) (4.31, 6.87) (1.23, 5.21, 3.62) |
Múltiples tipos¶
A partir de Python 3.10 podemos indicar que un parámetro puede ser de un tipo o de otro utilizando el operador |
.
Veamos algunos ejemplos válidos:
Anotación | Significado |
---|---|
tuple|dict |
Tupla o diccionario |
list[str|int] |
Lista de cadenas de texto y/o enteros |
set[int|float] |
Conjunto de enteros y/o flotantes |
Número variable de argumentos¶
Cuando trabajamos con funciones que pueden recibir un número variable de argumentos las anotaciones de tipo sólo deben hacer referencia al tipo que contiene la tupla, no es necesario indicar que se trata de una tupla (empaquetada).
En el siguiente ejemplo se define una función que calcula el máximo de una serie de valores enteros o flotantes, pero no indicamos que se reciben como tupla:
Ejercicio
pypas mcount
Tipos de funciones¶
En este apartado veremos los distintos tipos de funciones existentes en Python y sus características.
Funciones anónimas «lambda»¶
Una función «lambda» tiene las siguientes propiedades:
- Se escribe en una única sentencia (línea).
- No tiene nombre (por eso es anónima).
- Su cuerpo conlleva un
return
implícito. - Puede recibir cualquier número de parámetros.
Veamos un primer ejemplo de función «lambda» que permite contar el número de «palabras» de una cadena de texto dada:
- En funciones «lambda» hay una cierta «licencia» para abreviar nombres de variables y que no ocupen tanto espacio.
Visto así quizás haya dudas de su escritura, pero veamos cuál es la transformación que se ha llevado a cabo:
A continuación probamos el comportamiento de la función anónima «lambda» creada previamente:
>>> num_words = lambda t: len(t.split())#(1)!
>>> type(num_words)#(2)!
<class 'function'>
>>> num_words
<function <lambda> at 0x103ca9da0>
>>> num_words('This is a lambda function')#(3)!
5
- Para poder invocarla, es necesario asignar una variable.
- Obviamene se trata de una función.
- La llamada es análoga a la de una función «tradicional».
Una «lambda» como argumento¶
Las funciones «lambda» son habitualmente utilizadas como argumentos a otras funciones.
Un claro ejemplo de ello es la función sorted()
que recibe un parámetro opcional key
donde se define la clave de ordenación.
Partimos de una tupla con pares latitud-longitud:
>>> geoloc = (
... (15.623037, 13.258358),
... (55.147488, -2.667338),
... (54.572062, -73.285171),
... (3.152857, 115.327724),
... (-40.454262, 172.318877)
... )
Ahora veamos el comportamiento de la ordenación en función de la clave indicada:
Ejercicio
pypas order-by-age
Enfoque funcional¶
Como ya se comentó aquí Python es un lenguaje de programación multiparadigma. Uno de los paradigmas7 menos explotados en este lenguaje es la programación funcional.
Python nos ofrece tres funciones que encajan verdaderamente bien en este enfoque: map()
, filter()
y reduce()
:
Esta función aplica otra función sobre cada elemento de un iterable:
>>> map_gen = map(lambda x: 2*x, range(1, 6))#(1)!
>>> map_gen#(2)!
<map at 0x10781b0a0>
>>> list(map_gen)#(3)!
[2, 4, 6, 8, 10]
- Utilizamos una función «lambda» sobre
map
. map()
retorna una especie de generador.- Al convertirlo a lista podemos ver el resultado esperado.
Lista por comprensión
Este comportamiento se puede implementar igualmente con una lista por comprensión:
Esta función selecciona los elementos de un iterable que cumplen una determinada condición:
>>> filter_gen = filter(lambda x: x > 2, range(1, 6))#(1)!
>>> filter_gen#(2)!
<filter at 0x1078e7df0>
>>> list(filter_gen)#(3)!
[3, 4, 5]
- Utilizamos una función «lambda» sobre
filter
. filter()
retorna una especie de generador.- Al convertirlo a lista podemos ver el resultado esperado.
Lista por comprensión
Este comportamiento se puede implementar igualmente con una lista por comprensión:
Esta función reduce el resultado aplicando sucesivamente una función sobre un iterable:
- Importamos la función
reduce()
desde el módulofunctools
. - Utilizamos una función «lambda» sobre
reduce
.
Hazlo pitónico¶
Trey Hunner explica en una de sus «newsletters» lo que él entiende por código pitónico:
Código pitónico
Pitónico es un término extraño que significa diferentes cosas para diferentes personas. Algunas personas piensan que código pitónico va sobre legibilidad. Otras personas piensan que va sobre adoptar características particulares de Python. Mucha gente tiene una definición difusa que no va sobre legibilidad ni sobre características del lenguaje.
Yo normalmente uso el término código pitónico como un sinónimo de código idiomático o la forma en la que la comunidad de Python tiende a hacer las cosas cuando escribe Python. Eso deja mucho espacio a la interpretación, ya que lo que hace algo idiomático en Python no está particularmente bien definido.
Yo argumento que código pitónico implica adoptar el desempaquetado de tuplas, usar listas por comprensión cuando sea apropiado, usar argumentos nominales cuando tenga sentido, evitar el uso excesivo de clases, usar las estructuras de iteración adecuadas o evitar recorrer mediante índices.
Para mí, código pitónico significa intentar ver el código desde la perspectiva de las herramientas específicas que Python nos proporciona, en oposición a la forma en la que resolveríamos el mismo problema usando las herramientas que nos proporciona JavaScript, Java, C, ...
Generadores¶
Un generador es un artefacto que se encarga de generar «valores» que podemos tratar de manera individual (y aislada).
Es decir, no construye una secuencia de forma explícita, sino que nos permite ir «consumiendo» un valor de cada vez. Esta propiedad los hace idóneos para situaciones en las que el tamaño de las secuencias podría tener un impacto negativo en el consumo de memoria.
De hecho ya hemos visto algunos generadores y los hemos estado usando sin ser del todo conscientes. Un ejemplo de ello es range()
8 que ofrece la posibilidad de crear secuencias de números.
Básicamente existen dos implementaciones de generadores:
- Funciones generadoras.
- Expresiones generadoras.
Recordar el estado
A diferencia de las funciones ordinarias, los generadores tienen la capacidad de «recordar» su estado para recuperarlo en la siguiente iteración y continuar devolviendo nuevos valores.
Funciones generadoras¶
Una función generadora es una factoría de generadores, o dicho de otra manera, es una función que devuelve generadores.
Se escribe exactamente igual que una función ordinaria salvo por el hecho de que en vez de la sentencia return
aquí vamos a utilizar yield
.
Veamos un ejemplo en el que escribimos una función generadora de números pares:
>>> def evens(lim: int):
... for num in range(0, lim + 1, 2):
... yield num#(1)!
...
>>> type(evens)#(2)!
function
>>> evens_gen = evens(20)#(3)!
>>> type(evens_gen)#(4)!
generator
-
- Se retorna
num
sea cual sea su valor en este momento. - La ejecución de la función se congela (hasta la próxima llamada).
- Se retorna
- Efectivamente
evens
es una función. - Usamos la factoría de generadores
evens()
para crear un generador conlim=20
. - Efectivamente
evens_gen
es un generador.
¿Cómo obtengo entonces los valores «finales» a partir de un generador? Hay dos enfoques para resolver esta pregunta:
-
- Cada vez que iteramos sobre el generador, solicitamos un nuevo valor.
- Efectivamente la función se congela hasta la próxima «petición».
- El bucle acaba cuando ya no quedan valores que devolver desde el generador.
Los generadores se agotan
Es importante entender que los generadores se agotan. Es decir, una vez que hayamos consumido todos sus elementos, no obtendremos nuevos valores:
>>> evens_gen = evens(10)
>>> for even in evens_gen:
... print(even)
...
0
2
4
6
8
10
>>> list(evens_gen)#(1)!
[]
- Obtenemos la lista vacía porque los valores ya han sido generados (y consumidos) en el bucle anterior.
Ejercicio
pypas genfun-squares
Expresiones generadoras¶
Una expresión generadora es sintácticamente muy similar a una lista por comprensión, pero utilizamos paréntesis en vez de corchetes.
Podemos tratar de reproducir el ejemplo visto en funciones generadoras donde se generaban números pares:
>>> evens_gen = (n for n in range(0, 20, 2))
>>> type(evens_gen)#(1)!
generator
>>> for even in evens_gen:
... print(even)
...
0
2
4
6
8
10
12
14
16
18
- Hay que tener en cuenta que una expresión generadora es ya un generador, por tanto su aplicación es directa.
Una expresión generadora se puede explicitar9, sumar, buscar su máximo o su mínimo, o lo que queramos, tal y como lo haríamos con un iterable cualquiera:
>>> list(n for n in range(0, 20, 2))
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
>>> sum(n for n in range(0, 20, 2))
90
>>> min(n for n in range(0, 20, 2))
0
>>> max(n for n in range(0, 20, 2))
18
Ejercicio
pypas genexp-squares
Funciones interiores¶
Está permitido definir una función dentro de otra función. Es lo que se conoce como función interior.
Veamos un ejemplo en el que extraemos las palabras de un texto que contienen todas las vocales, haciendo uso de una función interior que nos indica si la palabra contiene todas las vocales:
>>> def get_words_with_all_vowels(text: str) -> list[str]:
... def has_all_vowels(word: str, vowels: str = 'aeiou') -> bool:#(1)!
... return len(set(vowels) - set(word.lower())) == 0
...
... return [w for w in text.split() if has_all_vowels(w)]#(2)!
...
>>> get_words_with_all_vowels('La euforia de ver el riachuelo fue inmensa')
['euforia', 'riachuelo']
- Una función interior no se diferencia en nada de una función «clásica» simplemente está dentro de otra.
- Llamada a nuestra función interior.
Clausuras¶
Una clausura (del término inglés «closure») establece el uso de una función interior que se genera dinámicamente y recuerda los valores de los argumentos con los que fue creada.
Veamos una clausura en acción para un ejemplo de tablas de multiplicar:
>>> def make_multiplier_of(n: int):
... def multiplier(x: int) -> int:#(1)!
... return x * n#(2)!
... return multiplier#(3)!
...
>>> m3 = make_multiplier_of(3)#(4)!
>>> type(m3)
function
>>> m3(7)#(5)!
21
>>> m5 = make_multiplier_of(5)#(6)!
>>> type(m5)
function
>>> m5(8)#(7)!
40
>>> make_multiplier_of(7)(8)#(8)!
56
- Función interior que se genera dinámicamente.
- El valor de
n
viene determinado por el parámetron
que recibe la funciónmake_multiplier_of()
- ¡Se devuelve una función!
m3
es una función que da la tabla de multiplicar del 3.- \(3 \cdot 7 = 21\)
m5
es una función que da la tabla de multiplicar del 5.- \(8 \cdot 5 = 40\)
-
- Aunque menos frecuente, también es posible hacer la
doblellamada directamente. - \(7 \cdot 8 = 56\)
- Aunque menos frecuente, también es posible hacer la
Factoría de funciones
En una clausura se retorna una función, no un valor. Es por esto que se dice que una clausura es una factoría de funciones.
Decoradores¶
Hay situaciones en las que necesitamos modificar el comportamiento de funciones existentes pero sin alterar su código. Para estos casos es muy útil usar decoradores.
Un decorador es una función que recibe como parámetro una función y devuelve otra función (se podría ver como un caso particular de una clausura).
El esqueleto básico de un decorador es el siguiente:
def my_decorator(func):#(1)!
def wrapper(*args, **kwargs):#(2)!
... #(3)!
return func(*args, **kwargs)#(4)!
... #(5)!
return wrapper#(6)!
-
Cada función envuelve («wraps») a la siguiente:
-
Función interior que «enmascara» a la función de decorada y recibe sus argumentos (posicionales y nominales).
- Acciones a tomar antes de invocar a la función decorada.
- Llamada a la función decorada.
- Acciones a tomar después de invocar a la función decorada.
- Se devuelve la función interior que modifica la función original.
Veamos un ejemplo de decorador que convierte el resultado numérico de una función a su representación binaria:
>>> def binarize(func):
... def wrapper(*args, **kwargs):
... result = func(*args, **kwargs)
... return bin(result)
... return wrapper
...
Este decorador lo podemos aplicar a cualquier función. Supongamos que lo queremos aplicar sobre la siguiente:
>>> def add(a, b):
... return a + b
...
>>> bin_add = binarize(add)#(1)!
>>> add(3, 6)#(2)!
9
>>> bin_add(3, 6)#(3)!
'0b1001'
- Aplicamos el decorador y obtenemos una nueva función modificada (decorada).
-
- La función original tiene un comportamiento normal.
- \(3+6=9\)
-
- La función decorada devuelve el resultado en base binaria.
- \(bin(3+6)=bin(9)=1001\)
Usando @
para decorar¶
Python nos ofrece un «syntactic sugar» para simplificar la aplicación de los decoradores a través del operador @
justo antes de la definición de la función que queremos decorar.
Veamos su aplicación con el ejemplo del decorador creado previamente:
- Aplicación del decorador.
- La función
add()
ya está modificada con lo que su resultado es directamente en binario.
Manipulando argumentos¶
Supongamos un ejemplo en el que queremos implementar un decorador que pase a minúsculas argumentos de tipo cadena de texto.
El planteamiento varía según el número de argumentos que queramos manipular. Para funciones con...
Si queremos manipular únicamente el primer argumento en funciones de 1 argumento...
>>> def args_to_lower(func):
... def wrapper(data):#(1)!
... return func(data.lower())#(2)!
... return wrapper
...
>>> @args_to_lower
... def count_a(text: str) -> str:
... return sum(c == 'a' for c in text)
...
>>> count_a('I USUALLY see you in the area EVERY EVENING')
3
- Como sólo vamos a decorar funciones con un parámetro, lo recibimos aquí en
data
. - Llamamos a la función decorada pasando previamente su argumento a minúsculas.
Si queremos manipular únicamente el primer argumento en funciones con 2 argumentos...
>>> def args_to_lower(func):
... def wrapper(data, arg):#(1)!
... return func(data.lower(), arg)#(2)!
... return wrapper
...
>>> @args_to_lower
... def count_char(text: str, char: str) -> str:
... return sum(c == char for c in text)
...
>>> count_char('I USUALLY see you in the area EVERY EVENING', 'e')
8
-
- Como sólo vamos a decorar funciones con dos parámetros, los recibimos aquí en
data
yarg
. - El parámetro
data
debe ser la cadena de texto mientras quearg
es «otro parámetro».
- Como sólo vamos a decorar funciones con dos parámetros, los recibimos aquí en
- Llamamos a la función decorada pasando previamente el primer argumento a minúsculas y el segundo tal cual está.
Si queremos manipular todos los argumentos posicionales...
>>> def args_to_lower(func):
... def wrapper(*args):#(1)!
... mod_args = [a.upper() if isinstance(a, str) else a for a in args]#(2)!
... return func(*mod_args)#(3)!
... return wrapper
...
>>> @args_to_lower
... def count_chars(text: str, *chars) -> str:
... return sum(c in chars for c in text)
...
>>> count_chars('I USUALLY see you in the area EVERY EVENING', 'a', 'i', 'u')
9
- Los argumentos posicionales se capturan con
*args
. - Pasamos a minúsculas los argumentos de tipo cadena de texto.
- Llamamos a la función decorada desempaquetando los argumentos modificados.
Si queremos manipular todos los argumentos posicionales y nominales...
>>> def args_to_lower(func):
... def wrapper(*args, **kwargs):#(1)!
... mod_args = [a.upper() if isinstance(a, str) else a for a in args]#(2)!
... mod_kwargs = {k: v.lower() if isinstance(v, str) else v for k, v in kwargs.items()}#(3)!
... return func(*mod_args, **mod_kwargs)#(4)!
... return wrapper
...
>>> @args_to_lower
... def count_chars(text: str, *chars, exclude: bool = False) -> str:
... if exclude:
... return sum(c not in chars for c in text)
... return sum(c in chars for c in text)
...
>>> count_chars('I USUALLY see you in the area EVERY EVENING', 'a', 'i', 'u', exclude=True)
34
-
- Los argumentos posicionales se capturan con
*args
. - Los argumentos nominales se capturan con
**kwargs
.
- Los argumentos posicionales se capturan con
- Pasamos a minúsculas los argumentos posicionales de tipo cadena de texto.
- Pasamos a minúsculas los argumentos nominales de tipo cadena de texto.
- Llamamos a la función decorada desempaquetando los argumentos modificados.
Ejercicio
pypas deco-abs
Múltiples decoradores¶
Python permite aplicar múltiples decoradores sobre una misma función. Lo más difícil aquí es entender el orden de ejecución.
Veamos el siguiente ejemplo para comprobar el orden en el que se ejecutan los decoradores:
>>> def deco1(func):
... def wrapper():
... print('Running deco1 before function')
... func()
... print('Running deco1 after function')
... return wrapper
...
>>> def deco2(func):
... def wrapper():
... print('Running deco2 before function')
... func()
... print('Running deco2 after function')
... return wrapper
...
>>> @deco1
... @deco2
... def my_function():
... print('Running function')
...
>>> my_function()
Running deco1 before function
Running deco2 before function
Running function
Running deco2 after function
Running deco1 after function
Cuando se aplican múltiples decoradores a una función, se ejecutan en orden de arriba hacia abajo, pero se aplican en orden de abajo hacia arriba.
Veamos un ejemplo más concreto con un par de operaciones aritméticas.
Definimos los siguientes decoradores:
>>> def plus5(func):
... def wrapper(*args, **kwargs):
... result = func(*args, **kwargs)
... return result + 5
... return wrapper
...
>>> def div2(func):
... def wrapper(*args, **kwargs):
... result = func(*args, **kwargs)
... return result // 2
... return wrapper
...
Aplicamos los decoradores a una función producto:
-
\[\frac{(4 \cdot 3)}{2}+5 = \frac{12}{2}+5 = 6+5 = 11\]
graph LR
s[prod*] --> p5[plus5]
p5 --> d2[div2]
d2 --> f[prod]
f -->|12| d2
d2 -->|6| p5
p5 -->|11| s
Decoradores con parámetros¶
El último «salto mortal» dentro del mundo de los decoradores sería definir decoradores con parámetros.
El esqueleto básico de un decorador con parámetros es el siguiente:
>>> def my_decorator_with_params(*deco_args, **deco_kwargs):
... def decorator(func):
... def wrapper(*args, **kwargs):
... return func(*args, **kwargs)
... return wrapper
... return decorator
...
Factoría de decoradores
Nótese que my_decorator_with_params()
no es exactamente un decorador sino que es una factoría de decoradores (clausura) que devuelve un decorador según los argumentos pasados.
Supongamos un ejemplo donde queremos implementar un decorador que convierta el resultado de una función a una base determinada:
>>> def basify(base: int):
... def decorator(func):
... def wrapper(*args, **kwargs):
... result = func(*args, **kwargs)
... match base:
... case 2:
... result = bin(result)
... case 8:
... result = oct(result)
... case 16:
... result = hex(result)
... case _:
... result = None
... return result
... return wrapper
... return decorator
...
Ahora podemos aplicarlo variando la base de representación:
- \(\equiv\)
bin(349+125)
- \(\equiv\)
oct(349+125)
Ejercicio
pypas deco-sort
Funciones recursivas¶
La recursividad es el mecanismo por el cual una función se llama a sí misma.
Si sólo tuviéramos esto en cuenta podría ocurrirnos algo así...
>>> def yeah():
... print('Yeah!')
... yeah()
...
>>> yeah()
Yeah!
Yeah!
Yeah!
Yeah!
Yeah!
... 2995 times
Cell In[1], line 3, in yeah()
1 def yeah():
2 print('Yeah!')
----> 3 yeah()
RecursionError: maximum recursion depth exceeded
Límite de recursión
Podemos observar que existe un número máximo de llamadas recursivas. Python controla esta situación por nosotros, ya que, de no ser así, podríamos deteriorar el funcionamiento del sistema consumiendo excesivos recursos.
Veamos ahora un ejemplo concreto en el que tratamos de calcular una potencia \(x^n\) de manera recursiva.
La idea detrás de esto es «sencilla» si manejamos ciertas nociones básicas de cálculo:
Cuando trabajamos con recursividad hay que detectar (al menos) dos casos:
- El caso base o condición de parada \((A)\).
- El caso recursivo \((B)\).
Veamos una posible implementación pensando en base
\(\equiv x\) y exponent
\(\equiv n\)
>>> def pow(base: int, exponent: int) -> int:
... if exponent == 0:#(1)!
... return 1
... return base * pow(base, exponent - 1)#(2)!
...
>>> pow(2, 4)
16
>>> pow(3, 5)
243
- Caso base condición de parada \((A)\).
- Caso recursivo \((B)\).
La «pila de llamadas» para el ejemplo de pow(2, 4)
sería la siguiente:
graph TD
s((Start)) --> pow24["<tt>pow(2,4)</tt>"]
pow24 --> pow23["<tt>pow(2,3)</tt>"]
pow23 --> pow22["<tt>pow(2,2)</tt>"]
pow22 --> pow21["<tt>pow(2,1)</tt>"]
pow21 --> pow20{{"<tt>pow(2,0)</tt>"}}
pow20 -.->|1| pow21
pow21 -.->|2 * 1 = 2| pow22
pow22 -.->|2 * 2 = 4| pow23
pow23 -.->|2 * 4 = 8| pow24
pow24 -.->|2 * 8 = 16| s
Ejercicio
pypas factorial-recursive
Otra aproximación a la recursividad se da en problemas donde tenemos que procesar una secuencia de elementos.
Supongamos por ejemplo que nos piden calcular la suma de las longitudes de una serie de palabras definidas en una lista:
>>> def get_size(words: list[str]) -> int:
... if len(words) == 0:
... return 0
... return len(words[0]) + get_size(words[1:])
...
>>> words = ['this', 'is', 'recursive']
>>> get_size(words)
15
La idea recursiva que hay detrás de esta implementación es que la longitud de todos los elementos de la lista es igual a la longitud del primer elemento más la longitud del resto.
Espacios de nombres¶
Zen de Python
Namespaces are one honking great idea — let’s do more of those!
Los espacios de nombres permiten definir ámbitos o contextos en los que agrupar nombres de objetos.
Los espacios de nombres proporcionan un mecanismo de empaquetado, de tal forma que podamos tener incluso nombres iguales que no hacen referencia al mismo objeto (siempre y cuando estén en ámbitos distintos).
Cada función define su propio espacio de nombres y es diferente del espacio de nombres global aplicable a todo nuestro programa:
Veamos un ejemplo de aplicación de los espacios de nombres:
>>> language = 'castellano'#(1)!
>>> def catalonia():
... print(f'{language=}')#(2)!
...
>>> language#(3)!
'castellano'
>>> catalonia()#(4)!
language='castellano'
- Creación de variable global.
- Acceso a variable global dentro de la función.
- Comprobación de acceso a variable global en el contexto (espacio de nombres) global.
- Comprobación de acceso a variable global en el contexto (espacio de nombres) local de la función.
>>> language = 'castellano'#(1)!
>>> def catalonia():
... language = 'catalan'#(2)!
... print(f'{language=}')
...
>>> language#(3)!
'castellano'
>>> catalonia()#(4)!
language='catalan'
>>> language#(5)!
'castellano'
- Creación de variable global.
-
- Creación de variable local (independientemente de que se llame igual que la global).
- Es posible modificar el valor de una variable global usando la sentencia
global
aunque no es especialmente recomendable.
- Comprobación de acceso a variable global en el contexto (espacio de nombres) global, antes de llamar a la función.
- Comprobación de acceso a variable
globallocal en el contexto (espacio de nombres) local de la función. - Comprobación de acceso a variable global en el contexto (espacio de nombres) global, después de llamar a la función.
Contenido de los espacios de nombres
Python permite acceder a todos los items existentes en los espacios de nombres de un programa:
Consejos para programar mejor¶
Chris Staudinger (cofundador de Level Up Coding) compartió en su día estos 7 consejos para mejorar nuestro código:
- Las funciones deberían hacer una única cosa.(1)
- Utiliza nombres descriptivos y con significado.(2)
- No uses variables globales.(3)
- Refactoriza regularmente.(4)
- No utilices «números mágicos» o valores «hard-codeados».(5)
- Escribe lo que necesites ahora, no lo que pienses que podrías necesitar en el futuro.(6)
- Usa comentarios para explicar el «por qué» y no el «qué».(7)
- Por ejemplo, un mal diseño sería tener una única función que calcule el total de una cesta de la compra, los impuestos y los gastos de envío. Sin embargo esto se debería hacer con tres funciones separadas. Así conseguimos que el código sea más fácil de matener, reutilizar y depurar
- Los nombres autoexplicativos de variables y funciones mejoran la legibilidad del código. Por ejemplo – deberíamos llamar «total_cost» a una variable que se usa para almacenar el total de un carrito de la compra en vez de «x» ya que claramente explica su propósito
- Las variables globales pueden introducir muchos problemas, incluyendo efectos colaterales inesperados y errores de programación difíciles de trazar. Supongamos que tenemos dos funciones que comparten una variable global. Si una función cambia su valor la otra función podría no funcionar como se espera
- El código inevitablemente cambia con el tiempo, lo que puede derivar en partes obsoletas, redundantes o desorganizadas. Trata de mantener la calidad del código revisando y refactorizando aquellas zonas que se editan
- No es lo mismo escribir «99 * 3» que «price * quantity». Esto último es más fácil de entender y usa variables con nombres descriptivos haciéndolo autoexplicativo. Trata de usar constantes o variables en vez de valores «hard-codeados»
- Los programas simples y centrados en el problema son más flexibles y menos complejos
- El código limpio es autoexplicativo y por lo tanto los comentarios no deberían usarse para explicar lo que hace el código. En cambio, los comentarios debería usarse para proporcionar contexto adicional, como por qué el código está diseñado de una cierta manera
Ejercicios¶
- pypas
num-in-interval
- pypas
extract-evens
- pypas
split-case
- pypas
perfect
- pypas
palindrome
- pypas
count-vowels-recursive
- pypas
pangram
- pypas
cycle-alphabet
- pypas
bubble-sort
- pypas
consecutive-seq
- pypas
magic-square
- pypas
nested-add
- pypas
fibonacci-recursive
- pypas
hyperfactorial
- pypas
fibonacci-generator
- pypas
palindrome-recursive
- pypas
deco-positive
- pypas
slice-recursive
-
Término para identificar el «algoritmo» o secuencia de instrucciones derivadas del procesamiento que corresponda. ↩
-
Uno de los escenarios de múltiples sentencias de retorno en una función son las cláusulas guarda: una pieza de código que normalmente está al comienzo de la función y que comprueba una serie de condiciones para continuar o cortar la ejecución. ↩
-
Fuente: Real Python ↩
-
NumPy es una biblioteca de Python que proporciona soporte para arrays multidimensionales y funciones matemáticas de alto rendimiento. ↩
-
Epydoc es una adaptación de Javadoc un conocido sistema de documentación para Java. ↩
-
Existen herramientas como
mypy
que sí se encargan de comprobar estas restricciones de tipos. Dispone de una guía rápida para anotación de tipos. ↩ -
Véase paradigmas de programación. ↩
-
La función
range()
es un tanto especial. Véase este artículo de Trey Hunner. ↩ -
Cuando hablamos de «explicitar» un generador nos referimos a obtener todos sus valores de forma directa como una lista (o sucedáneo). ↩