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
等。在其他语言(例如 JavaScript、Java、Go、PHP)中,“Trim” 的作用相当于 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
上次修改时间:2023-09-09 17:39:29 GMT