Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 616 – 字符串方法移除前缀和后缀

作者:
Dennis Sweeney <sweeney.dennis650 at gmail.com>
赞助人:
Eric V. Smith <eric at trueblade.com>
状态:
最终版
类型:
标准跟踪
创建:
2020年3月19日
Python 版本:
3.9
历史记录:
2020年3月20日

目录

摘要

本提案建议向 Python 各种字符串对象的 API 添加两种新方法,removeprefix()removesuffix()。这些方法将分别移除字符串的前缀或后缀(如果存在),并将添加到 Unicode str 对象、二进制 bytesbytearray 对象以及 collections.UserString 中。

基本原理

在 Python-Ideas [2] [3]、Python-Dev [4] [5] [6] [7]、Bug Tracker 和 StackOverflow [8] 上,反复出现关于用户对现有 str.lstripstr.rstrip 方法的混淆问题。这些用户通常期望 removeprefixremovesuffix 的行为,但他们惊讶地发现 lstrip 的参数被解释为字符集,而不是子字符串。这一反复出现的问题证明了这些方法的实用性。新方法允许更清晰地将用户重定向到所需的行为。

作为这些方法实用性的另一个证明,Python-Ideas 上的几位用户 [2] 报告说,他们经常在代码中包含类似的功能以提高生产力。实现通常包含关于空字符串处理的细微错误,因此经过良好测试的内置方法将非常有用。

创建所需行为的现有解决方案是:要么按照下面规范中所述实现这些方法,要么使用正则表达式,例如表达式 re.sub('^' + re.escape(prefix), '', s),这不太容易发现,需要导入模块,并且会导致代码可读性降低。

规范

内置 str 类将获得两种新方法,当 type(self) is type(prefix) is type(suffix) is str 时,它们的行为如下

def removeprefix(self: str, prefix: str, /) -> str:
    if self.startswith(prefix):
        return self[len(prefix):]
    else:
        return self[:]

def removesuffix(self: str, suffix: str, /) -> str:
    # suffix='' should not call self[:-0].
    if suffix and self.endswith(suffix):
        return self[:-len(suffix)]
    else:
        return self[:]

当参数是 str 子类的实例时,这些方法的行为应该与首先将这些参数强制转换为基本 str 对象相同,并且返回值应始终为基本 str

具有相应语义的方法将添加到内置 bytesbytearray 对象中。如果 bbytesbytearray 对象,则 b.removeprefix()b.removesuffix() 将接受任何类字节对象作为参数。这两种方法也将添加到 collections.UserString 中,并具有类似的行为。

Python 标准库中的激励示例

以下示例演示了提议的方法如何使代码实现以下一项或多项功能

  1. 更健壮

    代码不会依赖用户计算字面量的长度。

  2. 性能更高

    代码不需要调用 Python 内置的 len 函数,也不需要调用更昂贵的 str.replace() 方法。

  3. 更具描述性

    与传统的字符串切片方法相比,这些方法提供了更高级别的 API 以提高代码可读性。

find_recursionlimit.py

  • 当前
    if test_func_name.startswith("test_"):
        print(test_func_name[5:])
    else:
        print(test_func_name)
    
  • 改进
    print(test_func_name.removeprefix("test_"))
    

deccheck.py

这是一个有趣的案例,因为作者选择在仅打算移除前缀的情况下使用 str.replace 方法。

  • 当前
    if funcname.startswith("context."):
        self.funcname = funcname.replace("context.", "")
        self.contextfunc = True
    else:
        self.funcname = funcname
        self.contextfunc = False
    
  • 改进
    if funcname.startswith("context."):
        self.funcname = funcname.removeprefix("context.")
        self.contextfunc = True
    else:
        self.funcname = funcname
        self.contextfunc = False
    
  • 可以说进一步改进
    self.contextfunc = funcname.startswith("context.")
    self.funcname = funcname.removeprefix("context.")
    

cookiejar.py

  • 当前
    def strip_quotes(text):
        if text.startswith('"'):
            text = text[1:]
        if text.endswith('"'):
            text = text[:-1]
        return text
    
  • 改进
    def strip_quotes(text):
        return text.removeprefix('"').removesuffix('"')
    

test_i18n.py

  • 当前
    creationDate = header['POT-Creation-Date']
    
    # peel off the escaped newline at the end of string
    if creationDate.endswith('\\n'):
        creationDate = creationDate[:-len('\\n')]
    
  • 改进
    creationDate = header['POT-Creation-Date'].removesuffix('\\n')
    

