There are several ways to solve this problem. The first approach that may come to mind is to iterate over the characters in the string one by one and check whether each character belongs to the set of characters to be replaced. If it does, you can use the string replace() method to substitute it, then continue processing the newly created string and repeat the process as needed. This approach is implemented in the first function of the module below.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
# The following function variants replace the characters in the specified text based on the # mapping between from_chars and to_chars. # Each character in from_chars is replaced with the character at the same position in to_chars. # Therefore, the two specified strings must be of the same length. def replace_chars1(text: str, from_chars: str, to_chars: str) -> str: result = text for c in text: if c in from_chars: result = result.replace(c, to_chars[from_chars.find(c)]) return result def replace_chars2(text: str, from_chars: str, to_chars: str) -> str: return ''.join(c if c not in from_chars else to_chars[from_chars.find(c)] for c in text) def replace_chars3(text: str, from_chars: str, to_chars: str) -> str: mapping = dict(zip(from_chars, to_chars)) return ''.join(mapping.get(c, c) for c in text) def replace_chars4(text: str, from_chars: str, to_chars: str) -> str: mapping = dict(zip(from_chars, to_chars)) result = text for from_char in mapping: if from_char in result: result = result.replace(from_char, mapping[from_char]) return result def replace_chars5(text, from_chars: str, to_chars: str): result = text for src, dst in zip(from_chars, to_chars): result = result.replace(src, dst) return result def replace_chars6(text: str, from_chars: str, to_chars: str) -> str: return text.translate(str.maketrans(from_chars, to_chars)) |
The drawback of the first solution is that it traverses the same string multiple times. Whenever replace() performs a substitution, it creates a new string, which involves copying the entire contents. As a result, this approach requires more memory allocations and copying than the other solutions.
The second and third functions avoid this problem by generating the sequence of substituted characters with a generator expression and then combining them into a single string using the join() method.
In the second function, the replacement is performed with a conditional expression. For each character in the input string, we first check whether it needs to be replaced. If not, the generator simply yields the original character. Otherwise, we locate the character’s position in the string of characters to be replaced using the find() method, then use the resulting index to retrieve the corresponding replacement character from the replacement string.
The third function uses a different approach. It first creates a dictionary that maps each character to be replaced to its replacement counterpart. The generator then retrieves the appropriate value using the dictionary’s get() method. This solution takes advantage of the fact that get() accepts a default value, which, in this case, is the current character itself. As a result, if the current character exists as a key in the dictionary, get() returns the corresponding replacement character. Otherwise, it returns the default value—the original character—so no explicit conditional check is required.
The disadvantage of the first three solutions is that they process the input string in Python. For long strings, the large number of Python-level iterations can significantly increase execution time. The fourth, fifth, and sixth functions address this issue.
The fourth function iterates over the characters that need to be replaced rather than over the characters of the input string. Since the number of replacement characters is typically small and independent of the input length, the traversal of the input string is delegated to the corresponding replace() calls rather than being performed in Python code. Moreover, replace() is invoked only if the current character to be replaced actually occurs in the string. This solution also takes advantage of the fact that replace() substitutes every occurrence of the target character in a single call.
The fifth function takes an even simpler approach. It iterates over the pairs of source and replacement characters and calls replace() once for each pair.
The sixth and final solution is the most concise. It uses the translate() and maketrans() methods.
The following listing contains the functional tests, the benchmarking code, and the corresponding results.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
# TESTS from timeit import timeit from random import choice import string # ----- Functional Test ----- txt = 'João levou pão, maçãs e limões para a reunião na estação central' from_chars = 'ÁÀÂÃáàâãÉÊéêÍíÓÔÕóôõÚúÇç' to_chars = 'AAAAaaaaEEeeIiOOOoooUuCc' for replace_chars in (replace_chars1, replace_chars2, replace_chars3, replace_chars4, replace_chars5, replace_chars6): assert replace_chars(txt, from_chars, to_chars) == "Joao levou pao, macas e limoes para a reuniao na estacao central" # The successful execution of the assert statement proves that each function returns the expected result. # ----- Execution Time Benchmark ----- def test_running_times(text: str, from_chars: str, to_chars: str, *tested_functions): """Measure and display the execution time of the specified character replacement functions. Parameters: text: the text to be processed. from_chars: the characters to be replaced. to_chars: the replacement characters corresponding to the characters in from_chars. tested_functions: the functions to be tested. """ func_running_times = { replace_chars.__name__: timeit(lambda: replace_chars(text, from_chars, to_chars), number=100) for replace_chars in tested_functions} for func_name, time in func_running_times.items(): print('{} -> {:.3f}'.format(func_name, time)) txt = ''.join(choice(from_chars + string.ascii_letters) for _ in range(500_000)) test_running_times(txt, from_chars, to_chars, replace_chars1, replace_chars2, replace_chars3, replace_chars4, replace_chars5, replace_chars6) # Output (Python 3.14): # replace_chars1 -> 3.401 # replace_chars2 -> 4.655 # replace_chars3 -> 4.080 # replace_chars4 -> 0.467 # replace_chars5 -> 0.447 # replace_chars6 -> 1.434 |
The execution time measurements confirm that the first three solutions, which process the input string character by character, are slower than the remaining three approaches. Among the latter, the functions based on repeated replace() calls consistently outperformed the implementation based on translate().
Since all of these three solutions rely on built-in string operations implemented in C by CPython, the observed differences are most likely due to differences in the internal optimizations of their implementations.
The execution time of built-in functions and methods may also vary between Python versions. This is because their internal implementations can change over time, meaning that identical Python code may exhibit different performance characteristics. A good example is the translate() method. Measuring the execution time of the replace_chars6() under Python 3.10, 3.11, 3.12, 3.13, and 3.14 shows that all versions prior to 3.14 are noticeably slower. Under Python 3.12 and 3.13, the execution time of translate() is comparable to that of the first three solutions.
By contrast, the functions based on replace() exhibit similar execution times across the tested Python versions. Therefore, in the environment used for these measurements, these solutions are the preferred solutions—particularly replace_chars4() and replace_chars5(), which proved to be the fastest.
A more detailed explanation of the replace(), translate(), and maketrans() methods, complete with practical examples, can be found in the e-book Python Knowledge Building Step by Step: From the Basics to The First Desktop Application. These methods are covered in the chapter „Public Methods of Built-in Types”, under the section devoted to string methods, together with the rest of Python’s string manipulation methods.