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 对象、二进制 bytes 和 bytearray 对象,以及 collections.UserString。
基本原理
在 Python-Ideas [2] [3]、Python-Dev [4] [5] [6] [7]、Bug Tracker 和 StackOverflow [8] 上,反复出现用户对现有 str.lstrip 和 str.rstrip 方法感到困惑的问题。这些用户通常期望 removeprefix 和 removesuffix 的行为,但他们惊讶地发现 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。
具有相应语义的方法将添加到内置的 bytes 和 bytearray 对象。如果 b 是 bytes 或 bytearray 对象,则 b.removeprefix() 和 b.removesuffix() 将接受任何类字节对象作为参数。这两个方法也将添加到 collections.UserString,行为类似。
来自 Python 标准库的激励示例
下面的示例演示了提议的方法如何使代码具有以下一个或多个特点
- 减少脆弱性
代码将不再依赖用户计算字面量的长度。
- 提高性能
代码不需要调用 Python 内置的
len函数,也不需要调用开销更大的str.replace()方法。 - 更具描述性
这些方法提供了一个更高级别的 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.")
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。removeleft、leftremove或lremove更倾向于“prefix”的明确性。
cutprefix、deleteprefix、withoutprefix、dropprefix等。其中许多可能是可以接受的,但“remove”是明确的,并且与人们在英语中描述“移除前缀”行为的方式相符。
stripprefix:用户可能会受益于记住“strip”表示处理字符集,而其他方法处理子字符串,因此应避免在此处重复使用“strip”。
如何教授此内容
在 partition()、startswith() 和 split() 字符串方法或 enumerate() 或 zip() 内置函数的使用中,一个共同的主题是,如果初学者发现自己手动索引或切片字符串,那么他们应该考虑是否存在更高级别的方法,能够更好地传达代码应该做 什么,而不仅仅是代码应该 如何 做。提议的 removeprefix() 和 removesuffix() 方法扩展了高级字符串“工具箱”,并进一步允许这种对手动切片的怀疑。
用户困惑的主要机会将是 lstrip/rstrip 与 removeprefix/removesuffix 的混淆。因此,强调(如文档将强调的)这些方法之间的以下差异可能会有所帮助
(l/r)strip:- 参数被解释为字符集。
- 字符从字符串的相应末端重复移除。
remove(prefix/suffix):- 参数被解释为一个不间断的子字符串。
- 前缀/后缀最多只移除一个副本。
参考实现
请参阅 GitHub 上的拉取请求 [1]。
主要修订历史
- 版本3:移除元组行为。
- 版本2:将名称更改为
removeprefix/removesuffix;添加了对元组作为参数的支持 - 版本1:使用
cutprefix/cutsuffix的初始草案
参考资料
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0616.rst