2
\$\begingroup\$

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())
\$\endgroup\$
1
\$\begingroup\$

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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.