Skip to content
Advertisement

Python concatenate (multiple) strings to number

I have a python list ['a1', 'b1', 'a2', 'b2','a3', 'b3']. Set m=3 and I want get this list using loops, because here m=3 could be a larger number such as m=100.

Since we can have

m = 3

['a' + str(i) for i in np.arange(1,m+1)]
# ['a1', 'a2', 'a3']

['b' + str(i) for i in np.arange(1,m+1)]
# ['b1', 'b2', 'b3']

then I try to get ['a1', 'b1', 'a2', 'b2','a3', 'b3'] using

[ ['a','b'] + str(i) for i in np.arange(1,m+1)]

and have TypeError: can only concatenate list (not "str") to list

Then I try

[ np.array(['a','b']) + str(i) for i in np.arange(1,m+1)]

and I still get errors as UFuncTypeError: ufunc 'add' did not contain a loop with signature matching types (dtype('<U1'), dtype('<U1')) -> None.

How can I fix the problem? And even more, how to get something like ['a1', 'b1', 'c1', 'a2', 'b2','c2','a3', 'b3', 'c3'] through similar ways?

Advertisement

Answer

A simple combined list comprehension would work as pointed out in the @j1-lee’s answer (and later in other answers).

import string


def letter_number_loop(n, m):
    letters = string.ascii_letters[:n]
    numbers = range(1, m + 1)
    return [f"{letter}{number}" for number in numbers for letter in letters]

Similarly, one could use itertools.product(), as evidenced in Nick’s answer, to obtain substantially the same:

import itertools


def letter_number_it(n, m):
    letters = string.ascii_letters[:n]
    numbers = range(1, m + 1)
    return [
        f"{letter}{number}"
        for number, letter in itertools.product(numbers, letters)]

However, it is possible to write a NumPy-vectorized approach, making use of the fact that if the dtype is object, the operations do follow the Python semantics.

import numpy as np


def letter_number_np(n, m):
    letters = np.array(list(string.ascii_letters[:n]), dtype=object)
    numbers = np.array([f"{i}" for i in range(1, m + 1)], dtype=object)
    return (letters[None, :] + numbers[:, None]).ravel().tolist()

Note that the final numpy.ndarray.tolist() could be avoided if whatever will consume the output is capable of dealing with the NumPy array itself, thus saving some relatively small but definitely appreciable time.


Inspecting Output

The following do indicate that the functions are equivalent:

funcs = letter_number_loop, letter_number_it, letter_number_np

n, m = 2, 3
for func in funcs:
    print(f"{func.__name__!s:>32}  {func(n, m)}")
              letter_number_loop  ['a1', 'b1', 'a2', 'b2', 'a3', 'b3']
                letter_number_it  ['a1', 'b1', 'a2', 'b2', 'a3', 'b3']
                letter_number_np  ['a1', 'b1', 'a2', 'b2', 'a3', 'b3']

Benchmarks

For larger inputs, this is substantially faster, as evidenced by these benchmarks:

timings = {}
k = 16
for n in (2, 20):
    for k in range(1, 10):
        m = 2 ** k
        print(f"n = {n}, m = {m}")
        timings[n, m] = []
        base = funcs[0](n, m)
        for func in funcs:
            res = func(n, m)
            is_good = base == res
            timed = %timeit -r 64 -n 64 -q -o func(n, m)
            timing = timed.best * 1e6
            timings[n, m].append(timing if is_good else None)
            print(f"{func.__name__:>24}  {is_good}  {timing:10.3f} µs")

to be plotted with:

import matplotlib.pyplot as plt
import pandas as pd

n_s = (2, 20)
fig, axs = plt.subplots(1, len(n_s), figsize=(12, 4))
for i, n in enumerate(n_s):
    partial_timings = {k[1]: v for k, v in timings.items() if k[0] == n}
    df = pd.DataFrame(data=partial_timings, index=[func.__name__ for func in funcs]).transpose()
    df.plot(marker='o', xlabel='Input size / #', ylabel='Best timing / µs', ax=axs[i], title=f"n = {n}")

benchmarks

These show that the explicitly looped versions (letter_number_loop() and letter_number_it()) are somewhat comparable, while the NumPy-vectorized (letter_number_np()) fares much better relatively quickly for larger inputs, up to ~2x speed-up.

User contributions licensed under: CC BY-SA
4 People found this is helpful
Advertisement