如何在Python中反转字符串?

1641

Python的str对象中没有内置的reverse函数。实现此方法的最佳方法是什么?

如果提供非常简洁的答案,请说明其效率。例如,str对象是否转换为不同的对象等。

20个回答

3058

使用切片

>>> 'hello world'[::-1]
'dlrow olleh'

切片符号采用 [start:stop:step] 的形式,此时我们省略了 startstop 位置,因为我们想要整个字符串。同时使用 step = -1,意味着“反向每次向左移动一个字符”。


9
这个解决方案(以及大多数其他答案)即使应用于Python Unicode字符串,也不能处理Unicode的所有情况。例如,""[::-1] 的结果是 ""。正确的解决方案是 reversed_string = "".join(list(grapheme.graphemes(input_string))[::-1])。请参见下面的Martin的回答。 - amaurea
@amaurea 那个BG输出有什么问题? - undefined

309

@Paolo的s[::-1]是最快的方法;一个较慢但更易读(也许有争议)的方法是''.join(reversed(s))


29
这表示速度慢大约三倍。 - oneself
4
一个简短的评论可以更好地解释它的作用,比使用这个较慢的版本更为明确! - Tom Burrows
5
因为 join 必须构建列表才能获取其大小,所以速度较慢。 ''.join(list(reversed(s))) 可能会稍微快一些。 - Jean-François Fabre
1
你有关于为什么 [::-1] 是最快的的任何信息吗?我想深入了解。 - Tanner
4
@Tanner [::-1] 是最快的,因为它不调用任何外部函数,而是使用在Python中高度优化的切片操作。''.join(list(reversed(s))) 调用了三个函数。 - hd1

307

如何最好地实现字符串翻转函数?

我的经验是学术性的。但如果你是一名专业人士,寻找快速答案,请使用通过-1步进的切片:

>>> 'a string'[::-1]
'gnirts a'

更易读(但由于方法名称查找和join在给定迭代器时形成列表而导致速度较慢),使用str.join:

>>> ''.join(reversed('a string'))
'gnirts a'

或者为了可读性和可重用性,将切片放入一个函数中

def reversed_string(a_string):
    return a_string[::-1]

然后:

>>> reversed_string('a_string')
'gnirts_a'

更详细的解释

如果您对学术阐述感兴趣,请继续阅读。

Python的str对象中没有内置的反转函数。

下面是关于Python字符串的一些要点:

  1. 在Python中,字符串是不可变的。更改字符串不会修改原始字符串,而是创建一个新的字符串。

  2. 字符串可以被分片。使用分片符号或一个分片对象将一个字符串从某一点向前或向后按给定增量获取一个新字符串:

    string[subscript]
    

    下标可以通过在方括号内使用冒号创建一个切片:

        string[start:stop:step]
    

    如果想在大括号外创建一个切片,需要创建一个切片对象:

        slice_obj = slice(start, stop, step)
        string[slice_obj]
    

    易读的方法:

    虽然''.join(reversed('foo'))很易读,但它需要在另一个被调用的函数上调用一个字符串方法str.join,这可能相对较慢。让我们把它放在一个函数里 - 我们稍后再回来看:

    def reverse_string_readable_answer(string):
        return ''.join(reversed(string))
    

    最佳性能方案:

    更快的方法是使用反向切片:

    'foo'[::-1]
    

    但是,我们如何让不熟悉切片或原始作者意图的人更易读和理解?让我们在下标符号外创建一个切片对象,给它一个描述性名称,并将其传递给下标符号。

    start = stop = None
    step = -1
    reverse_slice = slice(start, stop, step)
    'foo'[reverse_slice]
    

    作为函数实现

    要将其实现为函数,我认为语义上足够清晰,只需使用一个描述性的名称:

    def reversed_string(a_string):
        return a_string[::-1]
    

    使用方法非常简单:

    reversed_string('foo')
    

    你的老师可能想要的:

    如果你有教练,那么他们可能希望你从一个空字符串开始,并从旧字符串中构建一个新字符串。你可以使用 while 循环和纯语法和字面量来做到这一点:

    def reverse_a_string_slowly(a_string):
        new_string = ''
        index = len(a_string)
        while index:
            index -= 1                    # index = index - 1
            new_string += a_string[index] # new_string = new_string + character
        return new_string
    

    理论上这是不好的,因为记住字符串是不可变的 - 所以每次在new_string上追加字符时,理论上都会创建一个新的字符串!然而,在某些情况下,CPython知道如何优化此过程,其中这个简单的案例就是其中之一。

    最佳做法

    理论上更好的方法是将子字符串收集到列表中,稍后再将它们连接:

    def reverse_a_string_more_slowly(a_string):
        new_strings = []
        index = len(a_string)
        while index:
            index -= 1                       
            new_strings.append(a_string[index])
        return ''.join(new_strings)
    

    然而,正如我们将在下面看到的CPython时间测试中,这实际上需要更长的时间,因为CPython可以优化字符串拼接。

    时间测试

    以下是时间测试结果:

    >>> a_string = 'amanaplanacanalpanama' * 10
    >>> min(timeit.repeat(lambda: reverse_string_readable_answer(a_string)))
    10.38789987564087
    >>> min(timeit.repeat(lambda: reversed_string(a_string)))
    0.6622700691223145
    >>> min(timeit.repeat(lambda: reverse_a_string_slowly(a_string)))
    25.756799936294556
    >>> min(timeit.repeat(lambda: reverse_a_string_more_slowly(a_string)))
    38.73570013046265
    

    CPython会对字符串拼接进行优化,但其他实现可能不会

    不要依赖CPython中针对形如 a += b 或 a = a + b 的语句的原地字符串拼接的高效实现。即使在CPython中,这种优化也是脆弱的(它只适用于某些类型),并且在不使用引用计数的实现中根本不存在。 在库的性能敏感部分应使用“''.join()”形式,这将确保拼接在各种实现中都以线性时间发生。


