python - pairwise - itertools.tee en una coroutine?
pairwise python (3)
Tengo una estructura arbórea de objetos. Necesito iterar sobre todos los elementos ("valores") en las hojas. Para esto actualmente estoy usando métodos generadores como se ilustra a continuación:
class Node(object):
def __init__(self):
self.items = [Leaf(1), Leaf(2), Leaf(3)]
def values(self):
for item in self.items:
for value in item.values():
yield value
class Leaf(object):
def __init__(self, value):
self.value = value
def values(self):
for i in range(2):
yield self.value
n = Node()
for value in n.values():
print(value)
Esto imprime:
1
1
2
2
3
3
Ahora, los valores devueltos por una Leaf
dependerán de un parámetro externo. Estaba pensando en emplear coroutines para poder pasar este parámetro a los nodos de la hoja:
import itertools
class Node2(object):
def __init__(self):
self.items = [Leaf2(1), Leaf2(2), Leaf2(3)]
def values(self):
parameter = yield
for item in self.items:
item_values = item.values()
next(item_values) # advance to first yield
try:
while True:
parameter = (yield item_values.send(parameter))
except StopIteration:
pass
class Leaf2(object):
def __init__(self, value):
self.value = value
def values(self):
parameter = yield
try:
for i in range(2):
parameter = (yield ''{}{}''.format(self.value, parameter))
except StopIteration:
pass
n2 = Node2()
values2 = n2.values()
next(values2) # advance to first yield
try:
for i in itertools.count(ord(''A'')):
print(values2.send(chr(i)))
except StopIteration:
pass
Este código está lejos de ser bonito, pero funciona. Se imprime:
1A
1B
2C
2D
3E
3F
Sin embargo, hay un problema con esta solución. Estaba usando itertools.tee
(y chain
) ampliamente para guardar fácilmente el estado del iterador en caso de que tuviera que retroceder. Esperaba que esto también funcionara en las coroutinas, pero desgraciadamente no hubo tanta suerte.
Algunas soluciones alternativas que estoy considerando en este momento:
- hacer que los generadores rindan funciones (cierres) que acepten el parámetro externo
- escriba clases personalizadas para emular coroutines con capacidades para guardar el estado
La primera opción parece la más atractiva. Pero tal vez hay mejores opciones?
Algún contexto: estoy usando esta construcción en RinohType donde el árbol está formado por los MixedStyledText
(node) y SingleStyledText
(leaf). Los métodos SingleStyledText
spans()
producen instancias de SingleStyledText
. Este último puede depender de parámetros externos. Por ejemplo, el número de página a la que se están procesando. Estos son actualmente tratados como un caso especial.
En la medida de lo posible, me gusta hacer que las funciones devuelvan estructuras de datos simples. En este caso,
- Haría que cada nodo
RinohType
un diccionario simple (o, en el caso deRinohType
, un objetoSingleStyledTextConfig
onamedtuple
). Esta implementación funcionará muy bien conitertools
, como antes, ya que simplemente transforma los datos en otros datos. - Transformaría esta colección de objetos para tener en cuenta los parámetros externos (como el número de página).
- Finalmente, usaría la recopilación de datos de configuración para crear realmente la salida (en el caso de
RinohType
, el objetoSingleStyledText
).
En un nivel superior: su solución propuesta (como entiendo su versión simplificada) está tratando de hacer muchas cosas al mismo tiempo . Se trata de configurar la creación de objetos, ajustar esa configuración y crear objetos basados en esa configuración en un solo paso. Su implementación será más sencilla, juegue mejor con itertools
y será más fácil de probar si separa estas preocupaciones.
Para un tratamiento más detallado de este tipo de pensamiento, vea la charla Los límites de Gary Bernhardt y la charla de Brandon Rhodes sobre la arquitectura limpia en Python (y, por supuesto, los recursos que mencionan en las charlas).
Este es un comienzo en la segunda opción.
from types import GeneratorType
def gen_wrapper(func):
def _inner(*args):
try:
if args:
if isinstance(args[0], GeneratorType):
func.gen = getattr(func, ''gen'', args[0])
func.recall = next(func.gen)
try:
return func.recall
except AttributeError:
func.recall = next(func.gen)
return func.recall
except StopIteration:
pass
return _inner
@gen_wrapper
def Gen_recall(*args):
pass
No estoy seguro si entiendo la pregunta correctamente. ¿Es necesario que Leaf2 haga los cálculos? Si no, entonces puedes hacer:
import itertools
class Node2(object):
def __init__(self):
self.items = [Leaf2(1), Leaf2(2), Leaf2(3)]
def values(self):
for item in self.items:
for value in item.values():
yield value
class Leaf2(object):
def __init__(self, value):
self.value = value
def values(self):
for i in range(2):
yield self.value
def process(i, parameter):
return ''{}{}''.format(i, parameter)
n = Node2()
for value, parameter in zip(n.values(), itertools.count(ord(''A''))):
print(process(value, chr(parameter)))
Si es realmente importante que Leaf2 haga el procesamiento, usted podría hacer
class Leaf2:
def values(self):
for i in range(2):
yield self
def process(self, parameter):
pass
Para que en lo principal se pueda hacer.
n = Node2()
for node, parameter in zip(n.values(), itertools.count(ord(''A''))):
print(node.process(chr(parameter)))