PEP 622 – 구조적 패턴 매칭
- Author:
- Brandt Bucher <brandt at python.org>, Daniel F Moisset <dfmoisset at gmail.com>, Tobias Kohn <kohnt at tobiaskohn.ch>, Ivan Levkivskyi <levkivskyi at gmail.com>, Guido van Rossum <guido at python.org>, Talin <viridia at gmail.com>
- BDFL-Delegate:
- Discussions-To:
- Python-Dev list
- Status:
- Superseded
- Type:
- Standards Track
- Created:
- 23-Jun-2020
- Python-Version:
- 3.10
- Post-History:
- 23-Jun-2020, 08-Jul-2020
- Superseded-By:
- 634
Table of Contents
- 초록
- 개요
- 근거와 목표
- 문법과 의미
- 런타임 명세
- 정적 검사기 명세
- 성능 고려 사항
- 하위 호환성
- 서드파티 도구에 미치는 영향
- 참조 구현
- 예제 코드
- 기각된 아이디어
- 이렇게 하지 마십시오, 패턴 매칭은 배우기 어렵습니다
- 이렇게 하지 말고 기존의 메서드 디스패치 메커니즘을 사용하십시오
- 대신 더 유연한 대입 대상을 허용하십시오
- 표현식으로 만드십시오
- 하드 키워드를 사용하십시오
- case 절에
case대신as나|사용하기 - 평평한 들여쓰기 방식 사용하기
- 상수 값 패턴의 대안
- 패턴에서 부동소수점 리터럴 금지
- 범위 매칭 패턴
- 매칭에 디스패치 딕셔너리 의미 체계 사용
- case 절에서
continue와break사용하기 - AND (
&) 패턴 - 부정 매치 패턴
- 런타임에 망라성 검사하기
- 패턴 변수에 대한 타입 어노테이션
- 클래스 패턴에서
*rest허용 - 상수 값 패턴에서
_.a금지 - 다른 토큰을 와일드카드로 사용
- OR 패턴에
|대신 다른 문법 사용 else절 추가하기
- 보류된 아이디어
- 감사의 글
- 버전 이력
- 참고 문헌
- 부록 A – 전체 문법
- Copyright
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 Scala, Erlang 및 기타 언어의 유사한 문법에서 영감을 받아 Python에 패턴 매칭 문을 추가할 것을 제안합니다.
패턴과 형태
패턴 문법은 Python의 기존 시퀀스 언패킹 문법(예: a, b = value)을 기반으로 합니다.
match 문은 하나의 값(대상)을 여러 가지 형태(패턴)와 차례로 비교하여 들어맞는 형태를 찾을 때까지 진행합니다. 각 패턴은 허용되는 값의 타입과 구조뿐만 아니라 그 내용을 캡처할 변수도 기술합니다.
패턴은 다음과 같은 형태를 지정할 수 있습니다:
- 앞서 언급한 것처럼 언패킹할 시퀀스
- 특정 키를 가진 매핑
- 지정된 클래스의 인스턴스로, (선택적으로) 특정 속성을 가지는 것
- 특정 값
- 와일드카드
패턴은 여러 가지 방식으로 조합할 수 있습니다.
문법
문법적으로 match 문은 다음을 포함합니다:
- 대상 표현식
- 하나 이상의
case절
각 case 절은 다음을 지정합니다:
- 패턴(매칭할 전체 형태)
- 선택적인 “가드”(패턴이 매칭될 경우 검사할 조건)
- 해당 case 절이 선택될 경우 실행할 코드 블록
동기
이 PEP의 나머지 부분에서는 다음을 다룹니다:
- 패턴 매칭이 Python에 좋은 추가 기능이라고 생각하는 이유를 설명합니다
- 설계상의 선택을 설명합니다
- 정확한 문법 및 런타임 명세를 제시합니다
- 정적 타입 검사기를 위한 지침을 제공합니다(
typing모듈에 대한 작은 추가 사항 하나도 포함합니다) - 저자 그룹 내부와 python-dev 커뮤니티 양쪽에서 이 제안을 폭넓게 논의하는 동안 제기된 주요 반론과 대안을 논의합니다
마지막으로, 커뮤니티가 현재 제안된 문법과 의미론에 대해 충분한 경험을 쌓은 뒤 향후 고려할 수 있는 몇 가지 확장 가능성을 논의합니다.
개요
패턴은 고유한 규칙과 특수한 경우를 가진 새로운 구문 범주입니다. 패턴은 입력(주어진 값)과 출력(캡처된 변수)을 새로운 방식으로 섞습니다. 패턴을 효과적으로 사용하기까지는 약간의 시간이 걸릴 수 있습니다. 저자들은 이 절에서 기본 개념을 간략하게 소개합니다. 이 절은 완전하거나 전적으로 정확하도록 의도된 것이 아니라는 점에 유의하십시오.
새로운 구문 구조인 패턴, 그리고 구조 분해
이 PEP에서는 패턴이라는 새로운 구문 구조를 도입합니다. 구문적으로 패턴은 표현식의 부분집합처럼 보입니다. 다음은 패턴의 예입니다:
[first, second, *rest]Point2d(x, 0){"name": "Bruce", "age": age}42
위 표현식들은 몇 가지 값을 매개변수로 받아 그 구성 요소들로 객체를 만드는 생성자를 이용한 객체 생성의 예처럼 보일 수 있습니다.
패턴으로 보면 위 패턴들은 생성의 역연산을 의미하며, 이를 구조 분해라고 부릅니다. 구조 분해는 대상 값을 받아 그 구성 요소를 추출합니다.
객체 생성과 구조 분해 사이의 구문적 유사성은 의도된 것입니다. 이는 또한 대입 대상(쓰기 컨텍스트)을 표현식(읽기 컨텍스트)처럼 보이게 하는 기존의 파이썬다운 컨텍스트 스타일을 따릅니다.
패턴 매칭은 결코 객체를 생성하지 않습니다. 이는 [a, b] = my_list가 새로운 [a, b] 리스트를 만들지도, a와 b의 값을 읽지도 않는 것과 같습니다.
매칭 과정
이 매칭 과정에서 패턴의 구조가 대상에 맞지 않을 수 있으며, 그러면 매칭은 실패합니다.
예를 들어, 패턴 Point2d(x, 0)을 대상 Point2d(3, 0)에 매칭하면 매칭이 성공합니다. 이 매칭은 또한 패턴의 자유 변수 x를 대상의 값 3에 바인딩합니다.
또 다른 예로, 대상이 [3, 0]이면 대상의 타입 list가 패턴의 Point2d가 아니므로 매칭은 실패합니다.
세 번째 예로, 대상이 Point2d(3, 7)이면 대상의 두 번째 좌표 7이 패턴의 0과 같지 않으므로 매칭은 실패합니다.
match 문은 하나의 대상을 case 절의 각 패턴에 매칭하려고 시도합니다. case 절의 패턴에 처음으로 매칭이 성공하면:
- 패턴의 변수들이 할당되고,
- 해당 블록이 실행됩니다.
각 case 절은 가드라고 하는 선택적 불리언 조건을 지정할 수도 있습니다.
match 문의 더 자세한 예를 살펴봅시다. match 문은 함수 안에서 3D 점의 생성을 정의하는 데 사용됩니다. 이 예에서 함수는 다음 중 어느 것이든 입력으로 받을 수 있습니다: 요소가 2개인 튜플, 요소가 3개인 튜플, 기존 Point2d 객체 또는 기존 Point3d 객체.:
def make_point_3d(pt):
match pt:
case (x, y):
return Point3d(x, y, 0)
case (x, y, z):
return Point3d(x, y, z)
case Point2d(x, y):
return Point3d(x, y, 0)
case Point3d(_, _, _):
return pt
case _:
raise TypeError("not a point we support")
패턴 매칭이 없다면 이 함수를 구현하는 데 여러 번의 isinstance() 검사, 한두 번의 len() 호출, 그리고 더 복잡한 제어 흐름이 필요할 것입니다. match 예제 버전과 match를 사용하지 않는 전통적인 Python 버전은 내부적으로 비슷한 코드로 변환됩니다. 패턴 매칭에 익숙하다면, match를 사용하는 이 함수를 읽는 사용자는 전통적인 방식보다 이 버전이 더 명확하다고 느낄 것입니다.
근거와 목표
Python 프로그램은 타입, 속성/키의 존재 여부, 또는 요소의 개수가 제각각인 데이터를 처리해야 하는 경우가 많습니다. 대표적인 예로는 AST와 같은 혼합 구조의 노드를 다루는 일, 여러 타입의 UI 이벤트를 처리하는 일, 구조화된 입력(구조화된 파일이나 네트워크 메시지 등)을 처리하는 일, 또는 타입과 매개변수 개수의 여러 조합을 받을 수 있는 함수의 인자를 “파싱”하는 일이 있습니다. 사실 고전적인 ‘방문자(visitor)’ 패턴이 이를 OOP 스타일로 구현한 예입니다 – 하지만 매칭을 사용하면 훨씬 덜 번거롭게 작성할 수 있습니다.
이를 처리하는 코드의 상당 부분은 len(), isinstance() 여러 번 호출과 인덱스/키/속성 접근을 포함하는, 중첩된 if/elif 문의 복잡한 연쇄로 이루어지는 경향이 있습니다. 사용자는 이러한 브랜치 안에서 필요한 구성 요소 값을 추출하기 위해 데이터를 더 분해해야 하는 경우도 있으며, 그 값은 여러 객체 깊이로 중첩되어 있을 수 있습니다.
다른 많은 언어에 존재하는 패턴 매칭은 이 문제에 대한 우아한 해결책을 제공합니다. 이러한 언어는 F#과 Haskell 같은 정적 컴파일 함수형 언어부터 Scala와 Rust 같은 혼합 패러다임 언어를 거쳐 Elixir와 Ruby 같은 동적 언어에 이르기까지 다양하며, JavaScript에서도 도입이 검토되고 있습니다. Python이 많은 기능을 다른 여러 언어에 빚지고 있듯이, 저희는 Python다운 패턴 매칭으로 가는 길을 안내해 준 이 언어들에 빚을 지고 있습니다. 많은 기본 문법 기능은 C에서, 예외는 Modula-3에서 물려받았고, 클래스는 C++에서 영감을 받았으며, 슬라이싱은 Icon에서, 정규 표현식은 Perl에서 왔고, 데코레이터는 Java 어노테이션과 닮았습니다.
이질적인 데이터를 다루는 일반적인 로직은 다음과 같이 요약할 수 있습니다:
- 데이터의 형태(타입과 구성 요소)에 대해 어떤 분석이 이루어집니다. 여기에는
isinstance()또는len()호출, 그리고/또는 특정 값이나 조건을 검사할 구성 요소를 (인덱싱이나 속성 접근을 통해) 추출하는 작업이 포함될 수 있습니다. - 형태가 예상과 같다면 구성 요소를 추가로 추출할 수 있으며, 추출된 값을 사용하여 어떤 연산을 수행합니다.
예를 들어 Django 웹 프레임워크의 이 코드 조각을 보십시오.:
if (
isinstance(value, (list, tuple)) and
len(value) > 1 and
isinstance(value[-1], (Promise, str))
):
*value, label = value
value = tuple(value)
else:
label = key.replace('_', ' ').title()
위쪽에서 value의 형태 분석이 이루어지고, 이어서 그 안에서 분해가 이루어지는 것을 볼 수 있습니다.
여기서 형태 분석에는 컨테이너와 그 구성 요소 중 하나의 타입을 모두 검사하는 것과 요소 개수에 대한 몇 가지 검사가 포함된다는 점에 유의하십시오. 형태가 일치하면 시퀀스를 분해해야 합니다. 이 PEP의 제안을 사용하면 이 코드를 다음과 같이 다시 작성할 수 있습니다:
match value:
case [*v, label := (Promise() | str())] if v:
value = tuple(v)
case _:
label = key.replace('_', ' ').title()
이 문법은 입력 데이터로 어떤 형식이 가능한지, 그리고 어떤 구성 요소가 어디에서 추출되는지를 훨씬 더 명시적으로 보여 줍니다. 리스트 언패킹과 비슷하면서도 타입 검사를 겸하는 패턴을 볼 수 있습니다: Promise() 패턴은 객체 생성이 아니라 Promise의 인스턴스인 모든 것을 나타냅니다. 패턴 연산자 |는 대안 패턴을 구분하며(정규 표현식이나 EBNF 문법과 크게 다르지 않습니다), _는 와일드카드입니다. (여기서 사용한 match 문법은 리스트와 튜플뿐만 아니라 사용자 정의 시퀀스도 받아들인다는 점에 유의하십시오.)
경우에 따라서는 정보를 추출하는 것보다 구조를 식별하는 것이 더 중요합니다. 파이썬 표준 라이브러리에서 가져온 다음 예를 보십시오:
def is_tuple(node):
if isinstance(node, Node) and node.children == [LParen(), RParen()]:
return True
return (isinstance(node, Node)
and len(node.children) == 3
and isinstance(node.children[0], Leaf)
and isinstance(node.children[1], Node)
and isinstance(node.children[2], Leaf)
and node.children[0].value == "("
and node.children[2].value == ")")
이 예는 별다른 추출 없이 데이터의 “형태”를 알아내는 예를 보여 줍니다. 이 코드는 읽기가 그리 쉽지 않으며, 이 코드가 매칭하려는 형태가 무엇인지 분명하게 드러나지 않습니다. 제안된 문법을 사용해 수정한 코드와 비교해 보십시오:
def is_tuple(node: Node) -> bool:
match node:
case Node(children=[LParen(), RParen()]):
return True
case Node(children=[Leaf(value="("), Node(), Leaf(value=")")]):
return True
case _:
return False
여기서 제안하는 코드는 Node와 다른 클래스의 정의를 전혀 수정하지 않아도 동작한다는 점에 유의하십시오. 위의 예제에서 보듯이, 이 제안은 시퀀스 언패킹뿐만 아니라 isinstance 검사(LParen() 또는 str()처럼), 객체 속성 들여다보기(예를 들어 Leaf(value="(")), 그리고 리터럴과의 비교도 지원합니다.
이 마지막 기능은 다른 언어에 있는 “switch” 문과 더 비슷해 보이는 일부 코드에 도움이 됩니다.:
match response.status:
case 200:
do_something(response.data) # OK
case 301 | 302:
retry(response.location) # Redirect
case 401:
retry(auth=get_credentials()) # Login first
case 426:
sleep(DELAY) # Server is swamped, try after a bit
retry()
case _:
raise RequestError("we couldn't get the data")
이 방식도 동작하기는 하지만, 반드시 이 제안이 초점을 맞추는 부분은 아니며, 새로운 문법은 구조 분해 시나리오를 가장 잘 지원하도록 설계되었습니다.
더 자세한 명세는 아래의 구문 절을 참고하십시오.
저희는 새로운 특수 __match_args__ 속성을 통해 객체의 구조 분해를 사용자 정의할 수 있도록 할 것을 제안합니다. 이 PEP의 일부로, 일반 API와 함께 일부 표준 라이브러리 클래스(네임드 튜플과 데이터클래스 포함)에 대한 그 구현을 명시합니다. 아래의 런타임 절을 참고하십시오.
마지막으로, 정적 타입 검사기와 이와 유사한 도구를 포괄적으로 지원하는 것을 목표로 합니다. 이를 위해 @typing.sealed 클래스 데코레이터를 도입할 것을 제안합니다. 이 데코레이터는 런타임에는 아무 동작도 하지 않지만, 이 클래스의 모든 서브클래스가 같은 모듈에 정의되어야 한다는 것을 정적 도구에 알려 줍니다. 이를 통해 효과적인 정적 완전성 검사가 가능해지며, 데이터클래스와 함께 algebraic data types에 대한 기본적인 지원을 제공하게 됩니다. 자세한 내용은 정적 검사기 절을 참고하십시오.
문법과 의미
패턴
패턴은 새로운 구문 구조로, 대입 대상을 느슨하게 일반화한 것으로 볼 수 있습니다. 패턴의 핵심 속성은 어떤 타입과 형태의 대상을 받아들이는지, 어떤 변수를 캡처하는지, 그리고 그 변수를 대상에서 어떻게 추출하는지입니다. 예를 들어, [a, b] 패턴은 정확히 2개의 요소를 가진 시퀀스에만 매치되며, 첫 번째 요소는 a로, 두 번째 요소는 b로 추출합니다.
이 PEP는 여러 종류의 패턴을 정의합니다. 가능한 패턴이 이것들만 있는 것은 분명 아니므로, 지금 유용하면서도 보수적인 기능의 부분 집합을 선택하기로 설계 결정을 내렸습니다. 이 기능이 더 널리 사용되면 나중에 더 많은 패턴을 추가할 수 있습니다. 자세한 내용은 Rejected Ideas와 Deferred Ideas 절을 참고하십시오.
여기에 나열한 패턴은 아래에서 더 자세히 설명하지만, 간결함을 위해 이 절에서 한데 모아 요약합니다:
- 리터럴 패턴은 구조 안의 상수 값을 걸러 내는 데 유용합니다. Python 리터럴처럼 생겼습니다(
True,False,None같은 일부 값 포함). 리터럴과 같은 객체에만 매치되며, 결코 바인딩하지 않습니다. - 캡처 패턴은
x처럼 생겼으며 동일한 대입 대상과 동등합니다: 항상 매치되며 주어진 (단순한) 이름의 변수를 바인딩합니다. - 와일드카드 패턴은 밑줄 하나인
_입니다. 항상 매치되지만 어떤 변수도 캡처하지 않습니다(이로써_의 다른 용도와 간섭하지 않으며 몇 가지 최적화가 가능해집니다). - 상수 값 패턴은 리터럴처럼 동작하지만 특정한 이름 있는 상수를 위한 것입니다. 캡처 패턴과 모호해질 수 있으므로 반드시 한정된(점으로 구분된) 이름이어야 한다는 점에 유의하십시오.
Color.RED처럼 생겼으며 해당 값과 같은 값에만 매치됩니다. 결코 바인딩하지 않습니다. - 시퀀스 패턴은
[a, *rest, b]와 같은 형태이며 리스트 언패킹과 비슷합니다. 중요한 차이점은 그 안에 중첩된 요소가 이름이나 시퀀스뿐만 아니라 어떤 종류의 패턴이든 될 수 있다는 것입니다. 이 패턴은 모든 하위 패턴도 매치되는 경우에 한해, 적절한 길이의 시퀀스에만 매치됩니다. 이 패턴은 하위 패턴이 만드는 모든 바인딩을 만듭니다. - 매핑 패턴은
{"user": u, "emails": [*es]}와 같은 형태입니다. 이 패턴은 주어진 키 집합을 최소한 포함하고, 모든 하위 패턴이 각각 대응하는 값에 매치되는 매핑에 매치됩니다. 이 패턴은 키에 대응하는 값과 매치하는 동안 하위 패턴이 바인딩하는 것을 모두 바인딩합니다. 나머지 항목을 캡처하기 위해 패턴 끝에**rest를 추가할 수 있습니다. - 클래스 패턴은 위와 비슷하지만 키 대신 속성을 매칭합니다. 이 패턴은
datetime.date(year=y, day=d)와 같은 형태입니다. 지정된 타입의 인스턴스 중에서 최소한 지정된 속성을 가지고 있고, 그 속성들이 대응하는 하위 패턴과 매칭되는 경우에 매칭됩니다. 지정된 속성의 값과 매칭할 때 하위 패턴이 바인딩하는 것을 모두 바인딩합니다. 선택적인 프로토콜을 통해 위치 인자를 매치할 수도 있습니다. - OR 패턴은
[*x] | {"elems": [*x]}와 같은 형태입니다. 이 패턴은 하위 패턴 중 하나라도 매치되면 매치됩니다. 이 패턴은 매치된 패턴 중 가장 왼쪽에 있는 패턴의 바인딩을 사용합니다. - 바다코끼리 패턴은
d := datetime(year=2020, month=m)와 같은 형태입니다. 이 패턴은 하위 패턴도 매치되는 경우에만 매치됩니다. 이 패턴은 하위 패턴 매치가 바인딩하는 것을 모두 바인딩하며, 이름이 지정된 변수를 객체 전체에 바인딩하기도 합니다.
match 문
제안하는 구문의 단순화된 대략적인 문법은 다음과 같습니다:
...
compound_statement:
| if_stmt
...
| match_stmt
match_stmt: "match" expression ':' NEWLINE INDENT case_block+ DEDENT
case_block: "case" pattern [guard] ':' block
guard: 'if' expression
pattern: walrus_pattern | or_pattern
walrus_pattern: NAME ':=' or_pattern
or_pattern: closed_pattern ('|' closed_pattern)*
closed_pattern:
| literal_pattern
| capture_pattern
| wildcard_pattern
| constant_pattern
| sequence_pattern
| mapping_pattern
| class_pattern
생략 없는 전체 문법은 부록 A를 참고하십시오. 이 절의 단순화된 문법은 독자의 이해를 돕기 위한 것이며, 완전한 명세가 아닙니다.
우리는 match 연산이 표현식이 아니라 문(statement)이어야 한다고 제안합니다. 많은 언어에서는 표현식이지만, 문으로 두는 것이 Python 문법의 전반적인 논리에 더 잘 맞습니다. 자세한 논의는 Rejected Ideas를 참고하십시오. 허용되는 패턴은 아래의 패턴 하위 절에서 자세히 설명합니다.
match와 case 키워드는 소프트 키워드로 제안됩니다. 따라서 각각 match 문과 case 블록의 시작 부분에서는 키워드로 인식되지만, 다른 곳에서는 변수나 인자 이름으로 사용할 수 있습니다.
제안하는 들여쓰기 구조는 다음과 같습니다:
match some_expression:
case pattern_1:
...
case pattern_2:
...
여기서 some_expression은 매치 대상이 되는 값을 나타내며, 이하에서는 이를 매치의 대상(subject)이라고 부릅니다.
매치 의미론
매치를 선택하는 방식으로 제안된 전체적인 의미 규칙은 처음으로 일치하는 패턴을 선택하고 그에 대응하는 스위트를 실행하는 것입니다. 나머지 패턴은 시도하지 않습니다. 일치하는 패턴이 없으면 이 문은 ‘통과(fall through)’하고, 실행은 다음 문에서 계속됩니다.
본질적으로 이는 if ... elif ... else 문을 연쇄한 것과 동등합니다. 이전에 제안되었던 switch 문과 달리, 미리 계산된 디스패치 딕셔너리 방식의 의미 규칙은 여기에 적용되지 않는다는 점에 유의하십시오.
default나 else 케이스는 없습니다. 대신 특수 와일드카드 _를 마지막 ‘모두 잡는(catch-all)’ 패턴으로 사용할 수 있습니다(capture_pattern 절 참조).
패턴 매칭이 성공하는 동안 만들어진 이름 바인딩은 실행된 스위트가 끝난 뒤에도 유지되며, match 문 이후에도 사용할 수 있습니다. 이는 for 루프와 with 문처럼 이름을 바인딩할 수 있는 다른 Python 문의 논리를 따른 것입니다. 예를 들면 다음과 같습니다.:
match shape:
case Point(x, y):
...
case Rectangle(x, y, _, _):
...
print(x, y) # This works
패턴 매칭이 실패하는 과정에서도 일부 하위 패턴은 성공할 수 있습니다. 예를 들어 값 [0, 1, 2]를 패턴 (0, x, 1)과 매칭할 때, 리스트 요소를 왼쪽에서 오른쪽으로 매칭한다면 하위 패턴 x는 성공할 수 있습니다. 구현은 이러한 부분 매치에 대해 바인딩을 지속시킬지 여부를 선택할 수 있습니다. match 문을 포함하는 사용자 코드는 실패한 매치에서 바인딩이 만들어진다는 데 의존해서는 안 되며, 동시에 실패한 매치로 인해 변수가 변경되지 않는다고 가정해서도 안 됩니다. 동작의 이 부분은 서로 다른 구현이 최적화를 추가할 수 있도록, 그리고 이 기능의 확장성을 제한할 수 있는 의미상의 제약이 도입되지 않도록 의도적으로 명시하지 않은 채로 두었습니다.
아래의 일부 패턴 유형은 바인딩이 언제 만들어지는지에 대해 더 구체적인 규칙을 정의한다는 점에 유의하십시오.
허용되는 패턴
제안하는 문법을 단계적으로 소개합니다. 여기서는 주요 구성 요소부터 시작합니다. 다음 패턴을 지원합니다:
리터럴 패턴
단순화된 문법:
literal_pattern:
| number
| string
| 'None'
| 'True'
| 'False'
리터럴 패턴은 문자열, 숫자, 불리언 리터럴(True 또는 False), 또는 None과 같은 단순한 리터럴로 구성됩니다.:
match number:
case 0:
print("Nothing")
case 1:
print("Just one")
case 2:
print("A couple")
case -1:
print("One less than nothing")
case 1-1j:
print("Good luck with that...")
리터럴 패턴은 오른쪽에 리터럴을 둔 동등 비교를 사용하므로, 위의 예에서는 number == 0이 평가되고, 이어서 필요하면 number == 1 등이 평가됩니다. 기술적으로 음수는 단항 마이너스를 사용하여 표현되지만, 패턴 매칭에서는 리터럴로 간주된다는 점에 유의하십시오. 단항 플러스는 허용되지 않습니다. 이항 플러스와 마이너스는 1+1j와 같이 실수와 허수를 결합하여 복소수를 만드는 경우에만 허용됩니다.
동등 비교(__eq__)를 사용하고 불리언과 정수 0, 1 사이에 동치 관계가 있으므로, 다음 두 코드 사이에는 실질적인 차이가 없다는 점에 유의하십시오.:
case True:
...
case 1:
...
삼중 따옴표 문자열을 지원합니다. 원시 문자열과 바이트 문자열을 지원합니다. F-문자열은 허용되지 않습니다(일반적으로 F-문자열은 실제로는 리터럴이 아니기 때문입니다).
캡처 패턴
단순화된 문법:
capture_pattern: NAME
캡처 패턴은 매치된 표현식의 대입 대상 역할을 합니다:
match greeting:
case "":
print("Hello!")
case name:
print(f"Hi {name}!")
단일 이름만 허용됩니다(점으로 구분된 이름은 상수 값 패턴입니다). 캡처 패턴은 항상 성공합니다. 스코프 안에 캡처 패턴이 나타나면 그 이름은 해당 스코프의 지역 이름이 됩니다. 예를 들어, "" case 절이 선택되었다면 위 코드 조각 이후에 name을 사용할 때 NameError가 아니라 UnboundLocalError가 발생할 수 있습니다:
match greeting:
case "":
print("Hello!")
case name:
print(f"Hi {name}!")
if name == "Santa": # <-- might raise UnboundLocalError
... # but works fine if greeting was not empty
각 case 절에 대해 매칭하는 동안 하나의 이름은 최대 한 번만 바인딩될 수 있으므로, 이름이 같은 캡처 패턴이 두 개 있으면 오류입니다:
match data:
case [x, x]: # Error!
...
참고: Guards를 사용하면 서로 같은 항목으로 이루어진 컬렉션에 대해서도 여전히 매칭할 수 있습니다. 또한 [x, y] | Point(x, y)는 두 대안이 동시에 매치되는 일이 없으므로 올바른 패턴입니다.
단일 밑줄(_)은 NAME으로 간주되지 않으며 Wildcard Pattern으로 특별하게 취급됩니다.
참고로 None, False, True는 이름이 아니라 리터럴을 나타내는 키워드입니다.
와일드카드 패턴
단순화된 문법:
wildcard_pattern: "_"
단일 밑줄(_) 이름은 항상 매치되지만 결코 바인딩하지 않는 특별한 종류의 패턴입니다:
match data:
case [_, _]:
print("Some pair")
print(_) # Error!
바인딩이 이루어지지 않으므로 캡처 패턴과 달리 원하는 만큼 여러 번 사용할 수 있습니다.
상수 값 패턴
단순화된 문법:
constant_pattern: NAME ('.' NAME)+
이 패턴은 상수와 enum 값에 대해 매칭할 때 사용됩니다. 패턴 안의 모든 점 표기 이름은 일반적인 Python 이름 결정 규칙에 따라 조회되며, 그 값은 매치 대상과의 동등 비교에 사용됩니다(리터럴의 경우와 같습니다):
from enum import Enum
class Sides(str, Enum):
SPAM = "Spam"
EGGS = "eggs"
...
match entree[-1]:
case Sides.SPAM: # Compares entree[-1] == Sides.SPAM.
response = "Have you got anything without Spam?"
case side: # Assigns side = entree[-1].
response = f"Well, could I have their Spam instead of the {side} then?"
한정되지 않은 이름을 상수 값 패턴으로 사용할 방법은 없다는 점에 유의하십시오(그런 이름은 항상 캡처될 변수를 나타냅니다). 상수 값 패턴을 위해 검토되었던 다른 문법적 대안은 Rejected Ideas를 참조하십시오.
시퀀스 패턴
단순화된 문법:
sequence_pattern:
| '[' [values_pattern] ']'
| '(' [value_pattern ',' [values pattern]] ')'
values_pattern: ','.value_pattern+ ','?
value_pattern: '*' capture_pattern | pattern
시퀀스 패턴은 언패킹 대입과 같은 의미 체계를 따릅니다. 언패킹 대입과 마찬가지로 튜플 형태와 리스트 형태의 문법을 모두 사용할 수 있으며, 의미는 동일합니다. 각 요소는 임의의 패턴이 될 수 있으며, 남은 항목을 모두 잡아내는 *name 패턴도 최대 하나까지 올 수 있습니다:
match collection:
case 1, [x, *others]:
print("Got 1 and a nested sequence")
case (1, x):
print(f"Got 1 and {x}")
시퀀스 패턴에 매치되려면 대상은 collections.abc.Sequence의 인스턴스여야 하며, 어떤 종류의 문자열(str, bytes, bytearray)이어서도 안 됩니다. 이터레이터여서도 안 됩니다. 특정 컬렉션 클래스에 대한 매칭은 아래의 클래스 패턴을 참조하십시오.
_ 와일드카드에 별표를 붙여 길이가 다양한 시퀀스에 매치할 수 있습니다. 예를 들면 다음과 같습니다:
[*_]는 길이와 상관없이 모든 시퀀스에 매치됩니다.(_, _, *_)는 길이가 2 이상인 모든 시퀀스에 매치됩니다.["a", *_, "z"]는"a"로 시작하고"z"로 끝나는 길이 2 이상의 모든 시퀀스와 매치됩니다.
매핑 패턴
단순화된 문법:
mapping_pattern: '{' [items_pattern] '}'
items_pattern: ','.key_value_pattern+ ','?
key_value_pattern:
| (literal_pattern | constant_pattern) ':' or_pattern
| '**' capture_pattern
매핑 패턴은 이터러블 언패킹을 매핑으로 일반화한 것입니다. 문법은 딕셔너리 디스플레이와 비슷하지만, 각 키와 값이 "{" (pattern ":" pattern)+ "}" 형태의 패턴이라는 점이 다릅니다. 나머지 항목을 추출하기 위한 **rest 패턴도 허용됩니다. 키 위치에는 리터럴 패턴과 상수 값 패턴만 허용됩니다:
import constants
match config:
case {"route": route}:
process_route(route)
case {constants.DEFAULT_PORT: sub_config, **rest}:
process_config(sub_config, rest)
대상은 collections.abc.Mapping의 인스턴스여야 합니다. **rest가 없더라도 대상에 있는 추가 키는 무시됩니다. 이 점은 추가 항목이 있으면 매치가 실패하는 시퀀스 패턴과 다릅니다. 하지만 매핑은 실제로 시퀀스와 다릅니다. 매핑은 자연스러운 구조적 서브타이핑 동작을 가지므로, 추가 키가 있는 딕셔너리를 어딘가에 전달해도 대개 그대로 동작합니다.
이러한 이유로 매핑 패턴에서 **_는 유효하지 않습니다. 이 패턴은 항상 아무 효과가 없으므로 제거해도 아무런 영향이 없기 때문입니다.
매치되는 키-값 쌍은 매핑에 이미 존재해야 하며, __missing__이나 __getitem__에 의해 즉석에서 생성되어서는 안 됩니다. 예를 들어, collections.defaultdict 인스턴스는 match 블록에 진입했을 때 이미 존재하던 키를 가진 패턴하고만 매치됩니다.
클래스 패턴
단순화된 문법:
class_pattern:
| name_or_attr '(' ')'
| name_or_attr '(' ','.pattern+ ','? ')'
| name_or_attr '(' ','.keyword_pattern+ ','? ')'
| name_or_attr '(' ','.pattern+ ',' ','.keyword_pattern+ ','? ')'
keyword_pattern: NAME '=' or_pattern
클래스 패턴은 임의의 객체를 구조 분해하는 기능을 제공합니다. 객체 속성에 대해 매치하는 방법은 두 가지입니다. Point(1, 2)처럼 위치로 매치하는 방법과 Point(x=1, y=2)처럼 이름으로 매치하는 방법입니다. 이 두 방법은 함께 사용할 수 있지만, 위치 매치가 이름 매치 뒤에 올 수는 없습니다. 클래스 패턴의 각 항목은 임의의 패턴일 수 있습니다. 간단한 예제:
match shape:
case Point(x, y):
...
case Rectangle(x0, y0, x1, y1, painted=True):
...
매치의 성공 여부는 isinstance 호출과 동등한 방식으로 결정됩니다. 대상(예제에서는 shape)이 지정된 클래스(Point 또는 Rectangle)의 인스턴스가 아니면 매치는 실패합니다. 그렇지 않으면 매치가 계속 진행됩니다(자세한 내용은 런타임 섹션을 참조하십시오).
지정된 클래스는 type을 상속해야 합니다. 이 클래스는 단일 이름이거나 점으로 구분된 이름(예: some_mod.SomeClass 또는 mod.pkg.Class)일 수 있습니다. 맨 앞의 이름은 _이면 안 되므로, 예를 들어 _(...)와 _.C(...)는 유효하지 않습니다. 매치된 객체에 foo 속성이 있는지 확인하려면 object(foo=_)를 사용하십시오.
기본적으로 사용자 정의 클래스의 하위 패턴은 키워드로만 매치할 수 있습니다. 위치 하위 패턴을 지원하려면 사용자 정의 __match_args__ 속성이 필요합니다. 런타임은 모든 인스턴스 검사와 속성 조회를 적절히 연결하여 임의로 중첩된 패턴에 대한 매치를 허용합니다.
여러 패턴 결합하기(OR 패턴)
여러 대안 패턴을 |를 사용하여 하나로 결합할 수 있습니다. 이는 대안 중 적어도 하나가 매치되면 전체 패턴이 매치된다는 뜻입니다. 대안은 왼쪽에서 오른쪽으로 시도되며 단락(short-circuit) 특성이 있으므로, 하나가 매치되면 이후 패턴은 시도되지 않습니다. 예시:
match something:
case 0 | 1 | 2:
print("Small number")
case [] | [_]:
print("A short sequence")
case str() | bytes():
print("Something string-like")
case _:
print("Something else")
각 대안이 (_를 제외하고) 같은 변수 집합을 바인딩하기만 하면, 대안은 변수를 바인딩할 수 있습니다. 예를 들면 다음과 같습니다.:
match something:
case 1 | x: # Error!
...
case x | 1: # Error!
...
case one := [1] | two := [2]: # Error!
...
case Foo(arg=x) | Bar(arg=x): # Valid, both arms bind 'x'
...
case [x] | x: # Valid, both arms bind 'x'
...
가드
각 최상위 패턴 뒤에는 if expression 형태의 가드가 올 수 있습니다. case 절은 패턴이 매치되고 가드가 참 값으로 평가될 때 성공합니다. 예를 들면 다음과 같습니다.:
match input:
case [x, y] if x > MAX_INT and y > MAX_INT:
print("Got a pair of large numbers")
case x if x > MAX_INT:
print("Got a large number")
case [x, y] if x == y:
print("Got equal items")
case _:
print("Not an outstanding input")
가드를 평가하는 중에 예외가 발생하면, 그 예외는 case 절을 실패시키는 대신 바깥으로 전파됩니다. 패턴에 나타나는 이름은 가드가 성공하기 전에 바인딩됩니다. 따라서 다음 코드는 동작합니다.:
values = [0]
match values:
case [x] if x:
... # This is not executed
case _:
...
print(x) # This will print "0"
중첩된 패턴에는 가드를 사용할 수 없으므로, [x if x > 0]은 SyntaxError이고 1 | 2 if 3 | 4는 (1 | 2) if (3 | 4)로 파싱된다는 점에 유의하십시오.
바다코끼리 패턴
하위 패턴을 매치하는 동시에 대응하는 값을 이름에 바인딩하는 것이 유용한 경우가 많습니다. 예를 들어, 더 효율적인 매치를 작성하거나 단순히 반복을 피하는 데 유용할 수 있습니다. 이러한 경우를 단순화하기 위해, (바다코끼리 패턴 자체를 제외한) 모든 패턴 앞에 이름과 바다코끼리 연산자(:=)를 붙일 수 있습니다. 예를 들면 다음과 같습니다.:
match get_shape():
case Line(start := Point(x, y), end) if start == end:
print(f"Zero length line at {x}, {y}")
바다코끼리 연산자 왼쪽의 이름은 가드, match 스위트 또는 match 문 이후에서 사용할 수 있습니다. 하지만 이름은 하위 패턴이 성공한 경우에만 바인딩됩니다. 또 다른 예시:
match group_shapes():
case [], [point := Point(x, y), *other]:
print(f"Got {point} in the second group")
process_coordinates(x, y)
...
엄밀히 말하면 이러한 예시 대부분은 가드나 중첩된 match 문 또는 둘 다를 사용하여 다시 작성할 수 있지만, 그렇게 하면 가독성이 떨어지거나 덜 효율적인 코드가 생성되거나 둘 다일 수 있습니다. 본질적으로, PEP 572의 인자 대부분이 여기에도 똑같이 적용됩니다.
와일드카드 _는 여기서 유효한 이름이 아닙니다.
런타임 명세
매치 프로토콜
객체가 주어진 클래스 패턴과 매칭되는지 판단하고 대응하는 속성을 추출하는 데에는 isinstance 호출에 해당하는 것이 사용됩니다. 다른 매칭 의미 체계(예: 덕 타이핑)가 필요한 클래스는 __instancecheck__(기존의 메타클래스 훅)를 정의하거나 typing.Protocol을 사용하여 그렇게 할 수 있습니다.
절차는 다음과 같습니다:
Class(<sub-patterns>)의Class에 해당하는 클래스 객체를 조회하고isinstance(obj, Class)를 호출합니다. 여기서obj는 매치 대상 값입니다. 거짓이면 매치는 실패합니다.- 그렇지 않고 위치 인자나 키워드 인자 형태로 하위 패턴이 주어진 경우, 이들은 다음과 같이 왼쪽에서 오른쪽으로 매칭됩니다. 하위 패턴 하나가 실패하는 즉시 매칭은 실패합니다. 모든 하위 패턴이 성공하면 클래스 패턴 매칭 전체가 성공합니다.
- 위치 기반 매칭 항목이 있고 클래스에
__match_args__속성이 있으면, 위치i에 있는 항목은__match_args__[i]속성으로 조회한 값과 매칭됩니다. 예를 들어,Point2d.__match_args__ == ["x", "y"]일 때 패턴Point2d(5, 8)은 (대략)obj.x == 5 and obj.y == 8로 변환됩니다. - 위치 항목이
__match_args__의 길이보다 많으면TypeError가 발생합니다. - 매칭 대상 클래스에
__match_args__속성이 없는데 매칭에 위치 항목이 하나 이상 나타나는 경우에도TypeError가 발생합니다.__slots__나__annotations__를 대신 사용하는 방식으로 대체하지 않습니다 – “모호한 상황에서는 추측하려는 유혹을 거부하십시오.” - 키워드 기반 매칭 항목이 있으면, 키워드를 대상(subject)의 속성으로 조회합니다. 조회에 성공하면 그 값을 해당 하위 패턴과 매칭합니다. 조회에 실패하면 매칭이 실패합니다.
이러한 프로토콜은 유연성과 성능보다 구현의 단순성을 우선합니다. 검토한 다른 대안은 extended matching을 참조하십시오.
가장 흔히 매칭되는 내장 타입(bool, bytearray, bytes, dict, float, frozenset, int, list, set, str, tuple)에는 호출에 위치 하위 패턴 하나를 전달할 수 있습니다. 이 하위 패턴은 대상의 특정 속성과 매칭되는 대신 대상 자체와 매칭됩니다. 이로써 이러한 객체에 대해 유용하고 직관적인 동작이 만들어집니다.
bool(False)는False와 매칭됩니다(0과는 매칭되지 않습니다).tuple((0, 1, 2))는(0, 1, 2)와 매칭됩니다([0, 1, 2]와는 매칭되지 않습니다).int(i)는 모든int와 매칭되며 이를i라는 이름에 바인딩합니다.
겹치는 하위 패턴
특정 종류의 겹치는 매칭은 런타임에 감지되어 예외를 발생시킵니다. 이전 하위 절에서 설명한 기본 검사 외에도 다음과 같은 검사를 수행합니다.
- 인터프리터는 두 매칭 항목이 같은 속성을 대상으로 하지 않는지 검사합니다. 예를 들어
Point2d(1, 2, y=3)은 오류입니다. - 또한 매핑 패턴이 같은 키를 두 번 이상 매칭하려 하지 않는지도 검사합니다.
특수 속성 __match_args__
__match_args__ 속성은 항상 패턴에 지정된 타입 객체에서 조회합니다. 이 속성이 있으면, 허용되는 위치 인자의 이름을 나타내는 문자열의 리스트나 튜플이어야 합니다.
매칭에 어떤 이름을 사용할 수 있게 할지 결정할 때 권장하는 방식은 클래스 패턴이 생성 방식을 그대로 반영하도록 하는 것입니다. 즉, 사용할 수 있는 이름의 집합과 그 타입이 __init__()의 인자와 비슷해야 합니다.
기본적으로는 이름 기반 매칭만 동작하며, 위치 기반 매칭을 지원하려는 클래스는 __match_args__를 클래스 속성으로 정의해야 합니다. 또한 데이터클래스와 네임드 튜플은 별도 설정 없이 위치 기반 매칭을 지원합니다. 자세한 내용은 아래를 참조하십시오.
예외와 부작용
각 케이스를 매칭하는 동안 match 문은 다른 함수(예: __getitem__(), __len__() 또는 프로퍼티)의 실행을 유발할 수 있습니다. 이로 인해 발생하는 예외는 거의 모두 평소처럼 match 문 바깥으로 전파됩니다. 예외가 전파되지 않는 유일한 경우는 클래스 패턴의 속성을 매칭하면서 속성을 조회하려다 발생한 AttributeError입니다. 이 경우에는 단지 매칭 실패로 처리되며, 문의 나머지 부분은 평소대로 진행됩니다.
매칭 과정이 명시적으로 일으키는 유일한 부작용은 이름 바인딩입니다. 그러나 이 과정은 대상(subject)과 그 구성 요소 일부에 대한 속성 접근, 인스턴스 검사, len(), 동등성 비교, 항목 접근에 의존합니다. 또한 상수 값 패턴과 클래스 패턴의 왼쪽 부분을 평가합니다. 이들 중 어느 것도 일반적으로 부작용을 일으키지 않지만, 일부 객체는 부작용을 일으킬 수도 있습니다. 이 제안은 어떤 메서드가 호출되는지, 또는 몇 번 호출되는지에 대한 명세를 의도적으로 포함하지 않습니다. 그러한 동작에 의존하는 사용자 코드는 버그가 있는 것으로 간주해야 합니다.
표준 라이브러리
패턴 매칭을 쉽게 사용할 수 있도록 표준 라이브러리에 몇 가지 변경이 이루어집니다:
- 네임드튜플과 데이터클래스에는
__match_args__가 자동으로 생성됩니다. - 데이터클래스의 경우 생성된
__match_args__의 속성 순서는 생성된__init__()메서드에서 대응하는 인자의 순서와 같습니다. 여기에는 속성을 슈퍼클래스로부터 상속받는 경우도 포함됩니다.
또한 기존 표준 라이브러리 클래스를 체계적으로 검토하여, 유익해 보이는 곳에 __match_args__를 추가할 것입니다.
정적 검사기 명세
완전성 검사
신뢰성 관점에서, 가능한 데이터 값의 집합을 다룰 때 케이스를 하나라도 빠뜨리면 디버깅하기 어려운 문제로 이어진다는 것이 경험으로 드러났으며, 이 때문에 사람들은 다음과 같은 안전용 assert를 추가해야 했습니다:
def get_first(data: Union[int, list[int]]) -> int:
if isinstance(data, list) and data:
return data[0]
elif isinstance(data, int):
return data
else:
assert False, "should never get here"
PEP 484는 정적 타입 검사기가 enum 값에 대한 조건 검사에서 완전성을 지원해야 한다고 명시합니다. 이후 PEP 586은 이 요구 사항을 리터럴 타입으로 일반화했습니다.
이 PEP는 이 요구 사항을 임의의 패턴으로 더욱 일반화합니다. 이것이 적용되는 전형적인 상황은 유니온 타입을 가진 표현식을 매칭하는 경우입니다:
def classify(val: Union[int, Tuple[int, int], List[int]]) -> str:
match val:
case [x, y] if x > 0 and y > 0:
return f"A pair of {x} and {y}"
case [x, *other]:
return f"A sequence starting with {x}"
case int():
return f"Some integer"
# Type-checking error: some cases unhandled.
완전성 검사는 패턴 매칭과 enum 값이 함께 사용되는 경우에도 적용되어야 합니다:
from enum import Enum
from typing import Union
class Level(Enum):
BASIC = 1
ADVANCED = 2
PRO = 3
class User:
name: str
level: Level
class Admin:
name: str
account: Union[User, Admin]
match account:
case Admin(name=name) | User(name=name, level=Level.PRO):
...
case User(level=Level.ADVANCED):
...
# Type-checking error: basic user unhandled
모든 클래스는 매칭 가능하므로 위에서 명시한 검사의 대상이 되며, 따라서 (PEP 544의 의미에서의) Matchable 프로토콜은 당연히 필요하지 않습니다.
대수적 데이터 타입으로서의 봉인된 클래스
임시방편의 유니온 타입을 정의하지 않고도 클래스 집합에 완전성을 적용하고 싶은 경우가 꽤 자주 있습니다. 그러한 유니온 타입은 정의에서 클래스 하나가 빠지기만 해도 문제가 생기므로 그 자체로 취약합니다. 레코드와 유사한 클래스 여러 개를 하나의 유니온으로 묶는 디자인 패턴은 패턴 매칭을 지원하는 다른 언어에서 널리 쓰이며, 대수적 데이터 타입(algebraic data types)이라는 이름으로 알려져 있습니다.
typing 모듈에 특별한 데코레이터 클래스 @sealed를 추가할 것을 제안합니다. 이 데코레이터는 런타임에는 아무런 효과가 없지만, 이 클래스의 모든 서브클래스(직접 및 간접)가 베이스 클래스와 같은 모듈에 정의되어야 함을 정적 타입 검사기에 알려 줍니다.
모든 서브클래스를 알 수 있으므로 타입 검사기가 봉인된 베이스 클래스를 그 모든 서브클래스의 유니온으로 취급할 수 있다는 것이 핵심 아이디어입니다. 데이터클래스와 함께 사용하면 Python에서 대수적 데이터 타입을 깔끔하고 안전하게 지원할 수 있습니다. 다음 예를 생각해 보십시오:
from dataclasses import dataclass
from typing import sealed
@sealed
class Node:
...
class Expression(Node):
...
class Statement(Node):
...
@dataclass
class Name(Expression):
name: str
@dataclass
class Operation(Expression):
left: Expression
op: str
right: Expression
@dataclass
class Assignment(Statement):
target: str
value: Expression
@dataclass
class Print(Statement):
value: Expression
이렇게 정의하면 타입 검사기는 Node를 Union[Name, Operation, Assignment, Print]로 안전하게 취급할 수 있으며, 예를 들어 Expression도 Union[Name, Operation]으로 안전하게 취급할 수 있습니다. 따라서 아래 코드 조각에서는 Name이 처리되지 않았기 때문에 타입 검사 오류가 발생합니다(그리고 타입 검사기는 유용한 오류 메시지를 제공할 수 있습니다):
def dump(node: Node) -> str:
match node:
case Assignment(target, value):
return f"{target} = {dump(value)}"
case Print(value):
return f"print({dump(value)})"
case Operation(left, op, right):
return f"({dump(left)} {op} {dump(right)})"
타입 소거
클래스 패턴에는 런타임 타입 소거가 적용됩니다. 즉, 타입 별칭 IntQueue = Queue[int]를 정의하여 IntQueue()와 같은 패턴을 구문상 유효하게 만들 수는 있지만, 타입 검사기는 이러한 매치를 거부해야 합니다:
queue: Union[Queue[int], Queue[str]]
match queue:
case IntQueue(): # Type-checking error here
...
위 코드 조각은 현재 typing 모듈의 제네릭 클래스 구현에서도, 최근 채택된 PEP 585의 내장 제네릭 클래스에서도 실제로 런타임에 실패한다는 점에 유의하십시오. 이들이 isinstance 검사를 금지하기 때문입니다.
분명히 하자면, 제네릭 클래스가 패턴 매칭에 참여하는 것 자체가 일반적으로 금지되는 것은 아니며, 단지 그 타입 매개변수를 명시적으로 지정할 수 없을 뿐입니다. 하위 패턴이나 리터럴이 타입 변수를 바인딩하는 것은 여전히 허용됩니다. 예를 들면 다음과 같습니다:
from typing import Generic, TypeVar, Union
T = TypeVar('T')
class Result(Generic[T]):
first: T
other: list[T]
result: Union[Result[int], Result[str]]
match result:
case Result(first=int()):
... # Type of result is Result[int] here
case Result(other=["foo", "bar", *rest]):
... # Type of result is Result[str] here
상수에 관한 참고 사항
캡처 패턴이 항상 대입 대상이라는 사실은, 사용자가 상수 값 패턴을 사용하는 대신 실수로 값을 상수와 “매치”하려 할 때 원치 않는 결과를 낳을 수 있습니다. 그 결과, 런타임에 이러한 매치는 항상 성공하며 게다가 상수의 값을 덮어쓰게 됩니다. 따라서 정적 타입 검사기가 이러한 상황에 대해 경고하는 것이 중요합니다. 예를 들면 다음과 같습니다:
from typing import Final
MAX_INT: Final = 2 ** 64
value = 0
match value:
case MAX_INT: # Type-checking error here: cannot assign to final name
print("Got big number")
case _:
print("Something else")
CPython 참조 구현도 이 경우에 SyntaxWarning 메시지를 생성한다는 점에 유의하십시오.
스타 매치의 정밀한 타입 검사
타입 검사기는 패턴 매칭에서 스타 항목에 대해 정밀한 타입 검사를 수행하여, 이들에게 이종(heterogeneous) list[T] 타입이나 PEP 589에 명시된 TypedDict 타입을 부여해야 합니다. 예를 들면 다음과 같습니다:
stuff: Tuple[int, str, str, float]
match stuff:
case a, *b, 0.5:
# Here a is int and b is list[str]
...
성능 고려 사항
이상적으로는, match 문은 이와 동등한 if 문 연쇄에 비해 좋은 런타임 성능을 보여야 합니다. 프로그래밍 언어의 역사에는 추가적인 CPU 사이클을 대가로 엔지니어의 생산성을 높인 새로운 기능의 사례가 넘쳐나지만, match의 이점이 전반적인 런타임 성능의 상당한 저하로 상쇄된다면 유감스러운 일일 것입니다.
이 PEP는 특정한 구현 전략을 명시하지 않지만, 프로토타입 구현과 그것이 성능을 극대화하려고 어떻게 시도하는지에 대해 몇 마디 언급해 두는 것이 좋겠습니다.
기본적으로 프로토타입 구현은 match 문 문법 전체를 동등한 if/else 블록으로, 더 정확히 말하면 같은 효과를 내는 Python 바이트코드로 변환합니다. 다시 말해, 인스턴스 타입, 시퀀스 길이, 매핑 키 등을 검사하는 모든 로직이 match 자리에 인라인으로 삽입됩니다.
이것이 유일하게 가능한 전략은 아니며, 반드시 최선의 전략인 것도 아닙니다. 예를 들어, 인스턴스 검사를 메모이제이션할 수 있으며, 하나의 match 문 안에 클래스 타입은 같지만 인자가 다른 인스턴스가 여러 개 있는 경우에 특히 그렇습니다. 또한 이론적으로는, 향후 구현이 case 절이나 하위 패턴을 하나씩 검사하는 대신 결정 트리를 사용하여 병렬로 처리하는 것도 가능합니다.
하위 호환성
이 PEP는 완전한 하위 호환성을 갖습니다. match와 case 키워드는 소프트 키워드로 제안되었으며 (앞으로도 계속 그럴 것입니다!), 따라서 이들을 변수, 함수, 클래스, 모듈, 속성 이름으로 사용하는 데 전혀 지장이 없습니다.
이는 match가 re 모듈에 있는 널리 쓰이고 잘 알려진 함수이자 메서드의 이름이기 때문에 중요하며, 우리는 이를 망가뜨리거나 폐지할 생각이 전혀 없습니다.
하드 키워드와 소프트 키워드의 차이는, 하드 키워드는 의미가 없는 위치에서조차 (예: x = class + 1) 항상 예약어인 반면, 소프트 키워드는 문맥 안에서만 특별한 의미를 갖는다는 점입니다. PEP 617 이후로 파서는 백트래킹을 하므로, 코드 조각을 파싱하려는 시도마다 소프트 키워드를 다르게 해석할 수 있습니다.
예를 들어, 파서가 다음과 같은 입력을 만났다고 가정해 봅시다:
match [x, y]:
파서는 먼저 이것을 표현식 문으로 파싱하려고 시도합니다. match를 NAME 토큰으로 해석하고, 이어서 [x, y]를 이중 서브스크립트로 간주합니다. 그런 다음 콜론을 만나게 되는데, 표현식 문 뒤에는 콜론이 올 수 없으므로 백트래킹해야 합니다. 그러면 파서는 줄의 시작 부분으로 백트래킹하여 match가 이 위치에서 허용되는 소프트 키워드임을 알아냅니다. 그런 다음 [x, y]를 리스트 표현식으로 간주합니다. 이번에는 콜론이 파서가 기대한 바로 그것이므로 파싱이 성공합니다.
서드파티 도구에 미치는 영향
Python 생태계에는 Python 소스 코드를 다루는 도구가 많습니다. 린터, 문법 강조기, 자동 포매터, IDE 등이 그것입니다. 이 도구들은 모두 match 문을 인식하도록 업데이트되어야 합니다.
일반적으로 이러한 도구는 다음 두 범주 중 하나에 속합니다.
얕은 파서는 Python의 전체 문법을 이해하려 하지 않고, 대신 소스 코드에서 이미 알려진 특정 패턴을 검색합니다. Visual Studio Code, Emacs, TextMate 같은 IDE는 이 범주에 속하는 경향이 있는데, 편집 중인 소스 코드는 유효하지 않은 경우가 많아서 엄격한 방식으로 파싱하면 실패하기 때문입니다.
이런 종류의 도구에서는 새 키워드를 인식하도록 하는 일이 비교적 쉬우며, 테이블에 항목을 추가하거나 정규 표현식을 수정하기만 하면 됩니다.
깊은 파서는 Python의 전체 문법을 이해합니다. 그 예로 자동 포매터인 Black이 있습니다. 이러한 종류의 도구에 특별히 요구되는 점은 현재 버전의 Python 문법뿐만 아니라 이전 버전의 Python 문법도 이해해야 한다는 것입니다.
match 문은 소프트 키워드를 사용하며, 새로운 PEG 파서의 기능을 활용하는 최초의 주요 Python 기능 중 하나입니다. 이는 ‘PEG 호환’이 아닌 서드파티 파서가 새로운 문법을 처리하는 데 어려움을 겪을 것임을 의미합니다.
이러한 서드파티 도구 중 상당수가 공통 파싱 라이브러리를 활용한다는 점이 지적되었습니다(예를 들어 Black은 lib2to3 파서의 포크를 사용합니다). 널리 사용되는 파싱 라이브러리(parso나 libCST 등)를 파악하여 PEG 호환이 되도록 업그레이드하는 것이 도움이 될 수 있습니다.
그러나 이 작업은 match 문뿐만 아니라 PEG 파서의 기능을 활용하는 모든 새로운 Python 문법에 대해 이루어져야 하므로, 이 PEP의 범위를 벗어나는 것으로 간주합니다. (다만 이것이 좋은 Summer of Code 프로젝트가 될 것이라는 제안이 있습니다.)
참조 구현
기능을 모두 갖춘 CPython 구현이 GitHub에 공개되어 있습니다.
위 구현을 기반으로 한 대화형 플레이그라운드가 Binder와 Jupyter를 사용하여 만들어졌습니다.
예제 코드
작은 예제 코드 모음이 GitHub에 공개되어 있습니다.
기각된 아이디어
이 전반적인 아이디어는 꽤 오랫동안 논의되어 왔으며, 여러 차례 번복을 거치며 많은 결정이 내려졌습니다. 여기서는 시도했지만 결국 포기한 여러 대안을 요약합니다.
이렇게 하지 마십시오, 패턴 매칭은 배우기 어렵습니다
저희는 제안된 패턴 매칭이 이터러블 언패킹에 isinstance()와 getattr()를 추가하는 것보다 더 어렵지 않다고 생각합니다. 또한 제안된 문법은 원하는 일을 어떻게 할지가 아니라 무엇을 할지를 표현할 수 있게 해 주므로, 폭넓은 코드 패턴에서 가독성을 크게 향상시킨다고 믿습니다. 위에서 이 PEP에 포함한 몇 가지 실제 코드 조각이 이 비교를 충분히 잘 보여 주기를 바랍니다. 더 많은 실제 코드 예제와 그 변환은 참고 문헌 [1]을 참조하십시오.
이렇게 하지 말고 기존의 메서드 디스패치 메커니즘을 사용하십시오
저희는 match 문의 일부 사용 사례가 클래스 상속을 사용하는 전통적인 객체 지향 프로그래밍(OOP) 설계 기법으로 할 수 있는 일과 겹친다는 것을 인정합니다. 매치 대상의 런타임 타입을 검사하여 다른 동작을 선택하는 기능은 엄격한 OOP 순수주의자들에게는 이단적으로 보일 수도 있습니다.
그러나 Python은 언제나 다양한 프로그래밍 스타일과 패러다임을 포용하는 언어였습니다. “덕” 타이핑과 같은 고전적인 Python 설계 관용구는 전통적인 OOP 모델을 넘어섭니다.
저희는 match를 사용하면 더 깔끔하고 유지 보수하기 쉬운 아키텍처를 얻을 수 있는 중요한 사용 사례가 있다고 믿습니다. 이러한 사용 사례는 대체로 다음과 같은 여러 특징을 보입니다:
- 전통적인 데이터 캡슐화의 경계를 가로지르는 알고리즘입니다. 알고리즘이 서로 다른 타입의 이질적인 요소를 처리하는 경우(예를 들어 추상 문법 트리를 평가하거나 변환하는 경우, 또는 수학 기호를 대수적으로 조작하는 경우), 사용자에게 알고리즘을 각 요소 타입의 개별 메서드로 구현하도록 강제하면 로직이 한곳에 깔끔하게 모이지 못하고 코드베이스 전체에 흩어지게 됩니다.
- 가능한 데이터 타입의 집합은 비교적 안정적이지만, 그 데이터 타입에 대해 수행할 연산의 집합은 계속 늘어나는 프로그램 아키텍처입니다. 이를 엄격한 OOP 방식으로 처리하려면 새 메서드를 지원하기 위해 베이스 클래스와 서브클래스 모두에 새 메서드를 끊임없이 추가해야 하며, 이는 매우 특수화된 수많은 메서드 정의로 베이스 클래스를 “오염”시키고 코드 전반에 광범위한 혼란과 잦은 변경을 일으킵니다. 반면에
match기반 디스패치에서는 새 동작을 추가할 때 새match문을 작성하기만 하면 됩니다. - 또한 OOP는 튜플의 길이나 속성의 존재 여부와 같은 객체의 형태에 따른 디스패치를 처리하지 못합니다. 대신 그러한 디스패치 결정은 모두 객체의 타입에 인코딩되어야 합니다. 형태 기반 디스패치는 “덕” 타이핑된 객체를 다룰 때 특히 흥미롭습니다.
OOP가 분명히 우월한 경우는 그 반대의 경우, 즉 가능한 연산의 집합은 비교적 안정적이고 잘 정의되어 있지만 연산 대상이 되는 데이터 타입의 집합은 계속 늘어나는 경우입니다. 이에 대한 고전적인 예는 UI 위젯 툴킷입니다. 여기에는 고정된 상호 작용 유형(다시 그리기, 마우스 클릭, 키 누름 등)의 집합이 있지만, 개발자들이 새롭고 창의적인 사용자 상호 작용 방식을 고안해 냄에 따라 위젯 타입의 집합은 끊임없이 늘어납니다. 새로운 종류의 위젯을 추가하는 것은 새 서브클래스를 작성하기만 하면 되는 간단한 일이지만, match 기반 접근 방식에서는 결국 곳곳에 흩어진 많은 match 문에 새 case 절을 추가해야 합니다. 따라서 저희는 그러한 상황에서 match를 사용하는 것을 권장하지 않습니다.
대신 더 유연한 대입 대상을 허용하십시오
새로운 종류의 문을 추가하는 대신, 이터러블 언패킹을 훨씬 더 일반적인 대입 대상으로 일반화하기만 하자는 아이디어가 있었습니다. 이 개념은 일부 다른 언어에서 “반박 불가능한 매치(irrefutable matches)”로 알려져 있습니다. 실제 잠재적 사용 사례를 검토한 결과, 대부분의 경우 구조 분해가 if 조건과 관련되어 있었기 때문에 저희는 이 방식을 채택하지 않기로 결정했습니다. 또한 그중 상당수는 상호 배타적인 일련의 선택지로 묶여 있습니다.
표현식으로 만드십시오
대부분의 다른 언어에서 패턴 매칭은 문이 아니라 표현식으로 표현됩니다. 그러나 이를 표현식으로 만들면 Python의 다른 문법적 선택과 일관성이 맞지 않게 됩니다. 모든 의사 결정 로직은 거의 예외 없이 문으로 표현되므로, 저희는 이 원칙에서 벗어나지 않기로 결정했습니다.
하드 키워드를 사용하십시오
match를 하드 키워드로 만들거나 다른 키워드를 선택하는 방안도 있었습니다. 하드 키워드를 사용하면 단순한 문법 강조기의 작업이 쉬워지겠지만, 저희는 다음과 같은 여러 이유로 하드 키워드를 사용하지 않기로 결정했습니다.
- 가장 중요한 이유는 새 파서에서는 그렇게 할 필요가 없다는 점입니다. 몇 릴리스 동안 소프트 키워드로 있으면서 어려움을 일으켰던
async와 달리, 여기서는match를 영구적인 소프트 키워드로 만들 수 있습니다. match는 기존 코드에서 매우 흔히 사용되므로, 거의 모든 기존 프로그램이 동작하지 않게 되고, 새로운 문법의 혜택을 받지도 못할 수 있는 많은 사람들에게 코드를 수정해야 하는 부담을 지우게 됩니다.- 기존 프로그램에서 식별자로 흔히 쓰이지 않으면서도 문의 의미를 명확하게 드러내는 대체 키워드를 찾기는 어렵습니다.
case 절에 case 대신 as나 | 사용하기
여기에서 제안하는 패턴 매칭은 다중 브랜치 제어 흐름(Algol 계열 언어의 switch나 Lisp의 cond와 같은 것)과 함수형 언어에서 볼 수 있는 객체 분해를 결합한 것입니다. 제안된 키워드 case는 다중 브랜치 측면을 강조하지만, 분해 측면을 강조하는 as 같은 대안 키워드도 똑같이 가능할 것입니다. 예를 들어 as나 with는 이미 Python의 키워드라는 장점도 있습니다. 하지만 키워드로서의 case는 match 문 안에서 맨 앞 키워드로만 나타날 수 있으므로, 파서가 키워드로 쓰인 경우와 변수로 쓰인 경우를 쉽게 구별할 수 있습니다.
다른 변형으로는 |나 => 같은 기호를 사용하거나, 특별한 표시를 전혀 사용하지 않는 방안이 있습니다.
Python은 Algol의 전통을 잇는 문장 중심 언어이고 각 복합문이 식별 키워드로 시작하므로, case가 Python의 스타일과 전통에 가장 잘 맞는 것으로 보였습니다.
평평한 들여쓰기 방식 사용하기
다른 들여쓰기 방식을 사용하자는 아이디어가 있었습니다. 예를 들어 각 case 절을 처음의 match 부분보다 들여쓰지 않는 방식입니다:
match expression:
case pattern_1:
...
case pattern_2:
...
그 이유는 평평한 들여쓰기가 가로 공간을 어느 정도 절약하기는 하지만, 다른 모든 곳에서는 콜론 뒤에 들여쓰기가 오기 때문에 Python 프로그래머의 눈에는 어색해 보일 수 있다는 것입니다. 또한 단순한 코드 편집기의 작업도 복잡해집니다. 마지막으로, 가로 공간 문제는 match 문에 “반 들여쓰기”(즉, 네 칸 대신 두 칸)를 허용하여 완화할 수 있습니다.
이 PEP를 개발하는 과정에서 작성한 match 사용 예제 프로그램에서는 코드 간결성이 눈에 띄게 향상되어, 늘어난 들여쓰기 수준을 충분히 상쇄하고도 남았습니다.
검토된 또 다른 제안은 평평한 들여쓰기를 사용하되 다음과 같이 식을 match: 다음 줄에 두는 것이었습니다:
match:
expression
case pattern_1:
...
case pattern_2:
...
이 제안은 첫 번째 블록이 Python 문법에서 새로운 형태, 즉 문장의 나열이 아니라 단일 식만을 내용으로 하는 블록이 된다는 이유로 결국 기각되었습니다.
상수 값 패턴의 대안
아마도 이것이 가장 까다로운 항목입니다. 미리 정의된 상수와 매칭하는 일은 매우 흔하지만, Python의 동적인 특성 때문에 캡처 패턴과 모호해지기도 합니다. 다섯 가지 다른 대안을 검토했습니다:
- 몇 가지 암묵적 규칙을 사용합니다. 예를 들어, 이름이 전역 스코프에 정의되어 있으면 그 이름은 캡처 패턴이 아니라 상수를 가리킵니다:
# Here, the name "spam" must be defined in the global scope (and # not shadowed locally). "side" must be local. match entree[-1]: case spam: ... # Compares entree[-1] == spam. case side: ... # Assigns side = entree[-1].
하지만 누군가 match 문 앞에서 관련 없는 같은 이름을 정의하면 예상치 못한 동작과 원격 작용(action at a distance)이 일어날 수 있습니다.
- 이름의 대소문자에 기반한 규칙을 사용합니다. 구체적으로, 이름이 소문자로 시작하면 캡처 패턴이 되고, 대문자로 시작하면 상수를 가리킵니다:
match entree[-1]: case SPAM: ... # Compares entree[-1] == SPAM. case side: ... # Assigns side = entree[-1].
이는 PEP 8의 상수 명명 권고와 잘 맞습니다. 주된 반대 의견은 핵심 Python의 다른 어떤 부분에서도 이름의 대소문자가 의미상 중요하지 않다는 것입니다. 게다가 Python은 식별자에 다양한 문자 체계를 사용할 수 있게 허용하는데, 그중 다수(예: CJK)에는 대소문자 구분이 없습니다.
- 추가 괄호를 사용하여 주어진 이름에 조회 의미 체계가 적용된다는 것을 나타냅니다. 예를 들면 다음과 같습니다.:
match entree[-1]: case (spam): ... # Compares entree[-1] == spam. case side: ... # Assigns side = entree[-1].
실현 가능한 방법일 수 있지만, 자주 사용하면 시각적으로 어수선해질 수 있습니다. 또한 솔직히 말해 상당히 낯설어 보이며, 중첩된 문맥에서는 특히 그렇습니다.
이 방법에는 패턴 안에서 그룹을 명확히 구분하려고 괄호를 쓰고 싶거나 써야 할 수도 있다는 문제도 있습니다. 예를 들면
Point(x, y=(y := complex()))에서 그렇습니다. - 주어진 이름이 대입될 대상이 아니라 매칭될 값이라는 것을 나타내기 위해
.,?,$,^같은 특수 기호를 도입합니다. 이 제안의 이전 버전에서는 이름 앞에 점을 붙이는 규칙을 사용했습니다.:match entree[-1]: case .spam: ... # Compares entree[-1] == spam. case side: ... # Assigns side = entree[-1].
잠재적으로 유용할 수는 있지만, 패턴 문법의 표현력을 전혀 높이지 못하면서 낯설어 보이는 새 문법을 도입합니다. 실제로 이름 붙은 상수는
Enum타입으로 변환하거나 자체 네임스페이스로 감싸면 기존 규칙으로도 동작하게 만들 수 있습니다(저자들은 네임스페이스를 아주 훌륭한 아이디어라고 생각합니다).:match entree[-1]: case Sides.SPAM: ... # Compares entree[-1] == Sides.SPAM. case side: ... # Assigns side = entree[-1].
필요하다면 이름 앞에 점을 붙이는 규칙(또는 이와 비슷한 변형)을 나중에 하위 호환성 문제 없이 다시 추가할 수 있습니다.
- 조회 의미 체계를 기본으로 하고, 캡처 패턴에는
$또는?기호를 사용하도록 요구하자는 아이디어도 있었습니다.:match entree[-1]: case spam: ... # Compares entree[-1] == spam. case side?: ... # Assigns side = entree[-1].여기에는 몇 가지 문제가 있습니다.
- 일반적인 코드에서는 캡처 패턴이 더 흔하므로, 캡처 패턴에 특별한 문법을 요구하는 것은 바람직하지 않습니다.
- 저자들이 아는 한, 이런 방식으로 캡처에 표시를 붙이는 다른 언어는 없습니다.
- 제안된 문법 중 어느 것도 Python에 선례가 없습니다. Python에서 이름을 바인딩하는 다른 어떤 곳(예:
import,def,for)도 특별한 마커 문법을 사용하지 않습니다. - 현재 문법이 가진 구문상의 대응 관계가 깨지게 됩니다.:
match coords: case ($x, $y): return Point(x, y) # Why not "Point($x, $y)"?
결국 이 대안들은 앞에서 언급한 단점들 때문에 기각되었습니다.
패턴에서 부동소수점 리터럴 금지
부동소수점 수는 정확하지 않기 때문에, 이 제안의 초기 버전에서는 부동소수점 상수를 매치 패턴으로 사용하는 것을 허용하지 않았습니다. 이렇게 금지한 근거 중 하나는 Rust가 그렇게 한다는 점이었습니다.
그러나 구현 과정에서 부동소수점 값을 다른 타입과 구별하려면 VM에 추가 코드가 필요하며, 이 코드 때문에 매칭이 전반적으로 느려진다는 사실이 밝혀졌습니다. Python과 Rust는 사용자층과 근본 철학이 서로 다른, 매우 다른 언어라는 점을 고려하여, 부동소수점 리터럴을 허용해도 큰 해가 되지 않으며 사용자에게도 덜 놀라울 것이라고 판단했습니다.
범위 매칭 패턴
이렇게 하면 1...6 같은 패턴을 사용할 수 있습니다. 그러나 모호한 점이 많습니다.
- 범위가 열린 범위입니까, 반열린 범위입니까, 아니면 닫힌 범위입니까? (즉, 위 예에서
6이 포함됩니까, 포함되지 않습니까?) - 범위가 단일 숫자와 매칭됩니까, 아니면 범위 객체와 매칭됩니까?
- 범위 매칭은 흔히 문자 범위(‘a’…’z’)에 사용되지만, Python에는 문자 데이터 타입 없이 문자열만 있으므로 Python에서는 이 방식이 동작하지 않습니다.
- 점프 테이블을 미리 만들 수 있다면 범위 매칭으로 성능을 크게 최적화할 수 있지만, Python에서는 이름이 동적으로 다시 바인딩될 수 있으므로 일반적으로 이것이 불가능합니다.
범위를 위한 특수한 문법을 만드는 대신, 사용자 정의 패턴 객체(InRange(0, 6))를 허용하는 편이 더 유연하고 덜 모호하다고 결정했습니다. 다만 이러한 아이디어는 당분간 보류되었습니다(Deferred Ideas를 참고하십시오).
매칭에 디스패치 딕셔너리 의미 체계 사용
전통적인 switch 문의 구현은 성능을 높이기 위해 연쇄적인 동등 비교 대신 미리 계산된 해시 테이블을 사용하기도 합니다. match 문에서도 리터럴 패턴에 대한 매칭이라면 기술적으로 이렇게 할 수 있습니다. 그러나 패턴 종류마다 의미가 미묘하게 달라진다면, 성능 이득이 크지 않을 수도 있는 데 비해 지나치게 놀라운 결과가 됩니다.
의미상의 차이를 일으키지 않는다면 이 방향의 성능 최적화는 여전히 실험해 볼 수 있습니다.
case 절에서 continue와 break 사용하기
기각된 또 다른 제안은 match 내부에서 continue와 break에 새로운 의미를 정의하는 것이었으며, 그 동작은 다음과 같습니다:
continue는 현재 case 절을 빠져나와 다음 case 절에서 매칭을 계속합니다.break는 match 문을 빠져나갑니다.
그러나 이 제안에는 심각한 단점이 있습니다. match 문이 루프 안에 중첩되어 있으면 continue와 break의 의미가 바뀌게 됩니다. 이는 리팩터링 중에 예상치 못한 동작을 일으킬 수 있습니다. 또한 같은 동작을 얻을 수 있는 다른 수단(가드 조건 사용 등)이 있으며, 실제로는 continue와 break의 기존 동작이 훨씬 더 유용할 가능성이 높다는 인자도 제기될 수 있습니다.
AND (&) 패턴
이 제안은 여러 대안 중 하나에 매칭하기 위한 OR 패턴(|)을 정의합니다. 그렇다면 AND 패턴(&)은 왜 정의하지 않습니까? 특히 다른 몇몇 언어(예를 들어 F#)가 이를 지원한다는 점을 고려하면 더욱 그렇습니다.
그러나 이것이 얼마나 유용할지는 분명하지 않습니다. 딕셔너리, 객체, 시퀀스를 매칭하는 의미에는 이미 암묵적인 ‘and’가 포함되어 있습니다. 매칭이 성공하려면 언급된 모든 속성과 요소가 존재해야 합니다. 가상의 ‘and’ 연산자가 쓰일 만한 사용 사례 중 상당수는 가드 조건으로도 처리할 수 있습니다.
결국 이것은 큰 이점 없이 문법만 더 복잡하게 만든다고 결정되었습니다.
부정 매치 패턴
연산자 !를 접두사로 사용하는 매치 패턴의 부정은 해당 패턴 자체가 매칭되지 않을 때 정확히 매칭됩니다. 예를 들어 !(3 | 4)는 3이나 4를 제외한 모든 것에 매칭됩니다.
이 기능은 (이를 지원하는 언어에서도) 거의 유용하지 않거나, 변수 스코프를 제어하고 변수 바인딩을 막기 위한 이중 부정 !!로 사용된다는(이는 Python에는 해당하지 않습니다) documented evidence가 있기 때문에 거부되었습니다. 또한 가드 조건을 사용하여 흉내 낼 수도 있습니다.
런타임에 망라성 검사하기
문제는 매칭되는 패턴을 가진 case 절이 하나도 없고 기본 case도 없을 때 어떻게 할 것인가입니다. 이 제안의 초기 버전에서는 이 경우 조용히 통과하는 대신 예외를 발생시키도록 규정했습니다.
양쪽의 인자가 많이 오갔지만, 결국 EIBTI(Explicit Is Better Than Implicit, 명시적인 것이 암묵적인 것보다 낫다) 인자가 이겼습니다. 그것이 원하는 동작이라면 프로그래머가 명시적으로 예외를 던지는 것이 더 낫습니다.
봉인된 클래스(sealed class)나 열거형처럼 패턴이 모두 이산적인 집합의 멤버임이 알려진 경우에는 정적 검사기가 누락된 패턴에 대해 경고할 수 있습니다.
패턴 변수에 대한 타입 어노테이션
패턴을 타입 어노테이션과 결합하자는 제안이었습니다:
match x:
case [a: int, b: str]: print(f"An int {a} and a string {b}:)
case [a: int, b: int, c: int]: print(f"Three ints", a, b, c)
...
이 아이디어에는 많은 문제가 있습니다. 우선, 콜론은 대괄호나 괄호 안에서만 사용할 수 있으며, 그렇지 않으면 문법이 모호해집니다. 그리고 Python은 제네릭 타입에 대한 isinstance() 검사를 허용하지 않으므로, 제네릭을 포함하는 타입 어노테이션은 예상대로 동작하지 않을 것입니다.
클래스 패턴에서 *rest 허용
클래스 패턴에서 *rest를 허용하여, 모든 위치 인자에 한꺼번에 바인딩되는 변수를 제공하자는 제안이 있었습니다(언패킹 대입에서의 용법과 유사합니다). 이는 시퀀스 패턴과 어느 정도 대칭을 이룰 것입니다. 그러나 이는 모든 위치 인자에 대한 값을 한꺼번에 제공하는 기능과 혼동될 수 있습니다. 또한 실질적인 필요성도 없어 보이므로 폐기되었습니다. (필요가 생기면 나중 단계에서 쉽게 추가할 수 있습니다.)
상수 값 패턴에서 _.a 금지
첫 번째 공개 초안에서는 _가 패턴 매칭에서 특별한 의미를 가지므로 상수 값 패턴의 첫 번째 이름이 _이어서는 안 된다고 했으며, 따라서 다음은 유효하지 않았을 것입니다:
case _.a: ...
(그러나 a._는 허용되며, 평소처럼 객체 a의 _라는 이름의 속성을 로드합니다.)
python-dev에서 이에 대한 반발이 일부 있었고(특히 i18n에서 _를 중요한 전역 변수로 사용하는 정당한 용도를 가진 사람들이 있습니다), 이 금지의 유일한 이유는 사용자의 혼동을 일부 방지하는 것이었습니다. 하지만 이는 사활을 걸 만한 문제가 아닙니다.
다른 토큰을 와일드카드로 사용
...(즉, 생략 부호 토큰)이나 *(별표)를 와일드카드로 사용하자는 제안이 있었습니다. 그러나 둘 다 임의 개수의 항목이 생략된 것처럼 보입니다:
case [a, ..., z]: ...
case [a, *, z]: ...
둘 다 두 개 이상의 항목으로 이루어진 시퀀스에 매치되어 첫 번째 값과 마지막 값을 캡처하는 것처럼 보입니다.
게다가 *를 와일드카드 문자로 사용한다면, 현재 다음과 같이 표기하는 시퀀스의 나머지를 캡처하는 다른 방법을 고안해야 할 것입니다:
case [first, second, *rest]: ...
생략 부호를 사용하면 ...가 자명하거나 무관한 것을 나타내는 데 흔히 쓰이는 문서와 예제에서 더 혼란스러울 것입니다. (그렇습니다, 이는 Python에서 ...를 다른 용도로 쓰는 것에 대한 반대 인자도 되겠지만, 그것은 이미 지나간 일입니다.)
또 다른 제안은 ?를 사용하는 것이었습니다. 토크나이저를 수정해야 하긴 하지만, 이는 수용할 만할 수도 있습니다.
또한 _는 이미 다른 문맥에서 버리는 대상으로 사용되고 있으며, 이 용법도 그와 꽤 유사합니다. 다음 예제는 표준 라이브러리의 difflib.py에서 가져온 것입니다:
for tag, _, _, j1, j2 in group: ...
아마도 가장 설득력 있는 인자는, 저희가 살펴본 패턴 매칭을 지원하는 다른 모든 언어, 즉 C#, Elixir, Erlang, F#, Haskell, Mathematica, OCaml, Ruby, Rust, Scala, Swift에서 _가 와일드카드로 사용된다는 점입니다. 물론 일반적으로는 다른 언어가 무엇을 하는지에 너무 신경 쓸 필요가 없습니다. Python은 이 모든 언어와 분명히 다르기 때문입니다. 하지만 이처럼 압도적이고 강력한 합의가 있다면, Python이 굳이 완전히 다른 방식을 택해서는 안 됩니다. 특히 _가 Python에서 잘 동작하고 이미 버리는 대상으로 사용되고 있다는 점을 고려하면 더욱 그렇습니다.
패턴은 _에 값을 대입하지 않는다는 점에 유의하십시오 – 이는 gettext 모듈 문서에서 권장하는 대로 _를 번역 가능한 문자열의 표시이자 gettext.gettext의 별칭으로 사용하는 것과의 충돌을 피합니다.
OR 패턴에 | 대신 다른 문법 사용
OR 패턴에서 대안들을 구분하는 데 |를 사용하는 것에 대한 몇 가지 대안이 제안되었습니다. 다음 대신:
case 401|403|404:
print("Some HTTP error")
아래와 같은 제안들이 나왔습니다:
- 쉼표 사용:
case 401, 403, 404: print("Some HTTP error")
이는 튜플과 너무 비슷해 보입니다 – 튜플을 표기하는 다른 방법을 찾아야 할 것이고, 클래스 패턴의 인자 목록 안에서는 이 구문을 괄호로 감싸야 할 것입니다. 일반적으로 쉼표는 Python에서 이미 여러 가지 다른 의미를 가지고 있으므로, 의미를 더 추가해서는 안 됩니다.
- 여러 case 겹쳐 쓰기 허용:
case 401: case 403: case 404: print("Some HTTP error")
C에서는 case의 폴스루(fall-through) 의미 체계를 이용해 이렇게 작성합니다. 그러나 사람들이
match/case가 폴스루 의미 체계(C에서 흔한 버그의 원인입니다)를 사용한다고 오해하게 만들고 싶지는 않습니다. 또한 이는 새로운 들여쓰기 패턴이므로 IDE 등에서 지원하기가 더 어려워질 수 있습니다(“콜론으로 끝나는 줄 다음에는 들여쓰기 수준을 하나 추가한다”라는 단순한 규칙이 깨집니다). 마지막으로, 이 방식은 다른 패턴 안에 중첩된 OR 패턴을 지원하지 못합니다. case in뒤에 쉼표로 구분된 목록 사용하기:case in 401, 403, 404: print("Some HTTP error")
이 방식은 다음과 같이 다른 패턴 안에 중첩된 OR 패턴에는 동작하지 않습니다.:
case Point(0|1, 0|1): print("A corner of the unit square")
or키워드 사용하기:case 401 or 403 or 404: print("Some HTTP error")
이 방식도 동작할 수 있으며, 가독성도
|를 사용하는 것과 크게 다르지 않습니다. 일부 사용자는|를 비트 단위 OR와 연관 짓기 때문에or를 선호한다고 밝혔습니다. 그러나 다음과 같은 이유가 있습니다.- 패턴 매칭을 지원하는 다른 많은 언어가
|를 사용합니다(Elixir, Erlang, F#, Mathematica, OCaml, Ruby, Rust, Scala 등이 있습니다). |는 더 짧으므로Point(0|1, 0|1)과 같은 중첩 패턴의 가독성에 도움이 될 수 있습니다.- 어떤 사람들은
|의 우선순위가 잘못되었다고 오해합니다. 하지만 패턴은 다른 연산자를 지원하지 않으므로 표현식에서와 같은 우선순위를 가집니다. - Python 사용자는
or를 매우 자주 사용하므로,or가 불리언 단락 평가와 강하게 연관되어 있다는 인상을 가질 수 있습니다. |는 정규 표현식과 (Python 자체의 문법과 같은) EBNF 문법에서 대안 사이에 사용됩니다.|는 비트 단위 OR에만 쓰이는 것이 아닙니다. 집합의 합집합, 딕셔너리 병합(PEP 584)에도 쓰이며,typing.Union의 대안으로도 검토되고 있습니다(PEP 604).|는 특히 문자열 사이에서 시각적 구분자로 더 잘 작동합니다. 다음을:case "spam" or "eggs" or "cheese":
아래와 비교해 보십시오.:
case "spam" | "eggs" | "cheese":
- 패턴 매칭을 지원하는 다른 많은 언어가
else 절 추가하기
저희는 몇 가지 이유로 else 절을 추가하지 않기로 했습니다.
- 이미
case _:가 있으므로 중복됩니다. else:의 들여쓰기 수준에 대한 혼란이 영원히 계속될 것입니다. case 목록에 맞춰 정렬해야 합니까, 아니면match키워드에 맞춰 정렬해야 합니까?- “다른 모든 문에는 있다”와 같은 완전성 인자는 틀렸습니다.
else절이 새로운 기능을 추가하는 문에만else절이 있습니다.
보류된 아이디어
매칭 문법을 확장하자는 제안이 여러 개 있었지만, 향후 PEP에서 다룰 수 있도록 보류하기로 했습니다. 이 제안들은 “멋진 아이디어이지만 필수는 아닌” 범주에 속하며, 이 중 일부 제안을 진행하기 전에 match 문이 실제로 어떻게 사용될지에 대한 실사용 데이터를 확보하는 편이 더 나을 수 있다고 판단했습니다.
각 경우 모두 해당 아이디어는 “양방향 문(two-way door)”으로 판단되었다는 점에 유의하십시오. 즉, 나중에 이 기능들을 추가하더라도 하위 호환성 문제가 없어야 합니다.
일회성 문법 변형
제안된 문법으로 가장 큰 이점을 얻을 수 있는 몇몇 코드베이스를 살펴본 결과, 단일 절 매치가 주로 다양한 특수 처리 용도로 비교적 자주 사용될 것으로 나타났습니다. 다른 언어에서는 이를 일회성 매치 형태로 지원합니다. 저희도 이러한 일회성 매치를 지원할 것을 제안했습니다.:
if match value as pattern [and guard]:
...
또는 if 없이 쓸 수도 있습니다:
match value as pattern [if guard]:
...
다음과 같이 전개한 것과 동등한 것으로:
match value:
case pattern [if guard]:
...
이것이 가독성을 얼마나 높이는지 보여 주기 위해, 실제 코드에서 가져온 다음 (약간 단순화한) 코드 조각을 살펴보십시오:
if isinstance(node, CallExpr):
if (isinstance(node.callee, NameExpr) and len(node.args) == 1 and
isinstance(node.args[0], NameExpr)):
call = node.callee.name
arg = node.args[0].name
... # Continue special-casing 'call' and 'arg'
... # Follow with common code
이는 다음과 같이 더 간단하게 다시 쓸 수 있습니다:
if match node as CallExpr(callee=NameExpr(name=call), args=[NameExpr(name=arg)]):
... # Continue special-casing 'call' and 'arg'
... # Follow with common code
이 일회성 형식은 단일 패턴 케이스만 처리하기 위한 것이었으므로 elif match 문을 허용하지 않습니다. 이 형식은 if 문의 특수한 경우가 아니라 match 문의 특수한 경우로 의도되었습니다:
if match value_1 as patter_1 [and guard_1]:
...
elif match value_2 as pattern_2 [and guard_2]: # Not allowed
...
elif match value_3 as pattern_3 [and guard_3]: # Not allowed
...
else: # Also not allowed
...
이는 망라적인 전체 매치를 보완한다는 일회성 매치의 목적에 어긋납니다. 이런 경우에는 전체 매치를 사용하는 것이 더 낫고 명확합니다.
마찬가지로, match ... as ...는 표현식이 아니므로 if not match도 허용되지 않습니다. 또한 패턴 매칭을 지원하는 일부 언어에 있는 while match 구문도 제안하지 않습니다. 편리할 수는 있지만 거의 사용되지 않을 가능성이 높기 때문입니다.
기타 패턴 기반 구문
패턴 매칭을 지원하는 다른 많은 언어는 패턴 매칭을 매칭 연산자, 일반화된 형태의 대입, 루프용 필터, 통신 동기화 방법, 특수화된 if 문 등 여러 언어 구문의 기반으로 사용합니다. 이 중 일부는 첫 번째 초안에 대한 논의에서 언급되었습니다. 또 다른 질문은 왜 다른 형식은 선택하지 않고 하필 이 형식(바인딩과 조건부 선택을 결합한 형식)을 선택했느냐는 것이었습니다.
패턴 사용에 관해 지금까지 쌓은 경험을 고려하면 패턴의 용도를 더 늘리는 것은 지나치게 대담하고 시기상조이며, 이 제안을 너무 복잡하게 만들 것입니다. 여기서 제시한 문은 유용할 만큼 충분히 일반적이면서도 그 자체로 완결된 형태의 기능을 제공하며, 언어 전체의 문법과 의미 체계에 큰 영향을 주지 않습니다.
이 기능을 어느 정도 사용해 본 후에는, 커뮤니티가 Python에서 패턴 매칭을 또 어떤 용도로 쓰면 가치가 있을지 더 잘 판단할 수 있을 것입니다.
반복된 이름의 대수적 매칭
Erlang이나 Elixir 같은 함수형 언어에서 가끔 볼 수 있는 기법은 같은 패턴 안에서 매치 변수를 여러 번 사용하는 것입니다:
match value:
case Point(x, x):
print("Point is on a diagonal!")
여기서의 아이디어는 x가 처음 나타나면 값을 그 이름에 바인딩하고, 이후에 나타나면 들어오는 값이 앞서 바인딩된 값과 같은지 확인한다는 것입니다. 값이 같지 않으면 매치는 실패합니다.
하지만 캡처 패턴에 로드-저장 의미 체계를 섞는 데에는 여러 가지 미묘한 문제가 따릅니다. 당분간은 같은 패턴 안에서 이름을 반복해서 사용하는 것을 오류로 처리하기로 했습니다. 이 제한은 나중에 언제든지 하위 호환성에 영향을 주지 않고 완화할 수 있습니다.
대안 선택지에서는 같은 이름을 두 번 이상 사용할 수 있다는 점에 유의하십시오:
match value:
case x | [x]:
# etc.
사용자 정의 매칭 프로토콜
이 PEP의 초기 설계 논의에서 사용자 정의 매처에 관한 아이디어가 많이 오갔습니다. 여기에는 몇 가지 동기가 있었습니다:
- 일부 클래스는 실제 클래스 프로퍼티와는 다른 “매치 가능한” 이름 집합을 노출하고 싶을 수 있습니다.
- 일부 클래스에는 계산 비용이 큰 프로퍼티가 있을 수 있으므로, 매치 패턴이 실제로 그 프로퍼티에 접근해야 하는 경우가 아니면 평가해서는 안 됩니다.
IsInstance(),InRange(),RegexMatchingGroup()등과 같은 색다른 매처에 대한 아이디어도 있었습니다.- 내장 타입과 표준 라이브러리 클래스가 합리적이고 직관적인 방식으로 매칭을 지원하려면 이러한 타입이 특별한 매칭 로직을 구현해야 한다고 여겨졌습니다.
이러한 사용자 정의 매치 동작은 클래스 이름에 있는 특별한 __match__ 메서드로 제어하도록 되어 있었습니다. 서로 경쟁하는 두 가지 변형이 있었습니다:
- 매칭 대상(subject)뿐만 아니라 지정된 패턴이 어떤 속성에 관심을 두는지에 대한 상세 정보까지 전달하는 ‘완전한 기능을 갖춘’ 매치 프로토콜.
- 대상 값만 전달하고, 매칭 가능한 속성을 담은 “프록시 객체”(대부분의 경우 대상 자체일 수 있음)를 반환하는 단순화된 매치 프로토콜.
다음은 제안되었던 더 복잡한 프로토콜의 한 버전을 보여 주는 예입니다:
match expr:
case BinaryOp(left=Number(value=x), op=op, right=Number(value=y)):
...
from types import PatternObject
BinaryOp.__match__(
(),
{
"left": PatternObject(Number, (), {"value": ...}, -1, False),
"op": ...,
"right": PatternObject(Number, (), {"value": ...}, -1, False),
},
-1,
False,
)
이 프로토콜의 한 가지 단점은 __match__에 전달할 인자를 구성하는 비용이 크고, 이름이 바인딩되는 방식 때문에 Python에는 진정한 상수가 없으므로 인자를 미리 계산할 수도 없다는 점입니다. 또한 이는 원래라면 Python VM의 C 코드로 구현되었을 매칭 로직의 상당 부분을 __match__ 메서드가 다시 구현해야 한다는 것을 의미했습니다. 그 결과, 이 방식은 동등한 if 문에 비해 성능이 떨어졌을 것입니다.
더 단순한 프로토콜은 성능은 더 좋았지만 유연성이 훨씬 떨어졌고, 사람들이 구상하던 창의적인 사용자 정의 매처 중 상당수를 허용하지 못한다는 문제가 있었습니다.
그러나 설계 과정 후반에 이르러, 사용자 정의 매칭 프로토콜의 필요성이 예상보다 훨씬 적다는 사실이 드러났습니다. 제기된 사용 사례 중 (공상적인 것이 아닌) 현실적인 사례는 사실상 모두 내장 매칭 동작으로 처리할 수 있었습니다. 다만 몇몇 경우에는 원하는 효과를 얻기 위해 추가적인 가드 조건이 필요했습니다.
더욱이, 표준 라이브러리 클래스 중 적절한 __match_args__ 프로퍼티 외에 특별한 매칭 지원이 실제로 필요한 클래스는 하나도 없는 것으로 밝혀졌습니다.
이 기능을 미루기로 한 결정의 바탕에는 이것이 되돌릴 수 없는 결정이 아니라는 인식이 있었습니다. 즉, 특히 실제 사용 사례와 실제 사용자 요구에 대한 경험이 더 쌓이면, 더 유연하고 사용자 정의가 가능한 매칭 프로토콜을 나중에 추가할 수 있습니다.
이 PEP의 저자들은 과거에 다른 “다단계” PEP들이 그랬던 것과 비슷한 방식으로, 사용 패턴과 관용구가 발전함에 따라 match 문도 시간이 지나면서 발전할 것으로 예상합니다. 그때가 되면 확장 매칭 문제를 다시 검토할 수 있습니다.
매개변수화된 매칭 문법
(“클래스 인스턴스 매처”라고도 합니다.)
이것은 앞 절에서 언급한 다양한 종류의 사용자 정의 매처를 허용하는 “사용자 정의 매치 클래스” 아이디어의 또 다른 변형입니다. 다만 확장된 매칭 프로토콜을 사용하는 대신, 고유한 문법을 가진 추가 패턴 유형을 도입하여 이를 달성합니다. 이 패턴 유형은 서로 다른 두 가지 매개변수 집합을 받습니다. 하나는 패턴 객체의 생성자에 전달되는 실제 매개변수로 이루어진 집합이고, 다른 하나는 패턴의 바인딩 변수를 나타내는 집합입니다.
이러한 객체의 __match__ 메서드는 무엇이 유효한 매치인지 판단할 때 생성자 매개변수 값을 사용할 수 있습니다.
이를 통해 InRange<0, 6>(value)와 같은 패턴이 가능해지며, 이 패턴은 0..6 범위의 숫자와 매칭되고 매칭된 값을 ‘value’에 대입합니다. 마찬가지로, 정규 표현식 매치 결과(‘match’라는 단어의 다른 의미)에 이름 있는 그룹이 존재하는지 검사하는 패턴도 만들 수 있습니다.
이 아이디어에 대한 지지가 어느 정도 있었지만, 문법을 두고 사소한 논쟁이 많았고(매력적인 선택지가 많지 않습니다) 뚜렷한 합의에 이르지 못했기 때문에, 당분간 이 기능은 이 PEP에 필수적이지 않다고 결정했습니다.
패턴 유틸리티 라이브러리
앞의 두 아이디어에는 유용한 매처를 풍부하게 담은 새로운 Python 표준 라이브러리 모듈이 함께 제공될 것입니다. 그러나 앞 절들에서 제시한 확장 패턴 제안 중 하나를 채택하지 않고서는 그러한 라이브러리를 구현하기가 사실상 불가능하므로, 이 아이디어 역시 보류되었습니다.
감사의 글
이 PEP를 작성하는 여러 단계에서 도움을 주신 다음 분들(그리고 그 밖의 많은 분들)께 감사드립니다:
- Gregory P. Smith
- Jim Jewett
- Mark Shannon
- Nate Lust
- Taine Zhao
버전 이력
- 초기 버전
- 다음을 포함한 대대적인 재작성:
- 사소한 명확화, 문법 및 오타 수정
- 여러 개념의 이름 변경
- 다음을 포함하여 기각된 아이디어에 대한 추가 논의:
- 와일드카드 패턴에
_를 선택한 이유 - OR 패턴에
|를 선택한 이유 - 캡처 변수에 특별한 문법을 사용하지 않기로 한 이유
- 다른 연산이 아닌 이 패턴 매칭 연산을 선택한 이유
- 와일드카드 패턴에
- 예외 및 부수 효과 의미론 명확화
- 부분 바인딩 의미론 명확화
- 로드 컨텍스트에서
_사용에 대한 제한 삭제 - 소수의 내장 타입을 제외하고, 단일 위치 인자가 기본적으로 대상 전체가 되는 동작 삭제
__match_args__의 동작 단순화__match__프로토콜 삭제(Deferred Ideas로 이동)ImpossibleMatchError예외 삭제- 로드를 위한 선행 점 삭제(Deferred Ideas로 이동)
- 초기 섹션 재작성(구문 이전의 모든 내용)
- 상세 설명 앞에 모든 패턴 종류에 대한 개요 추가
- 각 패턴의 설명 옆에 간소화된 문법을 추가했습니다
- 와일드카드에 대한 설명을 캡처 패턴과 분리
- Daniel F Moisset을 여섯 번째 공동 저자로 추가
참고 문헌
부록 A – 전체 문법
다음은 match_stmt의 전체 문법입니다. 이것은 compound_stmt의 추가 대안입니다. match와 case는 소프트 키워드라는 점, 즉 다른 문법적 맥락에서는 예약어가 아니라는 점을 이해해야 합니다(콜론이 와야 할 위치에 콜론이 없는 경우의 줄 시작 부분도 포함합니다). 관례에 따라 하드 키워드에는 작은따옴표를, 소프트 키워드에는 큰따옴표를 사용합니다.
표준 EBNF 외에 사용되는 기타 표기법은 다음과 같습니다.
SEP.RULE+는RULE (SEP RULE)*의 축약형입니다.!RULE은 부정 전방 탐색 단언입니다
match_expr:
| star_named_expression ',' star_named_expressions?
| named_expression
match_stmt: "match" match_expr ':' NEWLINE INDENT case_block+ DEDENT
case_block: "case" patterns [guard] ':' block
guard: 'if' named_expression
patterns: value_pattern ',' [values_pattern] | pattern
pattern: walrus_pattern | or_pattern
walrus_pattern: NAME ':=' or_pattern
or_pattern: '|'.closed_pattern+
closed_pattern:
| capture_pattern
| literal_pattern
| constant_pattern
| group_pattern
| sequence_pattern
| mapping_pattern
| class_pattern
capture_pattern: NAME !('.' | '(' | '=')
literal_pattern:
| signed_number !('+' | '-')
| signed_number '+' NUMBER
| signed_number '-' NUMBER
| strings
| 'None'
| 'True'
| 'False'
constant_pattern: attr !('.' | '(' | '=')
group_pattern: '(' patterns ')'
sequence_pattern: '[' [values_pattern] ']' | '(' ')'
mapping_pattern: '{' items_pattern? '}'
class_pattern:
| name_or_attr '(' ')'
| name_or_attr '(' ','.pattern+ ','? ')'
| name_or_attr '(' ','.keyword_pattern+ ','? ')'
| name_or_attr '(' ','.pattern+ ',' ','.keyword_pattern+ ','? ')'
signed_number: NUMBER | '-' NUMBER
attr: name_or_attr '.' NAME
name_or_attr: attr | NAME
values_pattern: ','.value_pattern+ ','?
items_pattern: ','.key_value_pattern+ ','?
keyword_pattern: NAME '=' or_pattern
value_pattern: '*' capture_pattern | pattern
key_value_pattern:
| (literal_pattern | constant_pattern) ':' or_pattern
| '**' capture_pattern
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.