18
我喜欢这个答案,有关优化、可读性与优化的解释,以及对老师想要什么的提示。我不确定while循环和减少索引的最佳实践部分,尽管这可能不够易读:for i in range(len(a_string)-1, -1, -1):。最重要的是,我喜欢您选择的示例字符串是您永远不需要反转它的一种情况,并且如果您反转了它,就无法区分:) - Davos

53

这个答案有点长,包含3个部分:现有方案的基准测试为什么大多数答案是错误的我的解决方案

如果忽略Unicode修饰符/字形簇,则现有的答案才是正确的。我稍后会处理这个问题,但首先看一下一些反转算法的速度:

enter image description here

注意:我所谓的list_comprehension应该被称为slicing

slicing         : min:   0.6μs, mean:   0.6μs, max:    2.2μs
reverse_func    : min:   1.9μs, mean:   2.0μs, max:    7.9μs
reverse_reduce  : min:   5.7μs, mean:   5.9μs, max:   10.2μs
reverse_loop    : min:   3.0μs, mean:   3.1μs, max:    6.8μs

enter image description here

slicing         : min:   4.2μs, mean:   4.5μs, max:   31.7μs
reverse_func    : min:  75.4μs, mean:  76.6μs, max:  109.5μs
reverse_reduce  : min: 749.2μs, mean: 882.4μs, max: 2310.4μs
reverse_loop    : min: 469.7μs, mean: 577.2μs, max: 1227.6μs

您可以看到,切片操作的时间 (reversed = string[::-1]) 在所有情况下都远远低于其他方法(即使在修复我的拼写错误后)。

字符串反转

如果您真的想按通常意义上的方式反转一个字符串,那么它会变得更加复杂。例如,取以下字符串(向左指的棕色手指, 向上指的黄色手指)。这是两个字形,但有3个Unicode代码点。额外的一个是肤色修饰符

example = ""

但是,如果您使用任何给定的方法将其反转,您会得到棕色手指向上, 黄色手指向左。原因是“棕色”颜色修饰符仍然在中间,并应用于其前面的任何内容。所以我们有

  • U:手指向上
  • M:棕色修饰符
  • L:手指向左

original: LMU                    
reversed: UML (above solutions)  ☝
reversed: ULM (correct reversal) 

Unicode 字符簇 比修饰符代码点要复杂一些。幸运的是,有一个处理 字符 的库:

>>> import grapheme
>>> g = grapheme.graphemes("")
>>> list(g)
['', '']

因此正确的答案是

def reverse_graphemes(string):
    g = list(grapheme.graphemes(string))
    return ''.join(g[::-1])

这也是迄今为止最慢的:

slicing           : min:    0.5μs, mean:    0.5μs, max:    2.1μs
reverse_func      : min:   68.9μs, mean:   70.3μs, max:  111.4μs
reverse_reduce    : min:  742.7μs, mean:  810.1μs, max: 1821.9μs
reverse_loop      : min:  513.7μs, mean:  552.6μs, max: 1125.8μs
reverse_graphemes : min: 3882.4μs, mean: 4130.9μs, max: 6416.2μs

代码

#!/usr/bin/env python3

import numpy as np
import random
import timeit
from functools import reduce
random.seed(0)


