反转 Python 中的字符串

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

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

答案

怎么样:

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

这是扩展片语法。它通过执行[begin:end:step] - 通过保留 begin 和 end 并指定步骤 - 1 来反转字符串。

@Paolo 的s[::-1]是最快的; ''.join(reversed(s))是一个较慢的方法(可能更具可读性,但这值得商''.join(reversed(s))

为字符串实现反向函数的最佳方法是什么?

我对这个问题的经验是学术上的。但是,如果您是专业人士,正在寻找快速解答,请使用以-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 对象:

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]

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

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()形式。这将确保在各种实现中串联发生在线性时间内。

快速解答(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 可以提供很好的概述。

范例 02

### 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 这方面的原理。

也可以看看

仅当忽略 Unicode 修饰符 / 字形群集时,现有答案才是正确的。我将在稍后处理,但首先请看一些反转算法的速度:

在此处输入图片说明

list_comprehension  : 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

在此处输入图片说明

list_comprehension  : 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])

到目前为止也是最慢的:

list_comprehension  : 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 python

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 = [(list_comprehension, 'list_comprehension', 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 list_comprehension(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()

较少困惑的方法是:

string = 'happy'
print(string)

'快乐'

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

'yppah'

用英语 [-1 ::-1] 读为:

“从 - 1 开始,一直走,采取 - 1 的步骤”

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])

在不使用 reversed()或 [::-1] 的情况下反转 python 中的字符串

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

这也是一种有趣的方式:

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

使用支持. reverse()的 BYTERArray 的另一种 “异国情调” 方式

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

将产生:

'!siht esreveR'
def reverse(input):
    return reduce(lambda x,y : y+x, input)