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)的评论。

  • ltrim, trimprefix 等。

    “Trim” 在其他语言(例如 JavaScript、Java、Go、PHP)中执行的功能与 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

最后修改:2025-02-01 08:55:40 GMT