# Python coroutine structure

I've completed Advent of Code's Day 9 in Python3, trying to use it to teach myself coroutines. I'm very familiar with generators, but I've pretty much never used coroutines before.

I've ended up with something that works (I got the answers correct for both parts of the challenge), but my use of coroutines feels really fragile in some poorly-defined sense, and I was particularly disappointed that I couldn't make a program in which I used yield from; I thought that this was the kind of thing that yield from was built for.

Overall strategy: input an iterable string containing the expression to parse. Hand that iterable to various generators in turn, depending on what is required; those consume the characters they need and then yield their answers before being destroyed.

A group is as defined in the problem: anything contained in {} in the input. score is the "score" of the input (i.e. the number of groups in the tree or in any subtree). garbage_len is the total length of the garbage objects in the tree or any subtree. I'm not really concerned with how to improve those two methods; I'd like feedback on the idiomatic use of generators/coroutines.

class color:
RED = '\033[91m'
END = '\033[0m'

class Garbage:
def __init__(self):
self.characters = []

def __str__(self):
return '{}{}{}'.format(color.RED, ''.join(str(s) for s in self.characters), color.END)

self.characters.append(ch)

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

def __bool__(self):
return True

class States(enum.Enum):
Group = 0
Garbage = 1
Escaped = 2

class Group:
def __init__(self):
self.contents = []

def __str__(self):
# A group contains a list of {Garbage or Group}.
return '{' + ','.join(str(s) for s in self.contents) + '}'

def score(self, depth=0):
ans = depth + 1
for g in self.contents:
if isinstance(g, Group):
ans += g.score(depth + 1)
return ans

def garbage_len(self):
ans = 0
for gr in self.contents:
if isinstance(gr, Group):
ans += gr.garbage_len()
else:
assert isinstance(gr, Garbage)
ans += len(gr)
return ans

def curr_garbage(gen):
# Coroutine which consumes garbage characters and yields characters.
# Stops when the current garbage is finished.
state = States.Garbage
for ch in gen:
if state == States.Escaped:
state = States.Garbage
elif state == States.Garbage:
if ch == '!':
state = States.Escaped
elif ch == '>':
return
else:
yield ch

def curr_group(gen):
# Coroutine which consumes characters and yields Groups and Garbages.
for ch in gen:
if ch == '{':
# New group.
subgroup = Group()
subgroup_gen = curr_group(gen)
subgroup.contents = list(sub)
yield subgroup
elif ch == ',':
pass
elif ch == '}':
# End of current group
raise StopIteration
elif ch == '<':
# New garbage
garbage = Garbage()
garbage_gen = curr_garbage(gen)
for garbage_char in garbage_gen:
yield garbage
else:
raise ValueError("Malformed string, received character {}".format(ch))

if __name__ == '__main__':
gen = curr_group(iter(line))
tree = next(gen)
print(tree.score())
print(tree.garbage_len())


The expression ''.join(str(s) for s in self.characters simply returns a copy of self.characters. Consider phrasing it

    return color.RED + self.characters + color.END


Similarly ','.join(str(s) for s in self.contents seems longer than necessary.

This does not appear to be used:

    def __bool__(self):
return True


I'm afraid you lost me here:

    gen = curr_group(iter(line))


I don't see any mentions that define line.

Yielding a value to co-routine seems to be working fine, just as you wish.