1
\$\begingroup\$

I'm writing the following:

def make_table(data: str, w: int) -> List[List]:
    pass
    

data is a string containing items separated by ,. Some items may be an empty sequence so multiple commas are possible, the input can also start/end with comma which mean that first/last item is empty. w stands for width and is the number of columns of the output table. We're assuming the input is valid.

Examples:

In [1]: make_table("a,b,c,d,e,f,g,h,i",3)
Out[1]: [['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i']]

In [2]: make_table(",,1,,1,,1,,",3)
Out[2]: [['', '', '1'], ['', '1', ''], ['1', '', '']]

My first working solution is this:

def make_table(data: str, w: int) -> list:
    data = data.split(",")
    return [data[i*w:(i+1)*w] for i in range(len(data)//w)]

What I don't like here is that

  1. range(len(...)) gives me bad memories
  2. I feel like this could be done prettier.

I know numpy could do it, but that's overkill. I'm looking through std libs but don't see anything related.

My second solution is more efficient but a little roundabout, I was looking for some lazy split solution but found only some rejected proposals. I did this:

def coma_split(data: str):
    i, j = 0, -1
    while True:
        i, j = j + 1, data.find(",", j + 1)
        if j != -1:
            yield data[i:j]
        else:
            yield data[i:]
            break


def make_3_column_table(data: str) -> list:
    g = coma_split(data)
    return list(zip(g,g,g))
    

But here I don't know how to make zip take arbitrary number of references to g.

How can I improve any of those?

\$\endgroup\$
0

2 Answers 2

3
\$\begingroup\$

I was looking for some lazy split solution but found only some rejected proposals

If you are looking for a lazy split solution, omit the list(...) call in the return statement, and the zip object will be returned.

def make_3_column_table(data: str) -> Iterator[tuple[str]]:
    g = coma_split(data)
    return zip(g,g,g)   # No list() call here

It looks like you are looking for the grouper recipe of itertools.

from more_itertools import grouper

def make_table(data: str, w: int) -> list[tuple[str]]:
    return list(grouper(data.split(','), w))

Or without the additional library (and without the fill padding as you assert the data is well formed):

def make_table(data: str, w: int) -> list[tuple[str]]:
    return list(zip(*[iter(data.split(','))] * w))

Note that the return type is not the requested original List[List] return type, but neither was the output of make_3_column_table(...).

Again, if a lazy option is desired with an Iterable[...] return type instead of the list[...] return type, then omit the list(...) calls from the return statements and adjust the return signature.

\$\endgroup\$
3
\$\begingroup\$

Python ranges have a step attribute. Your first implementation of the function is on the right track, but you can simplify things further by using the range to the fullest and eliminating most of your own mathematical hoop-jumping:

def make_table(comma_delimited, w):
    vals = comma_delimited.split(',')
    return [
        vals[i : i + w]
        for i in range(0, len(vals), w)
    ]

You are losing values. What if the number of values is not evenly divisible by w. Currently, you are throwing the surplus values away. Is that what you want? If so, just modify the second argument to range(): w * (len(vals) // w).

Avoid variable re-definition. Code like this is generally a bad idea. It's the moral equivalent of changing the meaning of term in the middle of a paragraph. That undermines readability. And anything that hampers readability increases the odds of confusion and/or bugs.

# What? I was just getting familiar with "data".
data = data.split(",")

Consider the csv module if the delimited string can contain escaped commas. For known-to-be-simple delimited data, str.split() is fine. For true csv data, use a proper parser:

import csv

def make_table(comma_delimited, w):
    vals = list(csv.reader([comma_delimited]))[0]
    ...

Lazy for the sake of laziness. If you are committed to your second approach, you can accommodate the w argument in the following manner. I don't really understand the purpose of a lazy implementation when the comma-delimited data is already committed to memory, but perhaps I'm overlooking a valid use case:

def make_table(comma_delimited, w):
    g = coma_split(comma_delimited)
    gen = (g for _ in range(w))
    return list(map(list, zip(*gen)))
\$\endgroup\$
3
  • \$\begingroup\$ love the comment for re-definition :D, variable names makes more sense too. Do you think the 2nd approach is dead end? \$\endgroup\$
    – Kazz
    Commented Dec 23, 2021 at 18:30
  • \$\begingroup\$ @Kazz Yes, I ignored the second approach: it involves too much algorithm for such a simple task. \$\endgroup\$
    – FMc
    Commented Dec 23, 2021 at 18:32
  • \$\begingroup\$ @Kazz That said, it's not a dead end, as shown above. \$\endgroup\$
    – FMc
    Commented Dec 23, 2021 at 18:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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