def main():
    longstring = ''.join(random.choices("ABCDEFGHIJKLM", k=2000))
    functions = [(slicing, 'slicing', longstring),
                 (reverse_func, 'reverse_func', longstring),
                 (reverse_reduce, 'reverse_reduce', longstring),
                 (reverse_loop, 'reverse_loop', longstring)
                 ]
    duration_list = {}
    for func, name, params in functions:
        durations = timeit.repeat(lambda: func(params), repeat=100, number=3)
        duration_list[name] = list(np.array(durations) * 1000)
        print('{func:<20}: '
              'min: {min:5.1f}μs, mean: {mean:5.1f}μs, max: {max:6.1f}μs'
              .format(func=name,
                      min=min(durations) * 10**6,
                      mean=np.mean(durations) * 10**6,
                      max=max(durations) * 10**6,
                      ))
        create_boxplot('Reversing a string of length {}'.format(len(longstring)),
                       duration_list)


def slicing(string):
    return string[::-1]


def reverse_func(string):
    return ''.join(reversed(string))


def reverse_reduce(string):
    return reduce(lambda x, y: y + x, string)


def reverse_loop(string):
    reversed_str = ""
    for i in string:
        reversed_str = i + reversed_str
    return reversed_str


def create_boxplot(title, duration_list, showfliers=False):
    import seaborn as sns
    import matplotlib.pyplot as plt
    import operator
    plt.figure(num=None, figsize=(8, 4), dpi=300,
               facecolor='w', edgecolor='k')
    sns.set(style="whitegrid")
    sorted_keys, sorted_vals = zip(*sorted(duration_list.items(),
                                           key=operator.itemgetter(1)))
    flierprops = dict(markerfacecolor='0.75', markersize=1,
                      linestyle='none')
    ax = sns.boxplot(data=sorted_vals, width=.3, orient='h',
                     flierprops=flierprops,
                     showfliers=showfliers)
    ax.set(xlabel="Time in ms", ylabel="")
    plt.yticks(plt.yticks()[0], sorted_keys)
    ax.set_title(title)
    plt.tight_layout()
    plt.savefig("output-string.png")


if __name__ == '__main__':
    main()

6
感谢您展示了正确的音素感知字符串翻转方法。对于几乎任何实际目的,这里的其他答案都是错误的。虽然很遗憾你的得票率不足最受欢迎的答案的1%。 - amaurea
4
很遗憾,您的解决方案被淹没在大量基准测试信息中间有点不显眼。我建议将其放在更早和更突出的位置,也许明确展示其他人给出的简单解决方案的缺陷(您进行了描述但并未展示)。旗帜表情符号也是很好的例子。 - amaurea
2
很高兴看到不仅开头的答案值得阅读。顺便说一句:把统计数据(包括您的音素感知版本)放在最后会更好吧? - Wolf
@khelwood 你说得对,“切片”可能是更好的名称。不过,我现在有点懒得调整所有的图形了...也许一个“3.11”更新会很有趣。 - Martin Thoma
同意@amaurea的观点,基准测试不应比指出正确解决方案更突出。而将乌克兰颠倒以获得澳大利亚会成为一个更加简洁的例子。 - user3840170
显示剩余3条评论

47

快速回答(TL;DR)

例子

### example01 -------------------
mystring  =   'coup_ate_grouping'
backwards =   mystring[::-1]
print(backwards)

### ... or even ...
mystring  =   'coup_ate_grouping'[::-1]
print(mystring)

### result01 -------------------
'''
gnipuorg_eta_puoc
'''

详细答案

背景

本答案旨在回应@odigity的以下关注:

哇。一开始我对Paolo提出的解决方案感到恐惧,但是当我读到第一个评论时,我感到的恐惧超过了使用如此基本的加密方法的如此明亮的社区认为这是一个好主意。为什么不只是s.reverse()?

问题

  • 上下文
    • Python 2.x
    • Python 3.x
  • 场景:
    • 开发人员想要转换一个字符串
    • 转换是将所有字符的顺序颠倒

解决方案

陷阱

  • 开发人员可能期望类似于 string.reverse() 的功能。
  • 本地惯用的(也称为“pythonic”)解决方案可能对新手开发人员不易理解。
  • 开发人员可能会尝试实现自己的版本 string.reverse() 以避免使用切片符号。
  • 在某些情况下,切片符号的输出可能令人感到迷惑:
    • 例如,参见 example02
      • print 'coup_ate_grouping'[-4:] ## => 'ping'
      • 相比之下
      • print 'coup_ate_grouping'[-4:-1] ## => 'pin'
      • 相比之下
      • print 'coup_ate_grouping'[-1] ## => 'g'
    • [-1] 进行索引的不同结果可能会使一些开发人员感到困惑

原理

