42

Say you have this string:

ABCDEFGH

And you want to reverse it so that it becomes:

GHEFCDAB

What would be the most efficient / pythonic solution? I've tried a few different things but they all look horrible...

Thanks in advance!

Update:

In case anyone's interested, this wasn't for homework. I had a script that was processing data from a network capture and returning it as a string of hex bytes. The problem was the data was still in network order. Due to the way the app was written, I didn't want to go back through and try to use say socket.htons, I just wanted to reverse the string.

Unfortunately my attempts seemed so hideous, I knew there must be a better way (a more pythonic solution) - hence my question here.

PeterM
  • 2,534
  • 6
  • 31
  • 38

14 Answers14

39

A concise way to do this is:

"".join(reversed([a[i:i+2] for i in range(0, len(a), 2)]))

This works by first breaking the string into pairs:

>>> [a[i:i+2] for i in range(0, len(a), 2)]
['AB', 'CD', 'EF', 'GH']

then reversing that, and finally concatenating the result back together.

Greg Hewgill
  • 951,095
  • 183
  • 1,149
  • 1,285
15

Lots of fun ways to do this

>>> s="ABCDEFGH"
>>> "".join(map(str.__add__, s[-2::-2] ,s[-1::-2]))
'GHEFCDAB'
John La Rooy
  • 295,403
  • 53
  • 369
  • 502
14

If anybody is interested, this is the timing for all* the answers.

EDIT (had got it wrong the first time):

import timeit
import struct

string = "ABCDEFGH"

# Expected resutlt => GHEFCDAB

def rev(a):
    new = ""

    for x in range(-1, -len(a), -2):
        new += a[x-1] + a[x]

    return new

def rev2(a):
    return "".join(reversed([a[i:i+2] for i in range(0, len(a), 2)]))

def rev3(a):
    return "".join(map(str.__add__, a[-2::-2] ,a[-1::-2]))

def rev4(a):
    return "".join(map("".join, reversed(zip(*[iter(a)]*2))))


def rev5(a):
    n = len(a) / 2
    fmt = '%dh' % n
    return struct.pack(fmt, *reversed(struct.unpack(fmt, a)))

def rev6(a):
    return "".join([a[x:x+2] for x in range(0,len(a),2)][::-1])


print "Greg Hewgill %f" %timeit.Timer("rev2(string)", "from __main__ import rev2, string").timeit(100000)
print "gnibbler %f" %timeit.Timer("rev3(string)", "from __main__ import rev3, string").timeit(100000)
print "gnibbler second %f" %timeit.Timer("rev4(string)", "from __main__ import rev4, string").timeit(100000)
print "Alok %f" %timeit.Timer("rev5(string)", "from __main__ import rev5, struct, string").timeit(100000)
print "elliot42 %f" %timeit.Timer("rev6(string)", "from __main__ import rev6, struct, string").timeit(100000)
print "me %f" %timeit.Timer("rev(string)", "from __main__ import rev, string").timeit(100000)

results for string = "ABCDEFGH":

Greg Hewgill 0.853000
gnibbler 0.428000
gnibbler second 0.707000
Alok 0.763000
elliot42 0.237000
me 0.200000

results for string = "ABCDEFGH"*5:

Greg Hewgill 2.246000
gnibbler 0.811000
gnibbler second 1.205000
Alok 0.972000
elliot42 0.594000
me 0.584000

results for string = "ABCDEFGH"*10:

Greg Hewgill 2.058000
gnibbler 1.178000
gnibbler second 1.926000
Alok 1.210000
elliot42 0.935000
me 1.082000

results for string = "ABCDEFGH"*100:

Greg Hewgill 9.762000
gnibbler 9.134000
gnibbler second 14.782000
Alok 5.775000
elliot42 7.351000
me 18.140000

*Sorry @Lacrymology could not make your's work!

Trufa
  • 39,971
  • 43
  • 126
  • 190
  • @elliot42: Thats because you were the fastest! :P – Trufa May 03 '11 at 04:09
  • You should use `string` without it being surrounded by single quotes (and import the name `string` in your setup statement as well). Otherwise you're checking the time for running on the constant string `'string'`. BTW, my method is the fastest for long strings (~50 characters or more on my computer). But I am not sure if I will write the function this way if it was production code :-). – Alok Singhal May 03 '11 at 04:23
  • Interesting how they scale. Did *1 and *100 tests with my own variant: def rev7(a): a=array.array('H',a) a.reverse() return a.tostring() Comparison to the fastest contenders: *1 Trufa 0.437, Yann 0.223. *100 Alok 5.19, Yann 2.38. – Yann Vernier May 03 '11 at 16:04
  • @YannVernier: Interesting indeed! I'll add as soon as I have some time you answer. – Trufa May 03 '11 at 16:48
  • @Yann: nice. You should post that as an answer. – Alok Singhal May 04 '11 at 02:04
  • @Alok: [he did?](http://stackoverflow.com/questions/5864271/reverse-a-string-in-python-two-characters-at-a-time-network-byte-order/5865754#5865754) – Trufa May 04 '11 at 02:29
  • oops, should have noticed earlier. – Alok Singhal May 04 '11 at 02:32
  • Just for fun, on my machine there was also a point (*3) where elliot42's version stood out; but it was still elliot42 0.844, Yann 0.274. – Yann Vernier May 04 '11 at 12:13
  • @YannVernier: For some reason I could not run it inside a function (haven't had much time though) I will look into it soon enough, sorry :) – Trufa May 04 '11 at 13:05
  • Feel free to use the version I linked in my answer. On the topic of needless optimization, `from array import array` could reduce namespace lookups even further - making it fastest for size 0 too. – Yann Vernier May 04 '11 at 15:10
10
>>> import array
>>> s="abcdef"
>>> a=array.array('H',s)
>>> a.byteswap()
>>> a.tostring()
'badcfe'

Finish up by using a.reverse() instead of a.byteswap() if you wanted to swap element order rather than byte order.

I took the liberty of editing Trufa's benchmark script a bit. The modified script generated a graphical plot showing approximately linear scaling for all functions.

Yann Vernier
  • 15,414
  • 2
  • 28
  • 26
  • Neat module find! Wasn't familiar with that one. – elliot42 May 05 '11 at 09:53
  • This answer appears to be the most Pythonic of the bunch, the fastest, and most consistent. It should have the green check! – Utkonos Jul 07 '19 at 15:37
  • One more thing: change `a.tostring()` to `a.tobytes()`. The method in your answer is deprecated according to the documentation: https://docs.python.org/3/library/array.html And, change the string to a byte string. Python 3 `array` doesn't work with `str` and Python 2 thankfully dies in a fire in about 6 months. – Utkonos Jul 07 '19 at 17:10
  • Python 3.2, which has the documentation note that `tostring` is deprecated (yet still works in 3.7), had been released for mere weeks when this answer was written. The `b` prefix for byte literals wasn't available in e.g. the Python version in CentOS at the time. Forward porting this isn't obscure (it's one of the most well known changes in Python 3) and isn't relevant to the core question; I don't feel it needs this edit nor vitriol. `tobytes` is not available in Python 2.7, so it would specifically break compatibility. – Yann Vernier Jul 09 '19 at 18:45
4

Here is a general form. The size of the grouping can easily be changed to a different number of characters at a time. The string length should be an exact multiple of the grouping size

>>> "".join(map("".join, reversed(zip(*[iter("ABCDEFGH")]*2))))
'GHEFCDAB'

(this is Python 2, it won't work in 3)

Janus Troelsen
  • 20,267
  • 14
  • 135
  • 196
John La Rooy
  • 295,403
  • 53
  • 369
  • 502
  • 1
    Love use of zip & map. I'm not familiar with this asterisk notation, could you explain pls. – laher May 03 '11 at 02:11
  • 1
    @amir75 http://docs.python.org/tutorial/controlflow.html#unpacking-argument-lists – John La Rooy May 03 '11 at 02:15
  • Could you explain this solution a little further? As in what is going on? – Senthil Kumaran May 03 '11 at 02:17
  • Curiously obscure code. From the inside out: an iterator is created for the data, then duplicated as both arguments to zip (*2 to duplicate, * to apply list as arguments); effectively causing zip to pair up adjacent items (in order, but I'm not sure that's meant to be guaranteed) as it's the same iterator. The list of pairs is then reversed, and both pairs and the list of pairs are joined into a single string. – Yann Vernier May 03 '11 at 17:35
  • @Yann, the `zip(*[iter()]*n)` for grouping an iterator into chunks appears numerous times on SO already – John La Rooy May 03 '11 at 21:07
4
st = "ABCDEFGH"
"".join([st[x:x+2] for x in range(0,len(st),2)][::-1])

EDIT: Curses, apparently 27 minutes slower than another poster. But I like the reverse slice notation better.

Some more information on the reverse slice here: "".join(reversed(val)) vs val[::-1]...which is pythonic?

Community
  • 1
  • 1
elliot42
  • 3,694
  • 3
  • 26
  • 27
  • Seems the most pythonic to me too, and fast. List slicing is very well optimised, although I rarely see it used in place of reverse() =/. – TyrantWave May 03 '11 at 09:18
3

You can use this, but don't tell anyone I wrote this code :-)

import struct

def pair_reverse(s):
    n = len(s) / 2
    fmt = '%dh' % n
    return struct.pack(fmt, *reversed(struct.unpack(fmt, s)))

pair_reverse('ABCDEFGH')
Alok Singhal
  • 93,253
  • 21
  • 125
  • 158
2

My friend Rob pointed out a beautiful recursive solution:

def f(s):
    return "" if not s else f(s[2:]) + s[:2]
Community
  • 1
  • 1
elliot42
  • 3,694
  • 3
  • 26
  • 27
0

and another gets on...

>>> rev = "ABCDEFGH"[::-1]
>>> ''.join([''.join(el) for el in zip(rev[1::2], rev[0::2])])
'GHEFCDAB'
dansalmo
  • 11,506
  • 5
  • 58
  • 53
0

I like this solution the most as it's the simplest and the neatest:

import struct
hex = struct.pack('<I', 0x41424344) #ABCD
print(hex) # BCDA
AK_
  • 1,879
  • 4
  • 21
  • 30
0

Here is a function based on the best, fastest, and most Pythonic answer above and in current Python 3 syntax:

def reverse_hex(hex_string):
    if isinstance(hex_string, str):
        input_is_string = True
        hex_string = hex_string.encode()
    a = array.array('H', hex_string)
    a.reverse()
    output = a.tobytes()
    if input_is_string:
        return output.decode()
    else:
        return output
Utkonos
  • 631
  • 6
  • 21
0

just a shot

st = "ABCDEFGH"
s = [st[2*n:2*n+1] for n in range(len(st)/2)]
return s[::-1].join('')

this assumes that len(st) is even, otherwise change that to range(len(st)/2+1) and I'm even sure there's a better way to do that split into twos.

If your python complains about s[::-1] you can use reversed(s)

Lacrymology
  • 2,269
  • 2
  • 20
  • 28
0

And yet another way:

a = "ABCDEFGH"
new = ""

for x in range(-1, -len(a), -2):
    new += a[x-1] + a[x]

print new
Trufa
  • 39,971
  • 43
  • 126
  • 190
0

This looks like homework. So here's a very unorthodox approach that you might find interesting:

>>> s = "ABCDEFGH"
>>> ''.join([s[::2][::-1][i]+s[::-2][i] for i in range(len(s[::2]))])
'GHEFCDAB'

Good Luck!

inspectorG4dget
  • 110,290
  • 27
  • 149
  • 241