6. Generadores

La iteración es uno de los más comunes patrones de programación en Python. Los programas hacen mucha iteración para procesar listas, leer archivos, consultar una base de datos, y más. Una de las características mas poderosas de Python es la habilidad de costumizar y redefinir la iteración en una función generadora. Al final de la sección, escribiremos algunos programas que procesan datos en tiempo real en una manera interesante.

6.1 Protocolo de iteración

Esta sección analiza el proceso subyacente de iteración.

6.1.1 Iteración en todas partes

Muchos objetos diferentes admiten la iteración.

a = 'hello'
for c in a: # cicla sobre los caracteres en a
    ...

b = { 'name': 'Dave', 'password':'foo'}
for k in b: # cicla sobre las claves del diccionario
    ...

c = [1,2,3,4]
for i in c: # cicla sobre los elementos en una lista/tupla
    ...

f = open('foo.txt')
for x in f: # cicla sobre las lineas en un archivo
    ...

6.1.2 Iteración: Protocolo

Considere la forafirmación.

for x in obj:
    # declaraciones

¿Qué pasa debajo del capó?

_iter = obj.__iter__()        # consigue el objeto iterador
while True:
    try:
        x = _iter.__next__()  # consigue el próximo elemento
        # statements ...
    except StopIteration:     # no hay más elementos
        break

Todos los objetos que trabajan con el for-loopimplementan este protocolo de iteración de bajo nivel.

Ejemplo: iteración manual sobre una lista.

>>> x = [1,2,3]
>>> iterador = x.__iter__()
>>> iterador
<listiterator object at 0x590b0>
>>> iterador.__next__()
1
>>> iterador.__next__()
2
>>> iterador.__next__()
3
>>> iterador.__next__()
Traceback (most recent call last):
File "<stdin>", line 1, in ? StopIteration
>>>

6.1.3 Apoyo a la iteración

Knowing about iteration is useful if you want to add it to your own objects. For example, making a custom container.

class Portfolio:
    def __init__(self):
        self.holdings = []

    def __iter__(self):
        return self.holdings.__iter__()
    ...

port = Portfolio()
for s in port:
    ...

6.1.4 Ejercicios

Ejercicio 6.1: Iteración ilustrada

Crea la siguiente lista:

a = [1,9,4,25,16]

Repetir manualmente esta lista. Llame __iter__() para obtener un iterador y llame al método __next__() para obtener elementos sucesivos.

>>> i = a.__iter__()
>>> i
<listiterator object at 0x64c10>
>>> i.__next__()
1
>>> i.__next__()
9
>>> i.__next__()
4
>>> i.__next__()
25
>>> i.__next__()
16
>>> i.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>

La función next() incorporada es un atajo para llamar al método __next__() de un iterador. Intente usarlo en un archivo:

>>> f = open('Data/portfolio.csv')
>>> f.__iter__()  # Nota: Esto retorna el archivo mismo <_io.TextIOWrapper name='Data/portfolio.csv' mode='r' encoding='UTF-8'>
>>> next(f)
'name,shares,price\n'
>>> next(f)
'"AA",100,32.20\n'
>>> next(f)
'"IBM",50,91.10\n'
>>>

Sigue llamando next(f) hasta que llegues al final del archivo. Mira lo que pasa.

Ejercicio 6.2: Apoyo a la iteración

En ocasiones, es posible que desee hacer que uno de sus propios objetos admita la iteración, especialmente si su objeto se ajusta a una lista existente u otra iterable. En un archivo nuevo portfolio.py, defina la siguiente clase:

# portfolio.py
class Portfolio:

    def __init__(self, holdings):
        self._holdings = holdings

    @property
    def total_cost(self):
        return sum([s.cost for s in self._holdings])

    def tabulate_shares(self):
        from collections import Counter
        total_shares = Counter()
        for s in self._holdings:
            total_shares[s.name] += s.shares
        return total_shares

Esta clase está destinada a ser una capa alrededor de una lista, pero con algunos métodos adicionales como la total_costpropiedad. Modifique la función read_portfolio() en report.py para que cree una instancia de Portfolio como esta:

# report.py
...

import fileparse
from stock import Stock
from portfolio import Portfolio

def read_portfolio(filename):
    '''
    Read a stock portfolio file into a list of dictionaries with keys
    name, shares, and price.
    '''
    with open(filename) as file:
        portdicts = fileparse.parse_csv(file,
                                        select=['name','shares','price'],
                                        types=[str,int,float])

    portfolio = [ Stock(d['name'], d['shares'], d['price']) for d in portdicts ]
    return Portfolio(portfolio)
...

Intente ejecutar el report.pyprograma. Descubrirá que falla espectacularmente debido al hecho de que las Portfolioinstancias no son iterables.

>>> import report
>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv')
... falla ...

Solucione esto modificando la Portfolioclase para admitir la iteración:

class Portfolio:

    def __init__(self, holdings):
        self._holdings = holdings

    def __iter__(self):
        return self._holdings.__iter__()

    @property
    def total_cost(self):
        return sum([s.shares*s.price for s in self._holdings])

    def tabulate_shares(self):
        from collections import Counter
        total_shares = Counter()
        for s in self._holdings:
            total_shares[s.name] += s.shares
        return total_shares

Una vez que haya realizado este cambio, su report.pyprograma debería funcionar nuevamente. Mientras lo hace, arregle su programa pcost.py para usar el nuevo objeto Portfolio. Me gusta esto:

# pcost.py
import report

def portfolio_cost(filename):
    ''' Computes the total cost (shares*price) of a portfolio file '''
    portfolio = report.read_portfolio(filename)
    return portfolio.total_cost

Pruébelo para asegurarse de que funcione:

>>> import pcost
>>> pcost.portfolio_cost('Data/portfolio.csv')
44671.15
>>>

Ejercicio 6.3: Hacer un recipiente más adecuado

Si crea una clase de contenedor, a menudo desea hacer más que solo iterar. Modifique la Portfolioclase para que tenga otros métodos especiales como este:

class Portfolio:
    def __init__(self, holdings):
        self._holdings = holdings

    def __iter__(self):
        return self._holdings.__iter__()

    def __len__(self):
        return len(self._holdings)

    def __getitem__(self, index):
        return self._holdings[index]

    def __contains__(self, name):
        return any([s.name == name for s in self._holdings])

    @property
    def total_cost(self):
        return sum([s.shares*s.price for s in self._holdings])

    def tabulate_shares(self):
        from collections import Counter
        total_shares = Counter()
        for s in self._holdings:
            total_shares[s.name] += s.shares
        return total_shares

Ahora, pruebe algunos experimentos con esta nueva clase:

>>> import report
>>> portfolio = report.read_portfolio('Data/portfolio.csv')
>>> len(portfolio)
7
>>> portfolio[0]
Stock('AA', 100, 32.2)
>>> portfolio[1]
Stock('IBM', 50, 91.1)
>>> portfolio[0:3]
[Stock('AA', 100, 32.2), Stock('IBM', 50, 91.1), Stock('CAT', 150, 83.44)]
>>> 'IBM' in portfolio
True
>>> 'AAPL' in portfolio
False
>>>

Una observación importante acerca de esto: generalmente, el código se considera “Pythonic” si habla el vocabulario común de cómo funcionan normalmente otras partes de Python. Para los objetos de contenedor, el soporte de iteración, indexación, contención y otros tipos de operadores es una parte importante de esto.

6.2 Personalización de la iteración con generadores

Esta sección analiza cómo se puede personalizar la iteración utilizando una función de generador.

6.2.1 Un problema

Suponga que desea crear su propio patrón de iteración personalizado.

Por ejemplo, una cuenta atrás.

>>> for x in countdown(10):
...   print(x, end=' ')
...
10 9 8 7 6 5 4 3 2 1
>>>

