2009-11-16 15 views
6

Actualización: Lo que realmente quería desde el principio era greenlets.¿Programación estructurada y generadores Python?


Nota: Esta pregunta mutado un poco como la gente respondió y me obligó a "subir la apuesta", como mis ejemplos triviales tenían simplificaciones triviales; en lugar de continuar mutando aquí, dejaré la pregunta cuando la tenga más clara en mi cabeza, según la sugerencia de Alex.


generadores de Python son una cosa de belleza, pero ¿cómo puedo romper fácilmente uno en módulos (programación estructurada)? Yo efectivamente quiero PEP 380, o al menos algo comparable en la carga de la sintaxis, pero en Python existente (por ejemplo 2.6)

Como un ejemplo (la verdad es estúpida), tome las siguientes:

def sillyGenerator(): 
    for i in xrange(10): 
    yield i*i 
    for i in xrange(12): 
    yield i*i 
    for i in xrange(8): 
    yield i*i 

Siendo un ferviente creyente en SECO, diviso el patrón que se repite aquí y el factor hacia fuera en un método:

def quadraticRange(n): 
    for i in xrange(n) 
    yield i*i 

def sillyGenerator(): 
    quadraticRange(10) 
    quadraticRange(12) 
    quadraticRange(8) 

... que por supuesto no funciona. El padre debe llamar a la nueva función en un bucle, produciendo los resultados:

def sillyGenerator(): 
    for i in quadraticRange(10): 
    yield i 
    for i in quadraticRange(12): 
    yield i 
    for i in quadraticRange(8): 
    yield i 

... que es incluso más largo que antes!

Si deseo insertar parte de un generador en una función, siempre necesito este envoltorio bastante detallado y de dos líneas para llamarlo. Se pone peor si deseo admitir el envío():

def sillyGeneratorRevisited(): 
    g = subgenerator() 
    v = None 
    try: 
    while True: 
     v = yield g.send(v) 
    catch StopIteration: 
    pass 
    if v < 4: 
    # ... 
    else: 
    # ... 

Y eso no está teniendo en cuenta la aprobación de excepciones. ¡El mismo texto repetitivo todo el tiempo! Sin embargo, uno no puede aplicar DRY y factorizar este código idéntico en una función, porque ... ¡necesitaría el texto repetitivo para llamarlo! Lo que quiero es algo como:

def sillyGenerator(): 
    yield from quadraticRange(10) 
    yield from quadraticRange(12) 
    yield from quadraticRange(8) 

def sillyGeneratorRevisited(): 
    v = yield from subgenerator() 
    if v < 4: 
    # ... 
    else: 
    # ... 

¿Alguien tiene una solución a este problema? Tengo un intento de primer intento, pero me gustaría saber qué han hecho otros. En última instancia, cualquier solución tendrá que abordar ejemplos en los que el generador principal realiza una lógica compleja basada en el resultado de los datos enviados al generador, y potencialmente hace una gran cantidad de llamadas a los subgeneradores: mi caso de uso son los generadores utilizados para implementar máquinas de estado complejas de larga ejecución.

+0

¿Podría simplificar su problema real (no demasiado, solo lo suficiente) en lugar de inventar uno que no parece coincidir muy bien? –

+0

