Ricardo Pérez López
IES Doñana, curso 2025/2026
Hasta ahora, hemos aprendido que:
Un programa está compuesto por instrucciones.
Las instrucciones de un programa son las expresiones y las sentencias.
Además, hemos visto que podemos crear instrucciones más complejas a partir de otras más simples. Es decir:
Podemos crear expresiones más complejas combinando entre sí expresiones más simples.
Podemos crear sentencias compuestas (estructuras de control, como bloques, condicionales, bucles, etc.) combinando entre sí otras sentencias.
La propiedad que tienen los lenguajes de programación de crear elementos más complejos combinando otros más simples se denomina composición.
La abstracción y la composición son dos conceptos relacionados:
Componer consiste en combinar elementos entre sí para formar otros más complejos.
Abstraer consiste en coger un elemento (normalmente complejo), darle un nombre y ocultar sus detalles internos (es decir, los elementos que lo componen) dentro de una caja negra.
Lo interesante es que la combinación y la abstracción son dos mecanismos recursivos:
Podemos crear elementos complejos a partir de otros elementos complejos.
Podemos crear abstracciones a partir de otras abstracciones.
Además, por supuesto, podemos crear abstracciones a partir de composiciones y composiciones a partir de abstracciones.
Por ahora, esos conceptos (composición y abstracción) sólo los hemos aplicado a las instrucciones del programa:
La composición de instrucciones da lugar a las expresiones compuestas y a las sentencias compuestas (también llamadas estructuras de control: secuencia, selección e iteración).
La abstracción de instrucciones da lugar a las abstracciones funcionales.
Pero también se pueden aplicar a los datos:
La composición de datos da lugar a los datos compuestos (también llamados datos estructurados) y, en consecuencia, a los tipos de datos compuestos (también llamados tipos de datos estructurados).
La abstracción de datos da lugar a los datos abstractos y, en consecuencia, a los tipos abstractos de datos.
En esta unidad hablaremos de la composición de datos y dejaremos la abstracción de datos para una unidad posterior.
Un dato estructurado (también llamado dato compuesto, colección o contenedor) es un dato formado, a su vez, por otros datos llamados componentes o elementos, los cuales representan su contenido.
Por contra, los datos no estructurados se denominan datos elementales, escalares o atómicos.
Un tipo de dato estructurado, también llamado tipo compuesto, es aquel cuyos valores son datos estructurados.
Normalmente, se puede acceder de manera individual a los elementos que componen un dato estructurado y, a veces, también se pueden modificar esos elementos de manera individual.
El término estructura de datos se suele usar como sinónimo de tipo de dato estructurado, aunque nosotros haremos una distinción:
Usaremos tipo de dato estructurado cuando usemos un dato sin conocer sus detalles internos de implementación.
Usaremos estructura de datos cuando nos interesen esos detalles.
Los datos estructurados se pueden clasificar atendiendo a su secuencialidad y a su mutabilidad.
Según su secuencialidad:
Secuenciales: Son aquellos en los que se puede acceder directamente y de forma eficiente a cada uno de sus elementos indicando la posición que ocupan dentro de la secuencia.
Por tanto, son colecciones ordenadas, ya que sus elementos están ordenados dentro de la secuencia según la posición en la que se encuentran situados dentro de la misma.
No secuenciales: Son aquellos en los que NO se puede acceder directamente y de forma eficiente a cada uno de sus elementos indicando la posición que ocupan dentro de la colección.
En general, las estructuras no secuenciales son colecciones desordenadas, en las que no se puede afirmar que sus elementos se encuentran en una posición determinada dentro de la colección.
Según su mutabilidad:
Inmutables: el dato estructurado no puede cambiar nunca su estado interno a lo largo de su vida.
Mutables: el dato estructurado puede cambiar su estado interno a lo largo de su vida sin cambiar su identidad.
El contenido de un dato estructurado forma parte del estado interno de ese dato estructurado, por lo que cambiar el contenido de un dato estructurado supone cambiar también su estado interno.
Por ejemplo, si en la lista [7, 8, 9]
sustituimos su segundo elemento (el 8
) por un 5
para obtener
la lista [7, 5, 9]
,
estamos cambiando el contenido de la lista y, por consiguiente, su
estado interno.
Su identidad no ha cambiado, pero su estado interno sí.
\text{Tipos} \begin{cases} \text{Escalares} \\ \\ \text{Estructurados} \begin{cases} \text{Secuencias} \begin{cases} \text{Inmutables} \begin{cases} \text{Cadenas (\texttt{str})} \\ \text{Tuplas (\texttt{tuple})} \\ \text{Rangos (\texttt{range})} \end{cases} \\ \\ \text{Mutables} \begin{cases} \text{Listas (\texttt{list})} \end{cases} \end{cases} \\ \\ \text{No secuencias} \begin{cases} \text{Inmutables} \begin{cases} \text{Conjuntos (\texttt{frozenset})} \end{cases} \\ \\ \text{Mutables} \begin{cases} \text{Conjuntos (\texttt{set})} \\ \text{Diccionarios (\texttt{dict})} \end {cases} \end{cases} \end{cases} \end{cases}
Un dato es hashable si cumple las siguientes dos condiciones:
Puede compararse con otros datos usando el operador
==
.
Tiene asociado un número entero llamado hash que nunca cambia durante toda la vida del dato.
Para obtener el hash de un dato, se usa la función hash
:
Si un dato d es
hashable, hash(
d)
devolverá el hash de
d.
En caso contrario, lanzará una excepción de tipo TypeError
.
Si dos datos hashables son iguales, entonces deben tener el mismo hash:
Si x ==
y, entonces debe cumplirse que hash
(
x)
==
hash
(
y)
.
En cambio, si dos datos son distintos, sus hash no tienen por qué serlo.
Ejemplos:
El concepto de hashable es importante en Python ya que existen tipos de datos estructurados que sólo pueden contener elementos hashables.
Por ejemplo, los elementos de un conjunto y las claves de un diccionario deben ser hashables.
La mayoría de los datos inmutables predefinidos en Python son hashables.
Las colecciones inmutables (como las tuplas o
los frozenset
s)
sólo son hashables si sus elementos también lo son.
Las coleccion mutables (como las listas o los diccionarios) NO son hashables.
El hash de un dato depende del estado interno del dato, ya que se calcula a partir de dicho estado interno usando un algoritmo que no nos debe preocupar por ahora.
Como el estado interno de una colección viene determinado principalmente por los elementos que contiene, el hash de una colección dependerá también del contenido de la colección.
Y por esta razón, las colecciones mutables no son hashables: si una colección es mutable, su contenido puede cambiar y, por tanto, su hash también cambiaría, pero esto está prohibido.
El hash de un dato se calcula en función del estado interno del dato y, en caso de ser una colección, también en función de su contenido.
El hash de un dato es un número que representa al dato y a todo su contenido.
En cierto modo, ese número resume el estado del dato en un simple número entero.
El hash de un dato se utiliza internamente para acceder al dato dentro de una colección de forma directa y eficiente.
Para ello, el intérprete utiliza ciertas técnicas que permiten localizar directamente a un dato dentro de una colección, de forma casi inmediata y sin importar el tamaño de la colección (pero recordemos que para ello es necesario que el hash del dato nunca cambie).
De no usar estas técnicas, el intérprete tendría que buscar el dato secuencialmente dentro de la colección, recorriéndola desde el principio hasta el final, lo que sería mucho más lento y consumiría un tiempo que sería mayor cuanto más grande fuese la colección.
Los hash permiten el acceso directo a un dato dentro de una colección.
Muy en resumen, las técnicas se basan en dividir el espacio de memoria que ocupa la colección en una serie de contenedores llamados buckets.
Cada bucket va numerado por un posible valor de hash, de forma que el bucket número n contendrá todos los elementos cuyo hash valga n.
Por tanto, el algoritmo que usa el intérprete para encontrar un elemento hashable dentro de una colección es:
Calcular el hash del elemento a localizar.
Irse directamente al bucket numerado con ese valor de hash (esta es una operación inmediata, con coste O(1)).
Localizar dentro del bucket el elemento que se está
buscando usando el ==
, lo cual consumirá un tiempo que, en
general, no será mucho, ya que los elementos están repartidos entre
todos los buckets y, por tanto, normalmente no habrá muchos
elementos en cada bucket.
Al final, se consigue encontrar al elemento (si está) de forma muy rápida, con un coste que es casi constante, independientemente de la cantidad de elementos que haya en la colección.
No se debe confundir el id
de un dato
con el hash
de un
dato:
Función id |
Función hash |
---|---|
|
|
|
|
|
|
|
|
|
|
Se dice que un dato compuesto es iterable cuando se puede acceder a todos sus elementos de uno en uno, operación que se denomina recorrer el iterable.
Gracias a esto, se dice que un iterable nos permite visitar sus elementos o, también, iterar sobre sus elementos.
Como iterables tenemos:
Todas las secuencias: listas, cadenas, tuplas y rangos.
Estructuras no secuenciales: diccionarios y conjuntos.
Los iterables no representan un tipo concreto, sino más bien una familia de tipos que comparten la misma propiedad.
Muchas funciones, como map
y filter
, actúan
sobre iterables en general, en lugar de hacerlo sobre un tipo concreto
(lista, tupla, …).
Por ejemplo, las listas son iterables ya que nos permite acceder a todos sus elementos de uno en uno y, por tanto, podemos recorrerla.
Para visitar sus elementos podemos usar la indexación, y para recorrer toda la lista podemos usar un bucle:
La forma básica de recorrer un dato iterable es usando un iterador.
De hecho, técnicamente, un iterable se define como aquel dato al que le podemos asociar, al menos, un iterador.
Un iterador es un objeto que sabe cómo recorrer un iterable.
Para ello, el iterador crea un flujo de datos perezoso que va entregando los elementos del iterable de uno en uno.
Los sucesivos elementos del flujo de datos se van obteniendo al
llamar repetidamente a la función next
aplicada
al iterador.
Cuando ya no hay más elementos disponibles, la función next
lanza una
excepción de tipo StopIteration
,
lo que indica que el iterador se ha agotado (se han
consumido todos sus elementos), por lo que si se sigue llamando a la
función next
se seguirá
lanzando esa excepción.
Se puede obtener un iterador a partir de cualquier dato iterable
aplicando la función iter
al
iterable.
(Recordemos que todo iterable debe tener asociado un iterador.)
Ejemplo de uso de iter
y next
:
Si se le pasa un dato no iterable, iter
lanza una
excepción TypeError
:
Los iteradores son iterables perezosos de un solo uso:
Son perezosos porque van generando sus elementos a medida que los va entregando, en lugar de generarlos todos a la vez primero.
Son de un solo uso porque cada elemento sólo se entrega una vez.
Además, los iteradores son iterables que actúan como sus propios iteradores:
Por tanto, cuando llamamos a iter
pasándole
un iterador, se devuelve el mismo iterador:
En consecuencia, podemos usar un iterador en cualquier sitio donde se espere un iterable.
Funciones como map
y filter
devuelven iteradores porque, al ser perezosos, son más eficientes en
memoria que si devolvieran toda una lista o tupla.
Por ejemplo: ¿qué ocurre si sólo necesitamos los primeros elementos
del resultado de un map
?
Los iteradores se pueden convertir en listas o tuplas usando las
funciones list
y tuple
:
for
Probablemente, la mejor forma de recorrer los elementos que
devuelve un iterador es mediante una estructura de
control llamada bucle for
.
Su sintaxis es:
for
⟨variable⟩(,
⟨variable⟩)*
in
⟨iterable⟩:
que no es más que azúcar sintáctico para el siguiente código equivalente:
iterador = iter(
⟨iterable⟩)
while True:
try:
,
⟨variable⟩)* = next(iterador)
except StopIteration:
break
else:
Si estamos recorriendo una secuencia y necesitamos recuperar
tanto el valor como el índice de cada elemento, podemos
usar la función enumerate
.
Esta función devuelve un iterador que va generando tuplas que contienen, además del elemento, el valor correspondiente de un contador numérico.
Las tuplas que devuelve el iterador llevan el contador en la primera posición y el elemento de la secuencia en la segunda posición.
Por defecto, el contador empieza desde 0
y se va
incrementando de uno en uno, por lo que coincide con el índice del
elemento en la secuencia:
itertools
El módulo itertools
contiene una variedad de iteradores de uso frecuente, así como funciones
que combinan varios iteradores.
Algunos de esos iteradores son muy especiales porque pueden devolver flujos infinitos o valores que se repiten continuamente, lo cual contradice en cierta manera lo que dijimos cuando definimos los iteradores como «iterables de un solo uso».
itertools.count
(
[⟨inicio⟩[,
⟨paso⟩]])
devuelve un
flujo infinito de valores separados uniformemente. Se puede indicar
opcionalmente un valor de comienzo (que por defecto es 0
) y el
intervalo entre números (que por defecto es 1
):
itertools.count()
\Rightarrow 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
…
itertools.count(10)
\Rightarrow 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, …
itertools.count(10, 5)
\Rightarrow 10, 15, 20, 25, 30, 35, 40,
45, …
itertools.cycle
(
⟨iterador⟩)
devuelve un
nuevo iterador que va generando sus elementos del primero al último,
repitiéndolos indefinidamente:
itertools.cycle([1, 2, 3, 4])
\Rightarrow 1, 2, 3, 4, 1, 2, 3, 4,
…
itertools.repeat
(
⟨elem⟩[,
⟨n⟩])
devuelve ⟨n⟩ veces el elemento ⟨elem⟩, o lo devuelve indefinidamente
si no se indica ⟨n⟩:
itertools.repeat('abc')
\Rightarrow abc, abc, abc, abc, abc,
abc, abc, …
itertools.repeat('abc', 5)
\Rightarrow abc, abc, abc, abc,
abc
zip
La función zip
en Python
devuelve un iterador de tuplas, donde cada tupla contiene un elemento de
cada uno de los iterables que se le pasen como argumentos, agrupados por
su posición.
En otras palabras:
Toma los elementos en posición 0 de cada iterable y forma la primera tupla.
Luego toma los elementos en posición 1 de cada iterable y forma la segunda tupla.
Y así sucesivamente, hasta que el iterable más corto se quede sin elementos.
Su sintaxis es:
zip
(
⟨iterable_1⟩ [, … ⟨iterable_n⟩])
El siguiente código:
produce la siguiente salida:
Si los iterables tienen distinta longitud, la función
zip
se detiene en el más corto. Por ejemplo, el siguiente
código:
produce la siguiente salida:
Sabemos que, en programación funcional, las funciones también son valores.
Por tanto, como pasa con cualquier otro valor, las funciones también tienen un tipo, se pueden ligar a identificadores, etcétera.
Pero si las funciones son valores, eso significa que también se pueden pasar como argumentos a otras funciones o se pueden devolver como resultado de otras funciones.
Una función de orden superior es una función que recibe funciones como argumentos o devuelve funciones como resultado.
Por ejemplo, la siguiente función recibe otra función como argumento y devuelve el resultado de aplicar dicha función al número 5:
No hace falta crear las funciones cuadrado
y cubo
para pasárselas a la función aplica5
como argumento. Se pueden pasar
directamente las expresiones lambda, que también son funciones:
Naturalmente, la función que se pasa a aplica5
debe recibir un único argumento
de tipo numérico.
Lo mismo se puede hacer con funciones imperativas, o combinando funciones imperativas con expresiones lambda:
También se puede devolver una función como resultado.
Por ejemplo, la siguiente función suma_o_resta
recibe una cadena y
devuelve una función que suma si la cadena es 'suma'
;
en caso contrario, devuelve una función que resta:
>>> suma_o_resta = lambda s: (lambda x, y: x + y) if s == 'suma' else \
(lambda x, y: x - y)
>>> suma_o_resta('suma')
<function <lambda>.<locals>.<lambda> at 0x7f526ab4a790>
>>> suma = suma_o_resta('suma')
>>> suma(2, 3)
5
>>> resta = suma_o_resta('resta')
>>> resta(4, 3)
1
>>> suma_o_resta('suma')(6, 4)
10
Tanto aplica5
como suma_o_resta
son funciones de
orden superior.
Y con funciones imperativas:
>>> def suma_o_resta(s):
... def sumar(x, y):
... return x + y
... def restar(x, y):
... return x - y
... if s == 'suma':
... return sumar
... else:
... return restar
>>> suma_o_resta('suma')
<function <lambda>.<locals>.<lambda> at 0x7f526ab4a790>
>>> suma = suma_o_resta('suma')
>>> suma(2, 3)
5
>>> resta = suma_o_resta('resta')
>>> resta(4, 3)
1
>>> suma_o_resta('suma')(6, 4)
10
Una función es una abstracción porque agrupa lo que tienen en común determinados casos particulares que siguen el mismo patrón.
El mismo concepto se puede aplicar a casos particulares de funciones, y al hacerlo damos un paso más en nuestro camino hacia un mayor grado de abstracción.
Es decir: muchas veces observamos el mismo patrón en funciones diferentes.
Para poder abstraer, de nuevo, lo que tienen en común dichas funciones, deberíamos ser capaces de manejar funciones que acepten a otras funciones como argumentos o que devuelvan otra función como resultado (es decir, funciones de orden superior).
Supongamos las dos funciones siguientes:
Estas dos funciones comparten claramente un patrón común. Se diferencian solamente en:
El nombre de la función.
La función que se aplica a a
para calcular cada término
de la suma.
Podríamos haber escrito las funciones anteriores rellenando los «casilleros» del siguiente patrón general:
lambda
a, b: 0
if
a > b
else
⟨término⟩(a) + ⟨nombre⟩(a + 1, b)La existencia de este patrón común nos demuestra que hay una abstracción esperando que la saquemos a la superficie.
De hecho, los matemáticos han identificado hace mucho tiempo esta abstracción llamándola sumatorio de una serie, y la expresan así: \sum _ {n=a}^b f(n)
La ventaja que tiene usar la notación anterior es que podemos trabajar directamente con el concepto de sumatorio en vez de trabajar con sumas concretas, y podemos sacar conclusiones generales sobre los sumatorios independientemente de la serie particular con la que estemos trabajando.
Igualmente, como programadores estamos interesados en que nuestro lenguaje tenga la suficiente potencia como para describir directamente el concepto de sumatorio, en vez de funciones particulares que calculen sumas concretas.
En programación funcional lo conseguimos creando funciones que conviertan los «casilleros» en parámetros que recibirían funciones:
De esta forma, las dos funciones suma_enteros
y suma_cubos
anteriores se podrían
definir en términos de esta suma
:
suma
es una abstracción que captura el patrón común
que comparten suma_enteros
y suma_cubos
, las
cuales también son abstracciones que capturan sus respectivos patrones
comunes.
suma
?map
Una forma de hacerlo sería:
¿Y elevar a la cuarta potencia?
Es evidente que hay un patrón subyacente que se podría abstraer
creando una función de orden superior que aplique una función f
a los elementos de una tupla y
devuelva la tupla resultante.
Esa función se llama map
, y viene
definida en Python con la siguiente signatura:
\texttt{map(\(func\),\;\(iterable\))\;->\;Iterator}
donde:
func debe ser una función de un solo argumento.
iterable puede ser cualquier iterable.
Podemos usarla así:
Lo que devuelve es un iterador que luego podemos recorrer o, por
ejemplo, convertir en una tupla usando la función tuple
:
Además de una tupla, también podemos usar cualquier otro iterable
como argumento para map
, como por
ejemplo un rango:
¿Cómo definirías la función map
de forma
que devolviera una tupla?
Podríamos definirla así:
filter
filter
es una
función de orden superior que devuelve aquellos
elementos de un iterable que cumplen una determinada
condición.
Su signatura es:
\texttt{filter(\(function\),\;\(iterable\))\;->\;Iterator}
donde function debe ser una función de un solo argumento que devuelva un booleano.
Como map
, también
devuelve un iterador, que se puede recorrer o convertir a tupla
con la función tuple
, por
ejemplo.
Por ejemplo:
reduce
reduce
es una
función de orden superior que aplica, de forma
acumulativa, una función a todos los elementos de un
iterable.
Captura un patrón muy frecuente de recursión sobre secuencias.
Por ejemplo, para calcular la suma de todos los elementos de una tupla, haríamos:
Y para calcular el producto:
Como podemos observar, la estrategia de cálculo es esencialmente
la misma; sólo se diferencian en la operación a realizar
(+
o *
) y en el valor inicial o
elemento neutro (0
o 1
).
Si abstraemos ese patrón común, podemos crear una función de orden superior que capture la idea de reducir todos los elementos de un iterable a un único valor.
Eso es lo que hace la función reduce
.
Su signatura es:
\texttt{reduce(\(function\),\;\(sequence\)\;\([\),\;\(initial\)\(]\))\;->\;Any}
donde:
function debe ser una función que reciba dos argumentos.
sequence debe ser cualquier objeto iterable (normalmente, una secuencia como una cadena, una tupla o un rango).
initial, si se indica, se usará como primer elemento sobre el que realizar el cálculo y servirá como valor por defecto cuando la secuencia esté vacía (si no se indica y la secuencia está vacía, generará un error).
Para usarla, primero tenemos que importarla del
módulo functools
:
No es la primera vez que importamos un módulo. Ya lo hicimos con
el módulo math
.
En su momento estudiaremos con detalle qué son los módulos. Por ahora nos basta con lo que ya sabemos: que contienen definiciones que podemos incorporar a nuestros scripts.
Por ejemplo, para calcular la suma y el producto de (1, 2, 3, 4)
,
podemos definir las funciones suma_de_numeros
y
producto_de_numeros
a partir de reduce
:
También podemos importar y usar las funciones add
y
mul
del módulo operator
, las cuales actúan,
respectivamente, como el operador +
y *
:
from functools import reduce
from operator import add, mul
tupla = (1, 2, 3, 4)
suma_de_numeros = lambda tupla: reduce(add, tupla, 0)
producto_de_numeros = lambda tupla: reduce(mul, tupla, 1)
De esta forma, usamos add
y mul
en lugar de
las expresiones lambda (lambda x, y: x + y)
y (lambda x, y: x * y)
,
respectivamente.
En general, si iterable representa un objeto iterable que contiene los elementos e_1, e_2, \ldots, e_n (en este orden), entonces tenemos que: \texttt{reduce(\(f\),\;\(iterable\),\;\(ini\))} = f(\ldots{}f(f(f(ini, e_1), e_2), e_3), \ldots, e_n)
Por ejemplo, la siguiente llamada a reduce
:
realiza y devuelve el resultado del siguiente cálculo:
lo que, en la práctica, equivale a:
Si iterable representa un iterable vacío, entonces:
\texttt{reduce(\(f\),\;\(iterable\),\;\(ini\))} = ini
Por ejemplo:
devuelve directamente 0
.
Si no se indica un valor inicial, tenemos que: \texttt{reduce(\(f\),\;\((e_1, e_2, \ldots, e_n)\))} = f(\ldots{}f(f(e_1, e_2), e_3), \ldots, e_n)
Es decir: se usará el primer elemento del iterable como valor inicial.
Por ejemplo, la siguiente llamada a reduce
:
realiza y devuelve el resultado del siguiente cálculo:
lo que, en la práctica, equivale a:
Pero si el iterable es vacío, dará un error:
Con lo que acabamos de ver, se demuestra que la implementación de
la función reduce
en Python va reduciendo de
izquierda a derecha y que, por tanto, las operaciones se hacen
agrupándose por la izquierda.
Esto es algo que debemos tener muy en cuenta a la hora de diseñar
la función que se le pasa a reduce
.
Se denomina iteración a cada paso que da la
función reduce
, es decir, cada vez que reduce
visita un nuevo elemento del iterable (la tupla, cadena o lo que sea) y
aplica la función para calcular el resultado parcial.
Esa función, como ya dijimos antes, debe tener dos parámetros, pero de forma que, en cada iteración:
Su primer parámetro va a contener siempre el valor parcial acumulado hasta ahora (por tanto, es un acumulador).
Su segundo parámetro va a contener el valor del elemento que en
este momento está visitando reduce
.
Por tanto, es frecuente que el primer parámetro de esa función se
llame acc
o algo similar, para expresar el hecho de que ahí
se va recibiendo el valor acumulado hasta el momento.
Por ejemplo, en la siguiente llamada:
acc
va a contener la suma parcial acumulada hasta
ahora.
e
va a contener el elemento que en este momento se
está visitando.
Así, durante la ejecución del reduce
, ésta provocará las
siguientes llamadas a la expresión lambda:
reduce
si
recibiera una tupla y no cualquier iterable?Una forma (con valor inicial obligatorio) podría ser así:
Dos operaciones que se realizan con frecuencia sobre un iterable son:
Realizar alguna operación sobre cada elemento (map
).
Seleccionar un subconjunto de elementos que cumplan alguna
condición (filter
).
Las expresiones generadoras son una notación copiada del lenguaje Haskell que nos permite realizar ambas operaciones de una forma muy concisa.
El resultado que devuelve es un iterador.
Su sintaxis es:
(
⟨expresión⟩
(for
⟨identificador⟩
in
⟨iterador⟩
[if
⟨condición⟩])^+)
Los elementos de la salida generada serán los sucesivos valores de ⟨expresión⟩.
Las cláusulas if
son opcionales. Si
están, la ⟨expresión⟩ sólo se
evaluará y añadirá al resultado cuando se cumpla la ⟨condición⟩.
Los paréntesis (
y )
alrededor de la
expresión generadora se pueden quitar si la expresión se usa como único
argumento de una función.
Por ejemplo:
Las expresiones generadoras, al igual que las expresiones lambda, determinan su propio ámbito.
Ese ámbito abarca toda la expresión generadora, de principio a fin.
Los identificadores que aparecen en la cláusula for
se se van
ligando automáticamente, uno a uno, a cada elemento del iterable
indicada en la cláusula in
.
Al recorrer el iterable, las variables van almacenando en cada iteración del bucle el valor del elemento que en ese momento se está visitando.
Debido a ello, podemos afirmar que las variables que aparecen en
en cada cláusula for
de la expresión generadora son
identificadores cuantificados, ya que toman sus valores
automáticamente y éstos están restringido a los valores que devuelva el
iterable.
Además, estos identificadores cuantificados son locales a la expresión generadora, y sólo existen dentro de ella.
Debido a lo anterior, esos identificadores cumplen estas dos propiedades:
Se pueden renombrar (siempre de forma consistente) sin que la expresión cambie su significado.
Por ejemplo, las dos expresiones generadoras siguientes son equivalentes, puesto que producen el mismo resultado:
No se pueden usar fuera de la expresión generadora, ya que estarían fuera de su ámbito y no serían visibles.
Por ejemplo, lo siguiente daría un error de nombre: