PEP 616 – 접두사와 접미사를 제거하는 문자열 메서드
- Author:
- Dennis Sweeney <sweeney.dennis650 at gmail.com>
- Sponsor:
- Eric V. Smith <eric at trueblade.com>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 19-Mar-2020
- Python-Version:
- 3.9
- Post-History:
- 20-Mar-2020
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 문서는 Python의 여러 문자열 객체 API에 removeprefix()와 removesuffix()라는 두 개의 새 메서드를 추가하자는 제안입니다. 이 메서드들은 문자열에 접두사나 접미사가 있으면 각각 그것을 제거하며, 유니코드 str 객체, 바이너리 bytes 및 bytearray 객체, 그리고 collections.UserString에 추가됩니다.
근거
기존 str.lstrip 및 str.rstrip 메서드에 대한 사용자의 혼란과 관련하여 Python-Ideas [2] [3], Python-Dev [4] [5] [6] [7], 버그 트래커, StackOverflow [8]에서 문제가 반복적으로 제기되어 왔습니다. 이러한 사용자들은 보통 removeprefix와 removesuffix의 동작을 기대하지만, lstrip의 매개변수가 부분 문자열이 아니라 문자 집합으로 해석된다는 사실에 놀랍니다. 이 문제가 반복된다는 사실은 이러한 메서드가 유용하다는 증거입니다. 새 메서드를 사용하면 사용자를 원하는 동작으로 더 깔끔하게 안내할 수 있습니다.
이러한 메서드의 유용성을 보여 주는 또 다른 증언으로, Python-Ideas [2]의 여러 사용자가 생산성을 위해 비슷한 함수를 자신의 코드에 자주 넣는다고 밝혔습니다. 그러한 구현에는 빈 문자열 처리와 관련된 미묘한 실수가 자주 있었으므로, 충분히 테스트된 내장 메서드가 있으면 유용할 것입니다.
원하는 동작을 얻는 기존 방법은 아래 Specification에서처럼 메서드를 직접 구현하거나, 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')
표준 라이브러리에는 이와 같은 예가 그 밖에도 많이 있었습니다.
기각된 아이디어
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())는lower_case_with_underscores가 아니라lowercase를 사용합니다.removeleft,leftremove, 또는lremove:“prefix”라는 명시적인 표현이 더 선호됩니다.
cutprefix,deleteprefix,withoutprefix,dropprefix등:이 중 상당수는 받아들일 만했을 수도 있지만, “remove”는 모호하지 않으며 “접두사를 제거한다(remove the prefix)”는 동작을 영어로 설명하는 방식과도 일치합니다.
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를 사용한 초안
참고 문헌
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.