Ejemplo: no realmente. Solo quiero una respuesta sobre cómo puedo descomponer un generador complejo sin un ámbito de repetición, similar a la declaración de "rendimiento de" que se describe a continuación, pero en Python existente. Los trucos que satisfacen casos simples no son suficientes :( –

Respuesta

11

Sin embargo, me gustaría hacer mis criterios de reutilización en un escalón más difícil: lo que si necesito una estructura de control alrededor de mi generación repetida?

itertools a menudo ayuda incluso allí - debe proporcionar ejemplos concretos en los que cree que no es así.

Para ejemplo, puede ser que quiera llamar a un subgenerator siempre con diferentes parámetros .

itertools.chain.from_iterable.

O mis subgenerators podría ser muy costoso, y sólo quiero iniciarlos como y cuando están alcanzado.

Tanto chain y chain_from_iterable hacer eso - no hay sub-iterador se "puso en marcha" hasta el instante en que el primer punto de que sea necesario.

O (y esto es un verdadero deseo) que podría querer variar lo que hago próxima basado en lo que mi controlador de me pasa a través de Enviar().

Un ejemplo específico sería muy apreciado. De todos modos, en el peor de los casos, codificará for x in blargh: yield x, donde el Pep3080 suspendido le permitirá codificar yield from blargh, unos 4 caracteres adicionales (no es una tragedia ;-). Y si alguna versión de corutina sofisticada de algunas funciones de itertools (itertools principalmente soporta iteradores, no hay un módulo equivalente de coroutools) está garantizada, porque un cierto patrón de composición de corrutina se repite en su código, entonces no es demasiado difícil para codificarlo usted mismo

Por ejemplo, supongamos que a menudo nos encontramos haciendo algo como: primero cede un cierto valor; luego, repetidamente, si nos envían 'foo', cedemos el siguiente ítem de fooiter, si es 'bla', de blaiter, si 'zop', de zopiter, cualquier otra cosa, de defiter. Tan pronto como detectamos la segunda aparición de este patrón de composición, podemos codificar:

def corou_chaiters(initsend, defiter, val2itermap): 
    currentiter = iter([initsend]) 
    while True: 
    val = yield next(currentiter) 
    currentiter = val2itermap(val, defiter) 

y llamar a esta función de composición simple como y cuando sea necesario. Si necesitamos componer otras corutinas, en lugar de iteradores generales, tendremos un compositor ligeramente diferente utilizando el método de envío en lugar de la siguiente función incorporada; Etcétera.

Si puede ofrecer un ejemplo que no sea domesticado fácilmente por tales técnicas, le sugiero que lo haga en una pregunta separada (dirigida específicamente a generadores parecidos a Coroutine), ya que hay mucho material sobre este que tendrá poco que ver con su otro ejemplo, mucho más complejo/sofisticado.

+0

Evaluación justa. Gracias. –

3
import itertools 

def quadraticRange(n): 
    for i in xrange(n) 
    yield i*i 

def sillyGenerator(): 
    return itertools.chain(
    quadraticRange(10), 
    quadraticRange(12), 
    quadraticRange(8), 
) 

def sillyGenerator2(): 
    return itertools.chain.from_iterable(
    quadraticRange(n) for n in [10, 12, 8]) 

La última es útil si desea asegurarse de que un iterador se agota antes de que comienza el otro (incluyendo su código de inicialización).

+0

+1 Agradable. Me gusta la forma en que esto resuelve el 99% de los casos de uso (conjunto infinito de generadores, costosa construcción del generador). Sin embargo, realmente estoy pensando en casos de uso como corrutinas , donde la lógica en la función de nivel superior es compleja y varía en función de lo que envía la persona que llama. Mis ejemplos han resultado ser demasiado triviales :( –

6

Usted quiere chain several iterators juntos:

from itertools import chain 

def sillyGenerator(a,b,c): 
    return chain(quadraticRange(a),quadraticRange(b),quadraticRange(c)) 
+0

Bueno. Ciertamente resuelve el problema para ejemplos más simples como los que inicialmente Sin embargo, esto me ha obligado a extender mi pregunta, ya que los problemas que realmente estoy buscando tienen lógica de control en torno a la puesta en marcha de los generadores :( –

+2

'def sillyGenerator (* args): cadena de retorno (* map (quadraticRange, args)) 'para evitar la repetición :) –

2

Para un número arbitrario de llamadas a quadraticRange:

from itertools import chain 

def sillyGenerator(*args): 
    return chain(*map(quadraticRange, args)) 

Este código utiliza map y itertools.chain. Toma una cantidad arbitraria de argumentos y los pasa con el fin de quadraticRange. Los iteradores resultantes se encadenan.

3

Hay una propuesta de mejora de Python para proporcionar un yield from statement para "delegar generación". Su ejemplo podría ser escrito como:

def sillyGenerator(): 
    sq = lambda i: i * i 
    yield from map(sq, xrange(10)) 
    yield from map(sq, xrange(12)) 
    yield from map(sq, xrange(8)) 

O mejor, en el espíritu de SECO:

def sillyGenerator(): 
    for i in [10, 12, 8]: 
    yield from quadraticRange(i) 

La propuesta está en estado de borrador y su eventual inclusión no es cierto, pero muestra que otros desarrolladores comparte tus pensamientos sobre generadores

+0

Me acabas de ganar, mientras buscaba la sintaxis de referencia :) –

6

Impráctico respuesta (por desgracia):

from __future__ import PEP0380 

def sillyGenerator(): 
    yield from quadraticRange(10) 
    yield from quadraticRange(12) 
    yield from quadraticRange(8) 

referencia Potencialmente práctica: Syntax for delegating to a subgenerator

haciendo Desafortunadamente, este práctico: Python language moratorium

ACTUALIZACIÓN febrero de 2011:

La moratoria ha sido levantado, y PEP 380 está en t la lista TODO para Python 3.3. Espero que esta respuesta sea práctica pronto.

Leer Guido's remarks on comp.python.devel

+1

+1 para un enlace interesante y bueno. Esperando algo más inmediato mport though :) –

