Replace All Specific Characters in String using Python

Viewed 160

I have a problem with a function I am trying to implement that needs to replace some letters (in a given string), for some other characters, defined on a dictionary.

I have this dictionary:

chars = {
        'A': ['4', '@'],
        'E': ['3', '€', '&', '£'],
        'T': ['7'],
        'S': ['$', '§', '5'],
        'I': ['1', '!'],
        'O': ['0'],
        'B': ['8'],
        'C': ['<'],
        'L': ['1']
    }

I want to pass a string that when a character matches any key from the dictionary (case insensitive match), it tries to save all the possible combinations for that letter, using the values on the dictionary.

Here's what I currently have:

def type4 (words, v):
    chars = {
        'A': ['4', '@'],
        'E': ['3', '€', '&', '£'],
        'T': ['7'],
        'S': ['$', '§', '5'],
        'I': ['1', '!'],
        'O': ['0'],
        'B': ['8'],
        'C': ['<'],
        'L': ['1']
    }

    tmp = []
    string = []
    for w in words:                                 # each character in a word
        for key in chars.keys():                    # [A, E, T, S, I, O, B, C, L]
            for c in chars[key]:                    # [4, @, 3, €, &, £, ...]
                string = list(w)                    
                k = 0                               
                ya = ""
                for char in string:
                    if (char == key or char == key.lower()):
                        string[k] = c
                    else:
                        string[k] = char
                    k += 1
                if (not string == list(w)):
                    for i in string:
                        ya += i
                    print(ya, end="\n\n")

                tmp.append(ya)

    return tmp

In the current code, I have only replaced a character on the string. I need to change it so that it matches the output below,

The desired output, using the word sopa, should be like the following:

sopa
sop4
sop@
s0pa
s0p4
s0p@
$opa
$op4
$op@
$0pa
$0p4
$0p@
§opa
§op4
§op@
§0pa
§0p4
§0p@
5opa
5op4
5op@
50pa
50p4
50p@

I appreciate any help. Thank y'all in advance.

3 Answers

You were on the right track. Here is your modified function without recursion or itertools. This code also allows the function to take either a single word or a list of words.

def type4 (words):
    chars = {'A': ['4', '@'],'E': ['3', '€', '&', '£'],'T': ['7'],'S': ['$', '§', '5'],
         'I': ['1', '!'],'O': ['0'],'B': ['8'],'C': ['<'],'L': ['1']}
    tmp = []
    #This allows you to take a single word or list of words as argument
    if type(words) is str:
        tmp = [words]
    elif type(words) is list:
        tmp.extend(words)

    for word in tmp:
        for letter in word:
            if letter.upper() in chars:
                for new_letter in chars[letter.upper()]:
                    new_word = word.replace(letter, new_letter)
                    tmp.append(new_word)
    return set(tmp)

print(type4("sopa"))
#{'$op@', '$opa', 's0pa', '$0pa', '§opa', '5op@', '§0pa', '$0p4', '50pa', '$op4', 'sopa', '$0p@', 's0p4', '5op4', '§op@', '5opa', '50p4', '§0p@', '50p@', 'sop4', '§0p4', '§op4', 's0p@', 'sop@'}

You'll have an easier time doing this with itertools.product(). Here's how, explanation follows:

chars = ... # paste your code from above

def type4(word):
    positions = []  # will hold the list of character options for each letter    
    for letter in word:
        positions.append([letter] + chars.get(letter.upper(), []))
    
    return [''.join(combo) for combo in itertools.product(*positions)]

The for letter in words loop looks up each letter (in uppercase) in the chars dictionary and adds the list of chars for that letter, along with the letter into positions.

  • chars.get(letter.upper(), []) attempts to find the key (upper case letter) in chars. If it does, then returns it, otherwise, returns the default value provided, which is an empty list [] in this case.
    • So for 's' in 'sopa', it would be ['s'] + ['$', '§', '5'] = ['s', '$', '§', '5']
    • And for 'p', since it's not there, it would be ['p'] + [] = ['p']
    • See dict.get() for details.
    • Using dict.get replaces the need for if letter in chars.keys()
  • For clarity, a longer version of the same loop:
    positions = []
    for letter in word:
        if letter.upper() in chars.keys():
            options = chars[letter.upper()]
        else:
            options = []
        positions.append([letter] + options)
    
  • Or in just one line:
    positions = [[letter] + chars.get(letter.upper(), []) for letter in word]
    

After that loop, positions will be a list containing lists of characters, one list for each letter in the word provided:

[
    ['s', '$', '§', '5'],
    ['o', '0'], 
    ['p'], 
    ['a', '4', '@']
]

2nd loop/return statement:

  • itertools.product creates all the possible combinations from each list of options it has (see the docs), and provides that in a consistent sequence.
  • The *positions basically expands it into individual lists (one for each char, so 4 in the example)
  • Then product uses one item from each of those lists and provides them as a tuple, like ('§', '0', 'p', '4').
  • ''.join() turns the tuple (or list, or any iterable) into one string.
  • If that single return line is confusing, consider this longer form:
    tmp = []  # as per your code
    for combo in itertools.product(*positions):
        tmp.append(''.join(combo))
    return tmp
    

I think this problem fits really nicely within a recursion paradigm. I have comments in the below code, but as a short explanation:

This is one function that will iterate through a dictionary and slowly accumulate new permutations of a given word. Once the value.lower() has been found inside your current word, it will create a new word to find all permutations of. This works by calling generate_permutations_of_word each time you find a match.

# instantiate replacement_dictionary
replacement_dictionary = {
    'A': ['4', '@'],
    'E': ['3', '€', '&', '£'],
    'T': ['7'],
    'S': ['$', '§', '5'],
    'I': ['1', '!'],
    'O': ['0'],
    'B': ['8'],
    'C': ['<'],
    'L': ['1']
}

def generate_permutations_of_word(word, all_permutations=None):
    # if not yet instantiated, start with first word
    all_permutations = all_permutations or {word}
    
    # iterate over the replacement dictionary
    for replace_this, replace_with_value_list in replacement_dictionary.items():
        
        # iterate over the values in the "value" of the value_list
        for replace_with in replace_with_value_list:
            
            # if it matches
            if replace_this.lower() in word.lower():
                
                # new sub_word
                new_sub_word = word.replace(replace_this.lower(), replace_with.lower())
                
                # add this sub word to our list
                all_permutations.add(new_sub_word)
                
                # also find permutations of this subword
                all_permutations = generate_permutations_of_word(new_sub_word, all_permutations)
                
    return all_permutations

for x in generate_permutations_of_word('sopa'):
    print(x)


Related