Hay una forma sencilla de hacer esto.

6.2.2 Generadores

Un generador es una función que define la iteración.

def countdown(n):
    while n > 0:
        yield n
        n -= 1

Por ejemplo:

>>> for x in countdown(10):
...   print(x, end=' ')
...
10 9 8 7 6 5 4 3 2 1
>>>

Un generador es cualquier función que usa la declaración yield.

El comportamiento de los generadores es diferente al de una función normal. Llamar a una función generadora crea un objeto generador. No ejecuta la función de inmediato.

def countdown(n):
    # agregando sdeclaracion de impresion
    print('Cuenta hacia abajo desde', n)
    while n > 0:
        yield n
        n -= 1
>>> x = countdown(10)
# There is NO PRINT STATEMENT
>>> x
# x is a generator object
<generator object at 0x58490>
>>>

La función solo se ejecuta en llamada a __next__().

>>> x = countdown(10)
>>> x
<generator object at 0x58490>
>>> x.__next__()
Counting down from 10
10
>>>

yield produce un valor, pero suspende la ejecución de la función. La función se reanuda en la próxima llamada a __next__().

>>> x.__next__()
9
>>> x.__next__()
8

Cuando el generador finalmente regresa, la iteración genera un error.

>>> x.__next__()
1
>>> x.__next__()
Traceback (most recent call last):
File "<stdin>", line 1, in ? StopIteration
>>>

Observación: Una función generadora implementa el mismo protocolo de bajo nivel que las declaraciones for usan en listas, tuplas, dictados, archivos, etc.

6.2.3 Ejercicios

Ejercicio 6.4: Un generador simple

Si alguna vez desea personalizar la iteración, siempre debe pensar en las funciones del generador. Son fáciles de escribir: cree una función que lleve a cabo la lógica de iteración deseada y utilíce yield para emitir valores.

Por ejemplo, pruebe este generador que busca en un archivo líneas que contengan una subcadena coincidente:

>>> def filematch(filename, substr):
        with open(filename, 'r') as f:
            for line in f:
                if substr in line:
                    yield line

>>> for line in open('Data/portfolio.csv'):
        print(line, end='')

name,shares,price
"AA",100,32.20
"IBM",50,91.10
"CAT",150,83.44
"MSFT",200,51.23
"GE",95,40.37
"MSFT",50,65.10
"IBM",100,70.44
>>> for line in filematch('Data/portfolio.csv', 'IBM'):
        print(line, end='')

"IBM",50,91.10
"IBM",100,70.44
>>>

Esto es algo interesante: la idea de que puede ocultar un montón de procesamiento personalizado en una función y usarlo para alimentar un ciclo for. El siguiente ejemplo analiza un caso más inusual.

Ejercicio 6.5: Supervisión de una fuente de datos de transmisión

Los generadores pueden ser una forma interesante de monitorear fuentes de datos en tiempo real, como archivos de registro o feeds del mercado de valores. En esta parte, exploraremos esta idea. Para comenzar, siga cuidadosamente las siguientes instrucciones.

El programa Data/stocksim.pyes un programa que simula datos del mercado de valores. Como salida, el programa escribe constantemente datos en tiempo real en un archivo Data/stocklog.csv. En una ventana de comando separada, vaya al Data/directorio y ejecute este programa:

bash % python3 stocksim.py

Si está en Windows, simplemente ubique el stocksim.pyprograma y haga doble clic en él para ejecutarlo. Ahora, olvídese de este programa (déjelo correr). Usando otra ventana, mire el archivo que Data/stocklog.csvestá escribiendo el simulador. Debería ver que se agregan nuevas líneas de texto al archivo cada pocos segundos. Nuevamente, deje que este programa se ejecute en segundo plano; se ejecutará durante varias horas (no debería tener que preocuparse por ello).

Una vez que el programa anterior se esté ejecutando, escribamos un pequeño programa para abrir el archivo, buscar el final y esperar una nueva salida. Cree un archivo follow.pyy coloque este código en él:

# follow.py
import os
import time

f = open('Data/stocklog.csv')
f.seek(0, os.SEEK_END)   # Move file pointer 0 bytes from end of file
while True:
    line = f.readline()
    if line == '':
        time.sleep(0.1)   # Sleep briefly and retry
        continue
    fields = line.split(',')
    name = fields[0].strip('"')
    price = float(fields[1])
    change = float(fields[4])
    if change < 0:
        print(f'{name:>10s} {price:>10.2f} {change:>10.2f}')

Si ejecuta el programa, verá un indicador de cotización en tiempo real. Bajo el capó, este código es como el comando tail -f de Unix que se usa para ver un archivo de registro.

Nota: El uso del método readline() en este ejemplo es algo inusual en el sentido de que no es la forma habitual de leer líneas de un archivo (normalmente solo usaría un for-loop). Sin embargo, en este caso, lo estamos usando para sondear repetidamente el final del archivo para ver si se han agregado más datos (readline() devolverá nuevos datos o una cadena vacía).

Ejercicio 6.6: uso de un generador para producir datos

Si observa el código del ejercicio 6.5, la primera parte del código produce líneas de datos, mientras que las declaraciones al final del whileciclo consumen los datos. Una característica importante de las funciones del generador es que puede mover todo el código de producción de datos a una función reutilizable.

Modifique el código del ejercicio 6.5 para que la lectura del archivo sea realizada por una función generadora follow(filename). Haga que funcione el siguiente código:

>>> for line in follow('Data/stocklog.csv'):
          print(line, end='')

... Debería ver lineas de salida producidas aquí ...

Modifique el código de cotización bursátil para que se vea así:

if __name__ == '__main__':
    for line in follow('Data/stocklog.csv'):
        fields = line.split(',')
        name = fields[0].strip('"')
        price = float(fields[1])
        change = float(fields[4])
        if change < 0:
            print(f'{name:>10s} {price:>10.2f} {change:>10.2f}')

Ejercicio 6.7: Observando tu portafolio

Modifique el programa follow.py para que observe el flujo de datos de acciones e imprima un ticker que muestre información solo para las acciones de una cartera. Por ejemplo:



if __name__ == '__main__':
    import report

    portfolio = report.read_portfolio('Data/portfolio.csv')

    for line in follow('Data/stocklog.csv'):
        fields = line.split(',')
        name = fields[0].strip('"')
        price = float(fields[1])
        change = float(fields[4])
        if name in portfolio:
            print(f'{name:>10s} {price:>10.2f} {change:>10.2f}')

Nota: Para que esto funcione, su Portfolioclase debe ser compatible con el inoperador. Vea el ejercicio 6.3 y asegúrese de implementar el operador __contains__().

Discusión

Algo muy poderoso acaba de ocurrir aquí. Movió un patrón de iteración interesante (líneas de lectura al final de un archivo) a su propia pequeña función. La función follow() es ahora esta utilidad de propósito completamente general que puede usar en cualquier programa. Por ejemplo, puede usarlo para ver registros del servidor, registros de depuración y otras fuentes de datos similares. Eso es algo genial.

6.3 Productores, consumidores y oleoductos

Los generadores son una herramienta útil para establecer varios tipos de problemas de productores / consumidores y tuberías de flujo de datos. Esta sección trata sobre eso.

6.3.1 Problemas entre productores y consumidores

Los generadores están estrechamente relacionados con diversas formas de problemas entre productores y consumidoress.

# Producer
def follow(f):
    ...
    while True:
        ...
        yield line        # Produces value in `line` below
        ...

# Consumer
for line in follow(f):    # Consumes value from `yield` above
    ...

yield produce valores que for consume.

6.3.2 Tuberías de generador

Puede utilizar este aspecto de los generadores para configurar tuberías de procesamiento (como las tuberías Unix).