Python有一个特殊情况需要注意:字符串是一种可迭代类型。

不提供string.reverse()方法的理由之一是为了让Python开发人员利用这种特殊情况的优势。

简单来说,这意味着字符串中的每个字符都可以像其他编程语言中的数组元素一样轻松操作,作为顺序排列的元素的一部分。

要了解这是如何工作的,可以查看example02以获得一个很好的概述。

Example02

### example02 -------------------
## start (with positive integers)
print 'coup_ate_grouping'[0]  ## => 'c'
print 'coup_ate_grouping'[1]  ## => 'o' 
print 'coup_ate_grouping'[2]  ## => 'u' 

## start (with negative integers)
print 'coup_ate_grouping'[-1]  ## => 'g'
print 'coup_ate_grouping'[-2]  ## => 'n' 
print 'coup_ate_grouping'[-3]  ## => 'i' 

## start:end 
print 'coup_ate_grouping'[0:4]    ## => 'coup'    
print 'coup_ate_grouping'[4:8]    ## => '_ate'    
print 'coup_ate_grouping'[8:12]   ## => '_gro'    

## start:end 
print 'coup_ate_grouping'[-4:]    ## => 'ping' (counter-intuitive)
print 'coup_ate_grouping'[-4:-1]  ## => 'pin'
print 'coup_ate_grouping'[-4:-2]  ## => 'pi'
print 'coup_ate_grouping'[-4:-3]  ## => 'p'
print 'coup_ate_grouping'[-4:-4]  ## => ''
print 'coup_ate_grouping'[0:-1]   ## => 'coup_ate_groupin'
print 'coup_ate_grouping'[0:]     ## => 'coup_ate_grouping' (counter-intuitive)

## start:end:step (or start:end:stride)
print 'coup_ate_grouping'[-1::1]  ## => 'g'   
print 'coup_ate_grouping'[-1::-1] ## => 'gnipuorg_eta_puoc'

## combinations
print 'coup_ate_grouping'[-1::-1][-4:] ## => 'puoc'

结论

了解Python中切片符号的工作原理所需的认知负荷可能对于一些不愿意花费太多时间学习该语言的采用者和开发人员来说确实太多。

然而,一旦掌握了基本原则,这种方法比固定字符串操作方法更具优势。

对于那些持不同看法的人,有替代方案,例如lambda函数、迭代器或简单的一次性函数声明。

如果需要,开发人员可以实现自己的string.reverse()方法,但了解Python背后的原理是很好的。

另请参阅


15

1. 使用切片表示法

def rev_string(s): 
    return s[::-1]

2. 使用 reversed() 函数

def rev_string(s): 
    return ''.join(reversed(s))

3. 使用递归

def rev_string(s): 
    if len(s) == 1:
        return s

    return s[-1] + rev_string(s[:-1])

3
注意递归解决方案,如果字符串长度足够长,你会遇到RecursionError: maximum recursion depth exceeded while calling a Python object。例如:rev_string("abcdef"*1000) - Adam Parkin

13
一种更简单的理解方式是:
string = 'happy'
print(string)

'开心'

string_reversed = string[-1::-1]
print(string_reversed)

'yppah'

在英文中[-1::-1]的意思是:

“从-1开始,一直到最后,每次步长为-1”


2
-1 仍然不需要。 - Eric Duminil
2
@EricDuminil 如果你想要理解,在我看来是必要的。 - Kyoko Sasagava

10

如何在Python中不使用reversed()或[::-1]来翻转一个字符串

def reverse(test):
    n = len(test)
    x=""
    for i in range(n-1,-1,-1):
        x += test[i]
    return x

1
在Python 2中,如果您不需要列表,难道不应该使用xrange吗? - UnitasBrooks

5
这也是一种有趣的方法:
def reverse_words_1(s):
    rev = ''
    for i in range(len(s)):
        j = ~i  # equivalent to j = -(i + 1)
        rev += s[j]
    return rev

或类似的:
def reverse_words_2(s):
    rev = ''
    for i in reversed(range(len(s)):
        rev += s[i]
    return rev

另一种更加“异国情调”的方法是使用bytearray,它支持 .reverse() 方法。

b = bytearray('Reverse this!', 'UTF-8')
b.reverse()
b.decode('UTF-8')`

将会产生:

'!siht esreveR'

4
def reverse(input):
    return reduce(lambda x,y : y+x, input)

3
我点了赞,因为我喜欢这个lambda表达式。不幸的是,与上面列出的所有解决方案相比,它是最低效的解决方案(测试:Gist palindrome.py)。 - oski86
1
这是一个糟糕的解决方案,效率低下且没有必要。 - juanpa.arrivillaga

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接