3
\$\begingroup\$

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:

  1. 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]]
Peilonrayz
44.4k7 gold badges80 silver badges157 bronze badges
asked Mar 31, 2024 at 4:40
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

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:

  1. A core function interacting with Iterable[str], and
  2. A helper function which interacts with Any and returns a str.

Looking at the helper code:

  1. Not a fan of whitespace after function names def multiline_concat (*args).
  2. Not a fan of the names you're using around arg and would change to column.
  3. You don't need as e.
  4. 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.
  5. 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:

  1. Once you have col_lines you can col_width by getting the max size:

    col_width = max(len(item) for item in col_lines)
    
  2. The approach is clever and is simple if you are not aware of itertools.zip_longest and zip.

    With zip_longest you can iterate through all the columns getting the row each time. Simplifying the code, as output_rows and row_len are likely a little trick to work around.

    With zip we merge the row with the pre-computed widths, so we can item.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)
 )
answered Mar 31, 2024 at 16:58
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.