productor → procesamiento → procesamiento → consumidor

Las tuberías de procesamiento tienen un productor de datos inicial, un conjunto de etapas de procesamiento intermedias y un consumidor final.

productor → procesamiento → procesamiento → consumidor

def productor():
    ...
    yield item
    ...

El productor suele ser un generador. Aunque también podría ser una lista de alguna otra secuencia. yield alimenta datos en la canalización.

productor → procesamiento → procesamiento → consumidor

def consumidor(s):
    for item in s:
        ...

El consumidor es un bucle for-loop. Obtiene artículos y hace algo con ellos.

productor → procesamiento → procesamiento → consumidor

def procesando(s):
    for item in s:
        ...
        yield newitem
        ...

Las etapas intermedias de procesamiento consumen y producen artículos simultáneamente. Pueden modificar el flujo de datos. También pueden filtrar (descartar elementos).

productor → procesamiento → procesamiento → consumidor

def productor():
    ...
    yield item  # entrega el elemento que es recibido por `procesando`
    ...

def procesando(s):
    for item in s:      # Viene del `productor`
        ...
        yield newitem   # entrega el nuevo elemento
        ...

def consumidor(s):
    for item in s:      # Viene del `procesando`
        ...

Código para configurar la canalización

a = productor()
b = procesando(a)
c = consumidor(b)

Notará que los datos fluyen gradualmente a través de las diferentes funciones.

6.3.4 Ejercicios

Para este ejercicio, el programa stocksim.py debe seguir ejecutándose en segundo plano. Utilizará la función follow() que escribió en el ejercicio anterior.

Ejercicio 6.8: Configurar una canalización simple

Veamos la idea de la canalización en acción. Escribe la siguiente función:

>>> def filematch(lines, substr):
        for line in lines:
            if substr in line:
                yield line

>>>

Esta función es casi exactamente la misma que en el primer ejemplo de generador del ejercicio anterior, excepto que ya no abre un archivo, simplemente opera en una secuencia de líneas que se le da como argumento. Ahora, intente esto:

>>> from follow import follow
>>> lines = follow('Data/stocklog.csv')
>>> ibm = filematch(lines, 'IBM')
>>> for line in ibm:
        print(line)

... espera por la salida ...

La salida puede tardar un poco en aparecer, pero eventualmente debería ver algunas líneas que contienen datos para IBM.

Ejercicio 6.9: configurar una canalización más compleja

Lleve la idea de canalización unos pasos más allá realizando más acciones.

>>> from follow import follow
>>> import csv
>>> lines = follow('Data/stocklog.csv')
>>> rows = csv.reader(lines)
>>> for row in rows:
        print(row)

['BA', '98.35', '6/11/2007', '09:41.07', '0.16', '98.25', '98.35', '98.31', '158148']
['AA', '39.63', '6/11/2007', '09:41.07', '-0.03', '39.67', '39.63', '39.31', '270224']
['XOM', '82.45', '6/11/2007', '09:41.07', '-0.23', '82.68', '82.64', '82.41', '748062']
['PG', '62.95', '6/11/2007', '09:41.08', '-0.12', '62.80', '62.97', '62.61', '454327']
...

Bueno, eso es interesante. Lo que está viendo aquí es que la salida de la función follow() se ha canalizado a la función csv.reader() y ahora estamos obteniendo una secuencia de filas divididas.

Ejercicio 6.10: creación de más componentes de canalización

Extendamos toda la idea a una tubería más amplia. En un archivo separado ticker.py, comience creando una función que lea un archivo CSV como lo hizo anteriormente:

# ticker.py
from follow import follow
import csv

def parse_stock_data(lines):
    rows = csv.reader(lines)
    return rows

if __name__ == '__main__':
    lines = follow('Data/stocklog.csv')
    rows = parse_stock_data(lines)
    for row in rows:
        print(row)

Escribe una nueva función que seleccione columnas específicas:

# ticker.py
...
def select_columns(rows, indices):
    for row in rows:
        yield [row[index] for index in indices]