+0

Tenga en cuenta que NO estoy abogando por su uso, pero para el curioso pep380 se puede implementar en Pure Python con solo unas pocas modificaciones para que sea sintácticamente válido: rendimiento _de (cuadraticoRango (10)). http://code.activestate.com/recipes/577153-yet-another-python-implementation-of-pep-380-yield/ – tokland

1

Hay un patrón que llamo "generador kernel" donde los generadores no ceden directamente al usuario sino a algún bucle "kernel" que trata (algunos de) sus rendimientos como "llamadas al sistema" con un significado especial.

Puede aplicarlo aquí mediante una función intermedia que acepte generadores generados y los desenrolle automáticamente. Para que sea más fácil de usar, vamos a crear esa función intermedia en un decorador:

import functools, types 

def flatten(values_or_generators): 
    for x in values_or_generators: 
     if isinstance(x, GeneratorType): 
      for y in x: 
       yield y 
     else: 
      yield x 

# Better name anyone? 
def subgenerator(g): 
    """Decorator making ``yield <gen>`` mean ``yield from <gen>``.""" 

    @functools.wraps(g) 
    def flat_g(*args, **kw): 
     return flatten(g(*args, **kw)) 
    return flat_g 

y entonces sólo podemos escribir:

def quadraticRange(n): 
    for i in xrange(n) 
     yield i*i 

@subgenerator 
def sillyGenerator(): 
    yield quadraticRange(10) 
    yield quadraticRange(12) 
    yield quadraticRange(8) 

Tenga en cuenta que subgenerator() está desenrollando exactamente un nivel de La jerarquía. Podría hacerlo fácilmente en múltiples niveles (administrando una pila manual, o simplemente reemplazando el bucle interno con for y in flatten(x):, pero creo que es mejor como es, de modo que cada generador que quiera usar esta sintaxis no estándar debe ser explícitamente envuelto con @subgenerator.

Tenga en cuenta también que la detección de los generadores es imperfecto! detectará cosas escritas como generadores, pero eso es un detalle de implementación. como una persona que llama de un generador, lo que importa es que se devuelve un iterador. Podría ser una función que devuelva algún objeto itertools, y este decorador fallaría.

Comprobar si el objeto tiene un método .next() es demasiado amplio: no podrá ceder cadenas sin que se deshagan. la mayoría re caso responsable sería para comprobar si hay algún marcador explícita, por lo que iba a escribir ej .:

@subgenerator 
def sillyGenerator(): 
    yield 'from', quadraticRange(10) 
    yield 'from', quadraticRange(12) 
    yield 'from', quadraticRange(8) 

Hey, eso es casi como el PEP!

[Créditos: this answer da una función similar - pero es profunda (que considero mal) y no es un enmarcado como decorador]

0
class Communicator: 
    def __init__(self, inflow): 
     self.outflow = None 
     self.inflow = inflow 

entonces usted:

c = Communicator(something) 
yield c 
response = c.outflow 

Y en lugar del código repetitivo, simplemente puede hacer:

for i in run(): 
    something = i.inflow 
    # ... 
    i.outflow = value_to_return_back 

Es un código suficientemente simple que funciona sin mucho cerebro

Cuestiones relacionadas