2

I have a list of lists with the structure [[s: str, s: str, s: str]] and there can be any number of items (lists) inside of it.

I want to print this cleanly in columns like so:

FirstFirst     FirstSecond    FirstThird
SecondFirst    SecondSecond   SecondThird
ThirdFirst     ThirdSecond    ThirdThird
foo            bar            foobar

So, despite having different lengths, each item in the sublists are left-justified in a column.

I have already tried very complex list comprehensions like

lists = [['FirstFirst', 'FirstSecond', 'FirstThird'],
         ['SecondFirst', 'SecondSecond', 'SecondThird'],
         ['ThirdFirst', 'ThirdSecond', 'ThirdThird'],
         ['foo', 'bar', 'foobar']]

[print(f'{_[0]}{" " * (15 - len(_[0]))}{_[1]}{" " * (30 - len(_[0] + _[1] + " " * (15 - len(_[0]))))}{_[2]}') for _ in lists]

While this does work, it is very bruteforce-ish.

What's worse is that this list comprehension is not at all scalable. If I want to add another string to each item in the sublists, I'll have to add a lot more to my list comprehension to make it all still work. Also, everything is foiled if I want two lists to have different lengths.

What is a better way of doing this?

YulkyTulky
  • 886
  • 1
  • 6
  • 20

2 Answers2

4

Using list comprehensions for a side effect is bad style: you create a full list (wich takes time & memory) that gets thrown away afterwards - use simple loops.

You need to calculate the length of each word (in each column). Getting the maximum length of your words is simple:

data = [['FirstFirst', 'FirstSecond', 'FirstThird'],
         ['SecondFirst', 'SecondSecond', 'SecondThird'],
         ['ThirdFirst', 'ThirdSecond', 'ThirdThird'],
         ['foo', 'verylongwordinsidehere', 'bar', ]]    # changed to a longer one


# get max word length
max_len = max(len(i) for j in data for i in j)

# do not use list comp for printing side effect - use a simple loop
for inner in data:
    for word in inner:
        print(f"{word:{max_len}}",end=" | ") # and format the length into it
    print()

to get

FirstFirst             | FirstSecond            | FirstThird             | 
SecondFirst            | SecondSecond           | SecondThird            | 
ThirdFirst             | ThirdSecond            | ThirdThird             | 
foo                    | verylongwordinsidehere | bar                    | 

This looks kinda ugly, would be better if you only got the max length per column imho:

# transpose the list, get the max of each column and store in as dict[column]=legnth
col_len = {i:max(map(len,inner)) for i,inner in enumerate(zip(*data))}

# print(col_len) # {0: 11, 1: 22, 2: 11}

# print using the column index from enumerate to lookup this columns lenght
for inner in data:
    for col,word in enumerate(inner):
        print(f"{word:{col_len[col]}}",end=" | ")
    print()

to get a column-width adjusted output:

FirstFirst  | FirstSecond            | FirstThird  | 
SecondFirst | SecondSecond           | SecondThird | 
ThirdFirst  | ThirdSecond            | ThirdThird  | 
foo         | verylongwordinsidehere | bar         | 

See


If you need to keep it shorter you can use ' | '.join() to print the lists:

# do not use list comp for printing side effect - use a simple loop
for inner in data:
    print( ' | '.join( (f"{word:{max_len}}" for word in inner)))

If you need to also print uneven lists, zip() won't do - you can get around that (research itertiols.zip_longest) but if you really need that, ask a new question with data for that after you tried something to do what you need it to do.

Patrick Artner
  • 50,409
  • 9
  • 43
  • 69
  • Thanks for the response! It's very useful and I think I'll implement it if I can't find anything better I've definitely got to learn transposing matrices and the zip builtin more clearly! Also, why do you recommend against using print in a comprehension? – YulkyTulky Apr 18 '20 at 07:56
  • @Yulk see [Is it Pythonic to use list comprehensions for just side effects?](https://stackoverflow.com/questions/5753597/is-it-pythonic-to-use-list-comprehensions-for-just-side-effects) You create a list that needs ressources (Memory, computing power, creating time, destroying time) wich all are bad - just to use it as looping-construct. We got loops in python that do what you need without allocating memory/time/space for a list that gets destroyed again. – Patrick Artner Apr 18 '20 at 07:57
2

Using comprehension:

# first map each string to formatted string with white space 
lists = [list(map(lambda item: f'{item:<20}', inner_list)) for inner_list in lists]
#then join them 
lists = [''.join(item) for item in lists]
print('\n'.join(lists))

The only issue here is that the 20 cannot be a variable, it needs to be hard-coded

Michael Hsi
  • 439
  • 2
  • 8
  • Thanks for the response! This works and is definitely cleaner than what I had. I still feel though that there is some hidden/super clever way to make this even cleaner. – YulkyTulky Apr 18 '20 at 07:45
  • @Michael - the length can be gotten dynamically, makes it slightly more complex. See my answer if you are interested. – Patrick Artner Apr 18 '20 at 07:48
  • @PatrickArtner Thx, it seems I need to research string formatting more. – Michael Hsi Apr 18 '20 at 17:51