5
\$\begingroup\$

I wrote this module in python for some minor tabulation needs in my science projects. This was very helpful for testing, debugging and plotting various data. I often use dictionaries for storing such data, either as an array or as in a spreadsheet (column and row). This tiny script helps me to plot such data in a similar fashion. Since I find myself using it a lot, I thought of improving it in terms of performance and readability. Also, I do understand that there are several third party libraries available for python to do the same with much more functionality, but I did it anyway for learning the language and ... well, and for fun. How could I refactor this code so that I could optimize it and possibly expand it in the future?

Summary

  • For dictionaries only
  • Rows can be specified separately for excluding some data
  • Automatic column spacing
  • Automatic table breaking (terminal size)
  • Specify headers and alignment

Dictionaries represent columns. Their keys marks the rows and their values contains the cell data. This produces the structure cell=column[row] which is very much similar to the A1 structure in spreadsheet programs.

I have added doc strings and comments to make this somewhat readable, but I would very much appreciate it if you could suggest an improvement. The Table class could be instantiated to specify rows and other features. User could then add columns using AddColumn method and that's pretty much it. Use Show to view the final table.

The [[]] in the code are used for containing info about each breaks in the table. [[break1], [break2], [break3] ...]. Each of these breaks contains columns that could fit inside the terminal window. [[c1,c2,c3...c7], [c8,c9], [c10,c11...] ...].

from shutil import get_terminal_size as termsize


class Table(object):
    """Lightweight tabulation class for dictionary type containers.

    Dictionaries represents columns of the table, and their values the cells.
    Keys identifies the rows of the table.
    """

    def __init__(self, rows, header="",align="<"):
        """Initialize the table by setting rows.

        'rows' must be an iterable containing the keys for every column. Each
        cell in the table is represented as : cell = column[row].
        Header for the row can be specified through 'head'.
        """
        # Row info
        self.rows = rows
        self.rowlength = self.GetMaxLength(rows, [header]) + 2
        self.rowstyle = "".join(("{:", align, str(self.rowlength), "}"))
        self.rowhead = self.rowstyle.format(str(header))

        # Table info (list of lists)
        self.columns = [[]]
        self.styles = [[]]
        self.headers = [[]]

        self.linelength = self.rowlength
        self.linelimit = termsize().columns


    def GetMaxLength(self, *iterables, threshold=0):
        """Returns the length of the longest item among the iterables.

        'threshold', if specified, must be the cut-off value of length.
        """
        for item in iterables:
            length = max(len(str(i)) for i in item)
            if length > threshold:
                threshold = length
        return threshold


    def AddColumn(self, content, header="", align="<"):
        """Adds a column to the table.

        'content' must be a dictionary containing the column values.
        Header for the column can be specified using 'header' argument.
        """
        length = self.GetMaxLength(content.values(), [header]) + 2
        style = "".join(("{:", align, str(length), "}"))

        self.linelength += length
        if self.linelength > self.linelimit:
            # Breaks the table
            self.columns.append([content])
            self.styles.append([style])
            self.headers.append([style.format(str(header))])
            self.linelength = self.rowlength + length
        else:
            # Resumes without breaking the table
            self.columns[-1].append(content)
            self.styles[-1].append(style)
            self.headers[-1].append(style.format(str(header)))


    def Show(self):
        """Displays the table."""
        for cols, stls, heds in zip(self.columns, self.styles, self.headers):
            # Print headers
            print("\n\x1b[7;1m", self.rowhead, "".join(heds), "\x1b[0m", sep="")
            # Print cells
            for row in self.rows:
                line = [s.format(str(c[row])) for s, c in zip(stls,cols)]
                print("\x1b[1m", self.rowstyle.format(row), "\x1b[0m",
                      "".join(line), sep="")


def test():
    # Sample data
    from random import sample
    rows = sample(range(100,200), 10)
    data = {k:v for k,v in zip(rows, sample(Table.__doc__.split(),10))}

    # Tabulate them
    table = Table(rows, header="INDEX")
    for i in range(3):
        table.AddColumn(data, header="LEFT ALIGN")
    for i in range(3):
        table.AddColumn(data, header="CENTRE ALIGN", align="^")
    for i in range(3):
        table.AddColumn(data, header="RIGHT ALIGN", align=">")
    table.Show()


if __name__ == '__main__':
    test()
\$\endgroup\$
3
\$\begingroup\$

I think you shouldn't use the word threshold as a GetMaxLength parameter. It implies to me that it's a maximum value, but it's instead treated as a minimum. You then say it's the "cut-off value of length" which is ambiguous to me again. I'd just call it min and then it's self explanatory.

def GetMaxLength(self, *iterables, min=0):
    """Returns the length of the longest item among the iterables.

    'min', if specified, is the minimum possible value of length.
    """

Also I think draw is a more appropriate name for printing the table. You could possibly even implement it as __repr__ so that the table could be drawn with repr(Table). Though in that case I'd implement a nice __str__ method too in case people try to print it unwittingly in a sentence because '{}'.format(Table) would call __repr__ if you didn't have a __str__ method defined.

Your naming in general should match the style guide (PEP0008), which suggests that function names be all lowercase with underscores as separators, not CamelCase. So get_max_length would be more appropriate.

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