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)
def add(self, ch):
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:
garbage.add(garbage_char)
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())