stdlib 中还有许多其他类似的示例。

被拒绝的想法

扩展 lstrip 和 rstrip API

因为 lstrip 以字符串作为其参数,所以可以将其视为接受长度为 1 的字符串的迭代器。因此,可以将 API 泛化为接受任何字符串迭代器,这些字符串将被依次移除作为前缀。虽然这种行为是一致的,但对于用户来说,为了常见的使用场景(单个前缀),必须调用 'foobar'.lstrip(('foo',)) 并不明显。

移除多个前缀副本

这是与前面提到的 lstrip/rstrip API 扩展一致的行为——重复应用该函数,直到参数不变。可以通过以下方式从提议的行为中获得这种行为

>>> s = 'Foo' * 100 + 'Bar'
>>> prefix = 'Foo'
>>> while s.startswith(prefix): s = s.removeprefix(prefix)
>>> s
'Bar'

未找到时引发异常

有人建议如果 not s.startswith(pre),则 s.removeprefix(pre) 应该引发异常。但是,这与其他字符串方法的行为和感觉不符。可以添加 required=False 关键字,但这违反了 KISS 原则。

接受前缀元组

将上面的 test_concurrent_futures.py 示例编写为 name.removesuffix(('Mixin', 'Tests', 'Test')) 可能很方便,因此有人建议新方法能够接受一个字符串元组作为参数,类似于 startswith() API。在元组中,只会移除第一个匹配的前缀或后缀。由于以下原因,此建议被拒绝

  • 这种行为可能会令人惊讶或视觉上令人困惑,尤其是在一个前缀为空或另一个前缀的子字符串时,例如 'FooBar'.removeprefix(('', 'Foo')) == 'FooBar''FooBar text'.removeprefix(('Foo', 'FooBar ')) == 'Bar text'
  • str.replace() 的 API 仅接受一对替换字符串,但经受住了时间的考验,拒绝在面对模棱两可的多重替换时进行猜测。
  • 将来可能会有一个引人注目的用例需要此功能,但在基本功能得到实际使用之前进行泛化很容易导致永久性错误。

替代方法名称

已经提出了几种替代方法名称。下面列出了一些,以及为什么应该拒绝它们而选择 removeprefix 的理由(相同的论点也适用于 removesuffix)。

  • ltrimtrimprefix 等。

    在其他语言(例如 JavaScript、Java、Go、PHP)中,“Trim” 的作用相当于 Python 中的 strip 方法。

  • lstrip(string=...)

    这将避免添加新方法,但对于不同的行为,最好有两个不同的方法,而不是一个具有选择行为的关键字参数的方法。

  • remove_prefix:

    字符串 API 的所有其他方法(例如 str.startswith())都使用 lowercase 而不是 lower_case_with_underscores

  • removeleftleftremovelremove

    “prefix” 的明确性更佳。

  • cutprefixdeleteprefixwithoutprefixdropprefix 等。

    其中许多可能是可以接受的,但“remove” 含义明确,并且与用英语描述“移除前缀”行为的方式相匹配。

  • stripprefix:

    用户可能会从记住“strip” 意味着使用字符集,而其他方法使用子字符串中获益,因此应避免在此处重复使用“strip”。

如何教授

partition()startswith()split() 字符串方法或 enumerate()zip() 内置函数的用途当中,一个共同的主题是,如果初学者发现自己手动索引或切片字符串,那么他们应该考虑是否存在更高层次的方法,这些方法能够更好地传达代码**应该做什么**,而不仅仅是代码**应该如何做**。提议的 removeprefix()removesuffix() 方法扩展了高级字符串“工具箱”,并进一步允许对手动切片进行这种怀疑态度。

用户混淆的主要机会将是 lstrip/rstripremoveprefix/removesuffix 的混淆。因此,强调(如文档中所述)这些方法之间的以下差异可能会有所帮助

  • (l/r)strip:
    • 参数被解释为字符集。
    • 这些字符从字符串的相应末尾重复删除。
  • remove(prefix/suffix):
    • 参数被解释为一个不间断的子字符串。
    • 最多只删除前缀/后缀的一个副本。

参考实现

参见 GitHub 上的拉取请求 [1]

主要修订历史

  • 版本 3:删除元组行为。
  • 版本 2:将名称更改为 removeprefix/removesuffix;添加了对元组作为参数的支持
  • 版本 1:使用 cutprefix/cutsuffix 的初始草稿

参考文献


来源:https://github.com/python/peps/blob/main/peps/pep-0616.rst

上次修改时间:2023-09-09 17:39:29 GMT