...
def parse_stock_data(lines):
    rows = csv.reader(lines)
    rows = select_columns(rows, [0, 1, 4])
    return rows

Ejecute su programa nuevamente. Debería ver la salida reducida así:

['BA', '98.35', '0.16']
['AA', '39.63', '-0.03']
['XOM', '82.45','-0.23']
['PG', '62.95', '-0.12']
...

Escriba funciones generadoras que conviertan tipos de datos y cree diccionarios. Por ejemplo:

```python
# ticker.py ...

def convert_types(rows, types):
    for row in rows:
        yield [func(val) for func, val in zip(types, row)]

def make_dicts(rows, headers):
    for row in rows:
        yield dict(zip(headers, row))
...
def parse_stock_data(lines):
    rows = csv.reader(lines)
    rows = select_columns(rows, [0, 1, 4])
    rows = convert_types(rows, [str, float, float])
    rows = make_dicts(rows, ['name', 'price', 'change'])
    return rows
...

Ejecute su programa nuevamente. Ahora debería tener un flujo de diccionarios como este:

{ 'name':'BA', 'price':98.35, 'change':0.16 }
{ 'name':'AA', 'price':39.63, 'change':-0.03 }
{ 'name':'XOM', 'price':82.45, 'change': -0.23 }
{ 'name':'PG', 'price':62.95, 'change':-0.12 }
...

Ejercicio 6.11: filtrado de datos

Escribe una función que filtre datos. Por ejemplo:

# ticker.py ...

def filter_symbols(rows, names):
    for row in rows:
        if row['name'] in names:
            yield row

Use esto para filtrar acciones solo para aquellas en su cartera:

import report
portfolio = report.read_portfolio('Data/portfolio.csv')
rows = parse_stock_data(follow('Data/stocklog.csv'))
rows = filter_symbols(rows, portfolio)
for row in rows:
    print(row)

Ejercicio 6.12: Poniéndolo todo junto

En el programa ticker.py, escriba una función ticker(portfile, logfile, fmt) que cree un ticker de acciones en tiempo real a partir de un portafolio, archivo de registro y formato de tabla determinados. Por ejemplo:

>>> from ticker import ticker
>>> ticker('Data/portfolio.csv', 'Data/stocklog.csv', 'txt')
      Name      Price     Change
---------- ---------- ----------
        GE      37.14      -0.18
      MSFT      29.96      -0.09
       CAT      78.03      -0.49
        AA      39.34      -0.32
...

>>> ticker('Data/portfolio.csv', 'Data/stocklog.csv', 'csv')
Name,Price,Change
IBM,102.79,-0.28
CAT,78.04,-0.48
AA,39.35,-0.31
CAT,78.05,-0.47
...

Discusión

Algunas lecciones aprendidas: puede crear varias funciones de generador y encadenarlas para realizar el procesamiento que involucre tuberías de flujo de datos. Además, puede crear funciones que empaqueten una serie de etapas de canalización en una sola llamada de función (por ejemplo, la función parse_stock_data()).

6.4 Expresiones generadoras

Esta sección presenta algunos temas adicionales relacionados con el generador, incluidas las expresiones del generador y el módulo itertools.

6.4.1 Expresiones generadoras

Una versión generadora de una lista de comprensión.

>>> a = [1,2,3,4]
>>> b = (2*x for x in a)
>>> b
<generator object at 0x58760>
>>> for i in b:
...   print(i, end=' ')
...
2 4 6 8
>>>

Diferencias con las comprensiones de listas.

  • No construye una lista.
  • El único propósito útil es la iteración.
  • Una vez consumido, no se puede reutilizar.

Sintaxis general.

(<expression> for i in s if <conditional>)

También puede servir como argumento de función.

sum(x*x for x in a)

Se puede aplicar a cualquier iterable.

>>> a = [1,2,3,4]
>>> b = (x*x for x in a)
>>> c = (-x for x in b)
>>> for i in c:
...   print(i, end=' ')
...
-1 -4 -9 -16
>>>

El uso principal de las expresiones generadoras es el código que realiza algunos cálculos en una secuencia, pero solo usa el resultado una vez. Por ejemplo, elimine todos los comentarios de un archivo.

f = open('somefile.txt')
lines = (line for line in f if not line.startswith('#'))
for line in lines:
    ...
f.close()

Con los generadores, el código se ejecuta más rápido y usa poca memoria. Es como un filtro aplicado a una corriente.

6.4.2 ¿Por qué generadores?

  • Muchos problemas se expresan mucho más claramente en términos de iteración.
    • Recorrer una colección de elementos y realizar algún tipo de operación (buscar, reemplazar, modificar, etc.).
    • Las canalizaciones de procesamiento se pueden aplicar a una amplia gama de problemas de procesamiento de datos.
  • Mejor eficiencia de la memoria.
    • Produzca valores solo cuando sea necesario.
    • Contraste con la construcción de listas gigantes.
    • Puede operar en transmisión de datos
  • Los generadores fomentan la reutilización del código
    • Separa la iteración del código que usa la iteración
    • Puede crear una caja de herramientas de funciones de iteración interesantes y mezclar y combinar .

6.4.3 El módulo itertools

El itertoolses un módulo de biblioteca con varias funciones diseñadas para ayudar con iteradores / generadores.

itertools.chain(s1,s2) itertools.count(n) itertools.cycle(s) itertools.dropwhile(predicate, s) itertools.groupby(s) itertools.ifilter(predicate, s) itertools.imap(function, s1, ... sN) itertools.repeat(s, n) itertools.tee(s, ncopies) itertools.izip(s1, ... , sN)

Todas las funciones procesan datos de forma iterativa. Implementan varios tipos de patrones de iteración.

Más información en el tutorial de Trucos de generador para programadores de sistemas de PyCon '08.

6.4.4 Ejercicios

En los ejercicios anteriores, escribió un código que siguió a las líneas que se escribieron en un archivo de registro y las analizó en una secuencia de filas. Este ejercicio continúa basándose en eso. Asegúrese de que Data/stocksim.py aún se esté ejecutando.

Ejercicio 6.13: Expresiones generadoras

Las expresiones generadoras son una versión generadora de una lista de comprensión. Por ejemplo:

>>> nums = [1, 2, 3, 4, 5]
>>> squares = (x*x for x in nums)
>>> squares
<generator object <genexpr> at 0x109207e60>
>>> for n in squares:
...     print(n)
...
1
4
9
16
25

A diferencia de una lista de comprensión, una expresión generadora solo se puede usar una vez. Por lo tanto, si prueba otro ciclo for, no obtiene nada:

>>> for n in squares:
...     print(n)
...
>>>

Ejercicio 6.14: Expresiones generadoras en argumentos de funciones

Las expresiones generadoras a veces se colocan en argumentos de función. Parece un poco extraño al principio, pero prueba este experimento:

>>> nums = [1,2,3,4,5]
>>> sum([x*x for x in nums])  # comprension de lista
55
>>> sum(x*x for x in nums)  # una expresión generadora
55
>>>

En el ejemplo anterior, la segunda versión que usa generadores usaría significativamente menos memoria si se manipulara una lista grande.

En su portfolio.pyarchivo, realizó algunos cálculos relacionados con listas por comprensión. Intente reemplazarlos con expresiones generadoras.

Ejercicio 6.15: simplificación de código

Las expresiones de generadores son a menudo un reemplazo útil para las funciones de pequeños generadores. Por ejemplo, en lugar de escribir una función como esta:

def filter_symbols(rows, names):
    for row in rows:
        if row['name'] in names:
            yield row

Podrías escribir algo como esto:

rows = (row for row in rows if row['name'] in names)

Modifique el programa ticker.py para usar expresiones generadoras según corresponda.