Problem Description
Printing the result of adding two numpy arrays together is very ugly.
Here we will use these numpy arrays as an example:
import numpy as np
arr_2d = np.array([[3, 3, 3],
[3, 3, 1],
[2, 3, 1]])
arr_1d = np.array([3,4,5])
Here are a few of the ugly ways to print the formula arr_2d + arr_1d
that are low-effort and ugly:
- Format strings
>>> print(f"{arr_1d}\n+\n{arr_2d}\n=\n{arr_1d + arr_2d}")
[3 4 5]
+
[[3 3 3]
[3 3 1]
[2 3 1]]
=
[[6 7 8]
[6 7 6]
[5 7 6]]
method 2 - printing as a list
>>> print([arr_1d,'+',arr_2d,'=',(arr_1d + arr_2d)])
[array([3, 4, 5]), '+', array([[3, 3, 3],
[3, 3, 1],
[2, 3, 1]]), '=', array([[6, 7, 8],
[6, 7, 6],
[5, 7, 6]])]
Solution
To solve this problem I used the following code:
def multiline_concat (*args):
"""
Concatenates arguments into a single str.
Supports multiline strs and arguments that become multiline strs when cast to str
"""
str_args = []
for arg_index, arg in enumerate(args):
try:
str_args.append(str(arg))
except TypeError as e:
raise TypeError(f"""ERROR: parameter passed to multiline_concat at index
{arg_index} of type {type(arg)} cannot be converted to a str""")
output_rows = []
row_len = 0
for col in str_args: # each value in *args is a column
col_lines = col.split('\n')
col_width = 0
for row_index, cell in enumerate(col_lines):
if len(output_rows) <= row_index: # Row doesn't exist yet so we add it.
output_rows.append("")
col_width = max(len(cell), col_width)
# padding the row so cells in this column start at the same index, then appending the cell
output_rows[row_index] = output_rows[row_index].ljust(row_len) + cell
row_len += col_width
return '\n'.join(output_rows)
we can now concatenate multi-line strings like this:
>>> print(multiline_concat(arr_1d, '+', arr_2d, '=',(arr_1d + arr_2d)))
[3 4 5] + [[3 3 3] = [[6 7 8]
[3 3 1] [6 7 6]
[2 3 1]] [5 7 6]]
1 Answer 1
A while ago I created the same function.
I was creating a number of formatting functions, and found working against str
or Any
to introduce a significant amount of boiler plate.
As such I'd recommend splitting the function into two:
- A core function interacting with
Iterable[str]
, and - A helper function which interacts with
Any
and returns astr
.
Looking at the helper code:
- Not a fan of whitespace after function names
def multiline_concat (*args)
. - Not a fan of the names you're using around
arg
and would change tocolumn
. - You don't need
as e
. - The multiline string is going to keep a ton of whitespace in the error message, you shouldn't use a multiline here. Instead use two normal strings with implicit concatenation.
- I would introduce a
sep
variable to allow specifying a custom separator.
from collections.abc import Iterable, Iterator
from typing import Any
def _multiline_concat(
*columns: Iterable[str],
sep: str = " ",
) -> Iterator[str]:
output_rows = []
row_len = 0
for col_lines in columns: # each value in *args is a column
col_width = 0
for row_index, cell in enumerate(col_lines):
if len(output_rows) <= row_index: # Row doesn't exist yet so we add it.
output_rows.append("")
col_width = max(len(cell), col_width)
# padding the row so cells in this column start at the same index, then appending the cell
output_rows[row_index] = output_rows[row_index].ljust(row_len) + cell
row_len += col_width
yield from output_rows
def multiline_concat(
*columns: Any,
sep: str = " ",
) -> str:
"""
Concatenates arguments into a single str.
Supports multiline strs and arguments that become multiline strs when cast to str
"""
columns_: list[list[str]] = []
for column in columns:
try:
columns_.append(str(column).splitlines())
except TypeError:
raise TypeError(
f"ERROR: parameter passed to multiline_concat at index"
f" {len(columns_)} of type {type(column)} cannot be converted to a str"
)
return "\n".join(_multiline_concat(*columns_, sep=sep))
Looking at the core function _multiline_concat
:
Once you have
col_lines
you cancol_width
by getting the max size:col_width = max(len(item) for item in col_lines)
The approach is clever and is simple if you are not aware of
itertools.zip_longest
andzip
.With
zip_longest
you can iterate through all the columns getting the row each time. Simplifying the code, asoutput_rows
androw_len
are likely a little trick to work around.With
zip
we merge the row with the pre-computed widths, so we canitem.ljust(width)
each value.
import itertools
def _multiline_concat(
*columns: Iterable[str],
sep: str = " ",
) -> Iterator[str]:
columns_ = [list(c) for c in columns]
columns_width = [
max(len(item) for item in column)
for column in columns_
]
for row in itertools.zip_longest(*columns_, fillvalue=""):
yield sep.join(
item.ljust(width)
for item, width in zip(row, columns_width)
)
Explore related questions
See similar questions with these tags.