8

I have a string where a character ('@') needs to be replaced by characters from a list of one or more characters "in order" and "periodically". So for example I have

'ab@cde@@fghi@jk@lmno@@@p@qrs@tuvwxy@z'

and want

'ab1cde23fghi1jk2lmno312p3qrs1tuvwxy2z'

for replace_chars = ['1', '2', '3']

The problem is that in this example there are more @ in the string than I have replacers.

This is my try:

result = ''
replace_chars = ['1', '2', '3']
string = 'ab@cde@@fghi@jk@lmno@@@p@qrs@tuvwxy@z'

i = 0
for char in string:
    if char == '@':
        result += replace_chars[i]
        i += 1
    else:
        result += char

print(result)

but this only works of course if there are not more than three @ in the original string and otherwise I get IndexError.

Edit: Thanks for the answers!

FirimaElda
  • 125
  • 6
  • 2
    use `replace_chars[i % replace_chars.length]`. then you only do up to "modulo" of the lengh in index. e.g. with 3 chars, you do `1 % 3 -> 1`, `2 % 3 -> 2`, `3 % 3 -> 0`, `4 % 3 -> 1`, etc... – Marc B Mar 07 '16 at 21:29
  • 4
    Add `i %= 3` below `i+=1` – Bhargav Rao Mar 07 '16 at 21:30

4 Answers4

10

Your code could be fixed by adding the line i = i%len(replace_chars) as the last line of your if clause. This way you will be taking the remainder from the division of i by the length of your list of replacement characters.

The shorter solution is to use a generator that periodically spits out replacement characters.

>>> from itertools import cycle
>>> s = 'ab@cde@@fghi@jk@lmno@@@p@qrs@tuvwxy@z'
>>> replace_chars = ['1', '2', '3']
>>>
>>> replacer = cycle(replace_chars)
>>> ''.join([next(replacer) if c == '@' else c for c in s])
'ab1cde23fghi1jk2lmno312p3qrs1tuvwxy2z'

For each character c in your string s, we get the next replacement character from the replacer generator if the character is an '@', otherwise it just gives you the original character.

For an explanation why I used a list comprehension instead of a generator expression, read this.

Community
  • 1
  • 1
timgeb
  • 76,762
  • 20
  • 123
  • 145
6

Generators are fun.

def gen():
    replace_chars = ['1', '2', '3']
    while True:
        for rc in replace_chars:
            yield rc

with gen() as g:
    s = 'ab@cde@@fghi@jk@lmno@@@p@qrs@tuvwxy@z'
    s = ''.join(next(g) if c == '@' else c for c in s)

As PM 2Ring suggested, this is functionally the same as itertools.cycle. The difference is that itertools.cycle will hold an extra copy of the list in memory which may not be necessary.

itertools.cycle source:

def cycle(iterable):
    saved = []
    for element in iterable:
        yield element
        saved.append(element)
    while saved:
        for element in saved:
              yield element
Goodies
  • 4,439
  • 3
  • 31
  • 57
  • 2
    Your `gen` is essentially what `itertools.cycle` does. – PM 2Ring Mar 07 '16 at 21:43
  • 1
    Right. Typically, even if there is a useful tool in a standard package, I like to write the native equivalent simply for readability. The assumption being one does not know of the aforementioned tool. Often times, it merely gives you a better understanding. If you want to use `itertools.cycle`, definitely do it. – Goodies Mar 07 '16 at 21:45
  • @PM2Ring `cycle` also saves the elements from the given iterable, because the iterable might be an iterator that can be exhausted - but if `replace_chars` is guaranteed to be a list, @Goodies generator should work fine. – timgeb Mar 07 '16 at 21:45
  • 2
    Certainly! And I've done the same thing myself when I want to cycle over a list / tuple / string. Sometimes, it *is* worthwhile reinventing the cycle wheel. :) – PM 2Ring Mar 07 '16 at 21:50
  • `itertools.cycle` will hold an extra copy of the list in memory. <- A shallow copy, of course, i.e. it is just saving references to the values in another list – timgeb Mar 07 '16 at 21:58
1

You could also keep your index logic once you use modulo, using a list comp by using itertools.count to keep track of where you are:

from itertools import count

cn, ln = count(), len(replace_chars)

print("".join([replace_chars[next(cn) % ln] if c == "@" else c for c in string]))

ab1cde23fghi1jk2lmno312p3qrs1tuvwxy2z
Padraic Cunningham
  • 176,452
  • 29
  • 245
  • 321
0

I think it is better to not iterate character-by-character, especially for long string with lengthy parts without @.

from itertools import cycle, chain

s = 'ab@cde@@fghi@jk@lmno@@@p@qrs@tuvwxy@z'
replace_chars = ['1', '2', '3']
result = ''.join(chain.from_iterable(zip(s.split('@'), cycle(replace_chars))))[:-1]

I don't know how to efficiently kill last char [:-1].

Richard Erickson
  • 2,568
  • 8
  • 26
  • 39
Sav
  • 616
  • 3
  • 9