6
\$\begingroup\$

I wanted to be able to parse Run Length Encoded (*.rle) files into something usable for a cellular automaton simulator (e.g. Conway's Game of Life and such).

Here is my main method which has the code I used for manual testing. The example file is taken from the above linked wiki.

def main():
    sample_rle = \
"""#N Gosper glider gun
#C This was the first gun discovered.
#C As its name suggests, it was discovered by Bill Gosper.
#O Bill Gosper Nov. 1970
x = 36, y = 9, rule = B3/S23
24bo$22bobo$12b2o6b2o12b2o$11bo3bo4b2o12b2o$2o8bo5bo3b2o$2o8bo3bob2o4b
obo$10bo5bo7bo$11bo3bo$12b2o! """

    rle_parser = RunLengthEncodedParser(sample_rle)
    print("name:", rle_parser.get_name())
    print("comments:")
    pprint.pprint(rle_parser.get_comments())
    print("author:", rle_parser.get_author())
    print("size_x:", rle_parser.get_size_x())
    print("size_y:", rle_parser.get_size_y())
    print("rule_birth:", rle_parser.get_rule_birth())
    print("rule_survival:", rle_parser.get_rule_survival())
    print("pattern_raw:", rle_parser.get_pattern_raw())
    #print("pattern_2d_array:")
    #print(rle_parser.get_pattern_2d_array())
    print("human_friendly_pattern:")
    print(rle_parser.get_human_friendly_pattern())

The console output of the file is as follows. I left out the output from print(rle_parser.get_pattern_2d_array()) as it is just a 2D list of b (dead) and o (alive) cells. For the sake of human readability, I replaced b dead cells with dots in the return from get_human_friendly_pattern.

name: Gosper glider gun
comments:
['This was the first gun discovered.',
 'As its name suggests, it was discovered by Bill Gosper.']
author: Bill Gosper Nov. 1970
size_x: 36
size_y: 9
rule_birth: [3]
rule_survival: [2, 3]
pattern_raw: 24bo$22bobo$12b2o6b2o12b2o$11bo3bo4b2o12b2o$2o8bo5bo3b2o$2o8bo3bob2o4bobo$10bo5bo7bo$11bo3bo$12b2o!
human_friendly_pattern:
........................o...........
......................o.o...........
............oo......oo............oo
...........o...o....oo............oo
oo........o.....o...oo..............
oo........o...o.oo....o.o...........
..........o.....o.......o...........
...........o...o....................
............oo......................

I used an OOP approach with the intention of only having to parse the file once, getting all the data from it. The above example is tiny, some of these files can contain thousands of rows and columns.

It works fine, but some parts of the populate_attributes method are nested up to 7 deep, and it feels like there should be some ways to improve its performance. Then again, the most deeply nested portions really only parse one line of characters, so I don't suspect the performance would suffer that much from it...

On another note, should I keep all those getters? Being that many of them just return an instance attribute, I thought perhaps just using the attributes in the calling code would be fine, but I don't know if that would be ideal. Not that anyone could stop someone from doing so anyways in Python, but if I plan on adapting this in other languages that support private attributes, would using getters like this be a sensible approach?

I'd like to hear any other improvements as well, from readability to structure to naming. Here is a working version on repl.it, and the RunLengthEncodedParser class below.

class RunLengthEncodedParser:
    """
    Parser for Run Length Encode (RLE) strings / files.
    More information: http://www.conwaylife.com/w/index.php?title=Run_Length_Encoded
    """
    def __init__(self, rle_string):
        self.rle_string = rle_string
        self.name = ""
        self.comments = []
        self.author = ""
        self.size_x = 0
        self.size_y = 0
        self.rule_birth = []
        self.rule_survival = []
        self.pattern_raw = ""
        # Fill in instance attributes by parsing the raw strings
        self.populate_attributes(self.rle_string.strip().splitlines())
        self.pattern_2d_array = self.populate_pattern(self.pattern_raw, self.size_x, self.size_y)

    def populate_attributes(self, lines):
        """
        This method performs all the string parsing required to parse the various 
        fields of data into their respective data members.
        """
        for line in lines:
            # Name of the pattern
            if line.startswith("#N"):
                self.name = line.lstrip("#N ")
            # Comments accompanying the pattern
            elif line.startswith("#C") or line.startswith("#c"):
                self.comments.append(line.lstrip("#Cc "))
            # Authorship of the pattern
            elif line.startswith("#O"):
                self.author = line.lstrip("#O ")
            # Grid sizes and rules
            elif line.startswith("x"):
                data = line.split(",")
                for d in data:
                    # Grid sizes
                    if d.strip().startswith("x"):
                        _, x = d.split("=")
                        self.size_x = int(x.strip())
                    elif d.strip().startswith("y"):
                        _, y = d.split("=")
                        self.size_y = int(y.strip())
                    # Rules
                    elif d.strip().startswith("rule"):
                        _, rule = d.split("=")
                        for r in rule.strip().split("/"):
                            if r.startswith("B"):
                                for digit in list(r.lstrip("B")):
                                    self.rule_birth.append(int(digit))
                            if r.startswith("S"):
                                for digit in list(r.lstrip("S")):
                                    self.rule_survival.append(int(digit))
            # Other lines should contain the actual pattern
            else:
                self.pattern_raw += line.strip(" \n\r\t")

    def populate_pattern(self, pattern_raw, size_x, size_y, default_cell='b'):
        pattern = []
        pattern_rows = pattern_raw.rstrip("!").split("$")
        assert len(pattern_rows) == size_y, \
        "Number of data rows {0} does not match size y = {1}".format(len(pattern_rows), size_y)
        for y in range(size_y):
            pattern.append([])
            tmp_num_str = ""
            for c in pattern_rows[y]:
                if self.isdigit(c):
                    tmp_num_str += c
                else:
                    if tmp_num_str == "":
                        num_cells = 1
                    else:
                        num_cells = int(tmp_num_str)
                    for n in range(num_cells):
                        pattern[y].append(c)
                    #reset count until another number is encountered
                    tmp_num_str = ""
            #fill in empty spaces at end of each row
            for _ in range(len(pattern[y]), size_x):
                pattern[y].append(default_cell)
        return pattern


    def isdigit(self, c):
        """Returns True is the character is a digit"""
        return '0' <= c <= '9'


    def __str__(self):
        return self.rle_string

    # Getters
    def get_name(self):
        return self.name

    def get_comments(self):
        return self.comments

    def get_author(self):
        return self.author

    def get_size_x(self):
        return self.size_x

    def get_size_y(self):
        return self.size_y

    def get_rule_birth(self):
        return self.rule_birth

    def get_rule_survival(self):
        return self.rule_survival

    def get_pattern_raw(self):
        return self.pattern_raw

    def get_pattern_2d_array(self):
        return self.pattern_2d_array

    def get_human_friendly_pattern(self):
        pattern_str = ""
        for row in self.pattern_2d_array:
            row_str = ""
            for c in row:
                if c == 'b':
                    row_str += '.'
                else:
                    row_str += c
            pattern_str += row_str + '\n'
        return pattern_str
\$\endgroup\$
4
\$\begingroup\$

In Python using getters when not necessary is frowned upon.

If you really need them, use a property:

class A:
    def __init__(self, x):
        self.l = x   #  attribute can be accessed directly
        self._api_code = "XXX"   # attribute will be accessed as property

    @property
    def size(self):
        return len(self.l)

    @property
    def api_code(self):
        return self._api_code

>>> a = A([1,2,3])
>>> a.size
3
>>> a.l
[1, 2, 3]

This would also make you main a lot easier:

def main():
    sample_rle = \
"""#N Gosper glider gun
#C This was the first gun discovered.
#C As its name suggests, it was discovered by Bill Gosper.
#O Bill Gosper Nov. 1970
x = 36, y = 9, rule = B3/S23
24bo$22bobo$12b2o6b2o12b2o$11bo3bo4b2o12b2o$2o8bo5bo3b2o$2o8bo3bob2o4b
obo$10bo5bo7bo$11bo3bo$12b2o! """

    rle_parser = RunLengthEncodedParser(sample_rle)
    print("""name: {self.name}
comments: {self.comments}
author: {self.author}
size_x: {self.size_x}
size_y: {self.size_y}
rule_birth: {self.rule_birth}
rule_survival: {self.rule_survivor}
pattern_raw: {self.pattern_raw}
human_friendly_pattern: {self.human_friendly_pattern}""".format(self=rle_parser))

For most of these, you don't need to change anything. Only self.comments and self.human_friendly_pattern would need a definition using @property, for the rest, direct attribute access is sufficient.

And actually, you can now move this whole string into RunLengthEncodedParser.__str__, then you can just do print(rle_parser). If you want to keep that one as just the string, override RunLengthEncodedParser.__format__

Final code:

import pprint

class RunLengthEncodedParser:
    """
    Parser for Run Length Encode (RLE) strings / files.
    More information: http://www.conwaylife.com/w/index.php?title=Run_Length_Encoded
    """
    def __init__(self, rle_string):
        self.rle_string = rle_string
        self.name = ""
        self._comments = []    # Note underscore
        self.author = ""
        self.size_x = 0
        self.size_y = 0
        self.rule_birth = []
        self.rule_survival = []
        self.pattern_raw = ""
        # Fill in instance attributes by parsing the raw strings
        self.populate_attributes(self.rle_string.strip().splitlines())
        self.pattern_2d_array = self.populate_pattern(self.pattern_raw, self.size_x, self.size_y)

    def populate_attributes(self, lines):
        ...

    def populate_pattern(self, pattern_raw, size_x, size_y, default_cell='b'):
        ...


    def isdigit(self, c):
        """Returns True is the character is a digit"""
        return '0' <= c <= '9'


    def __str__(self):
        return self.rle_string

    def __format__(self, fmt):
        return """name: {self.name}
comments: {self.comments}
author: {self.author}
size_x: {self.size_x}
size_y: {self.size_y}
rule_birth: {self.rule_birth}
rule_survival: {self.rule_survivor}
pattern_raw: {self.pattern_raw}
human_friendly_pattern: {self.human_friendly_pattern}""".format(self=self)

    @property
    def human_friendly_pattern(self):
        pattern_str = ""
        for row in self.pattern_2d_array:
            row_str = ""
            for c in row:
                if c == 'b':
                    row_str += '.'
                else:
                    row_str += c
            pattern_str += row_str + '\n'
        return pattern_str

    @property
    def comments(self):
        return pprint.pformat(self._comments)


def main():
    sample_rle = \
"""#N Gosper glider gun
#C This was the first gun discovered.
#C As its name suggests, it was discovered by Bill Gosper.
#O Bill Gosper Nov. 1970
x = 36, y = 9, rule = B3/S23
24bo$22bobo$12b2o6b2o12b2o$11bo3bo4b2o12b2o$2o8bo5bo3b2o$2o8bo3bob2o4b
obo$10bo5bo7bo$11bo3bo$12b2o! """

    rle_parser = RunLengthEncodedParser(sample_rle)
    print(format(rle_parser))

In Python 3.6, this gets slightly easier, because you can replace """...{self.author}...""".format(self=self) with f-strings using f"""...{self.author}...""".

\$\endgroup\$
5
\$\begingroup\$

Apart from @Graipher's great answer (my answer is based on his), I'd use the following when it comes to multi-line styling:

from pprint import pformat


class RunLengthEncodedParser:
    """
    Parser for Run Length Encode (RLE) strings / files.
    More information: http://www.conwaylife.com/w/index.php?title=Run_Length_Encoded
    """
    def __init__(self, rle_string):
        self.rle_string = rle_string
        self.name = ""
        self._comments = []    # Note underscore
        self.author = ""
        self.size_x = 0
        self.size_y = 0
        self.rule_birth = []
        self.rule_survival = []
        self.pattern_raw = ""

        # Fill in instance attributes by parsing the raw strings
        self.populate_attributes(self.rle_string.strip().splitlines())
        self.pattern_2d_array = self.populate_pattern(self.pattern_raw, self.size_x, self.size_y)

    def populate_attributes(self, lines):
        ...

    def populate_pattern(self, pattern_raw, size_x, size_y, default_cell='b'):
        ...

    def isdigit(self, c):
        """Returns True is the character is a digit"""
        return '0' <= c <= '9'

    def __str__(self):
        return self.rle_string

    def __format__(self, fmt):
        return 'name: {self.name}\n' \
               'comments: {self.comments}\n' \
               'author: {self.author}\n' \
               'size_x: {self.size_x}\n' \
               'size_y: {self.size_y}\n' \
               'rule_birth: {self.rule_birth}\n' \
               'rule_survival: {self.rule_survivor}\n' \
               'pattern_raw: {self.pattern_raw}\n' \
               'human_friendly_pattern: {self.human_friendly_pattern}\n'.format(self=self)

    @property
    def human_friendly_pattern(self):
        pattern_str = ""
        for row in self.pattern_2d_array:
            row_str = ""
            for c in row:
                if c == 'b':
                    row_str += '.'
                else:
                    row_str += c
            pattern_str += row_str + '\n'
        return pattern_str

    @property
    def comments(self):
        return pformat(self._comments)


def main():
    sample_rle = '#N Gosper glider gun\n' \
                 '#C This was the first gun discovered.\n' \
                 '#C This was the first gun discovered.\n' \
                 '#C As its name suggests, it was discovered by Bill Gosper.\n' \
                 '#O Bill Gosper Nov. 1970\n' \
                 'x = 36, y = 9, rule = B3/S23\n' \
                 '24bo$22bobo$12b2o6b2o12b2o$11bo3bo4b2o12b2o$2o8bo5bo3b2o$2o8bo3bob2o4b\n' \
                 'obo$10bo5bo7bo$11bo3bo$12b2o!\n'

    rle_parser = RunLengthEncodedParser(sample_rle)
    print(format(rle_parser))
if __name__ == '__main__':
    main()

For me, the trade-off of using \n and \ to gain the correct indentation makes sense + makes those snippets look clear.


I also added the from pprint import pformat at the top of the code.

\$\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.