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대신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문은 다음을 포함합니다.
- subject 표현식
- 하나 이상의
case절
각 case 절은 다음을 지정합니다.
- 패턴(일치시킬 전체 형태)
- 선택적 “guard”(패턴이 일치할 경우 확인할 조건)
- 해당 case 절이 선택되었을 때 실행할 코드 블록
동기
이 PEP의 나머지 부분에서는 다음을 다룹니다.
- 패턴 매칭이 Python에 유용한 추가 기능이라고 생각하는 이유를 설명합니다.
- 설계 선택을 설명합니다.
- 정확한 구문 및 런타임 사양을 포함합니다.
- 정적 타입 검사기를 위한 지침을 제공하고
typing모듈에 대한 작은 추가 사항 하나를 제시합니다. - 제안에 대한 광범위한 논의에서 제기된 주요 반론과 대안을 논의하며, 이는 저자 그룹 내부와 python-dev 커뮤니티 모두에서 이루어진 논의를 포함합니다.
마지막으로, 커뮤니티가 현재 제안된 구문과 의미론을 충분히 경험한 후 향후 고려할 수 있는 몇 가지 확장 기능을 논의합니다.
개요
패턴은 고유한 규칙과 특수 사례를 갖는 새로운 구문 범주입니다. 패턴은 입력(주어진 값)과 출력(캡처된 변수)을 새로운 방식으로 혼합합니다. 효과적으로 사용하는 데 약간의 시간이 걸릴 수 있습니다. 저자들은 여기에서 기본 개념을 간략하게 소개합니다. 이 절은 완전하거나 전적으로 정확하도록 작성된 것이 아님을 유의하십시오.
패턴, 새로운 구문 구성체 및 구조 분해
이 PEP에서는 패턴이라고 하는 새로운 구문 구성체를 도입합니다. 구문상 패턴은 표현식의 하위 집합처럼 보입니다. 다음은 패턴의 예입니다.
[first, second, *rest]Point2d(x, 0){"name": "Bruce", "age": age}42
위의 표현식은 일부 값을 매개변수로 받아 해당 구성 요소로 객체를 만드는 생성자를 사용한 객체 구성의 예처럼 보일 수 있습니다.
패턴으로 보면 위 패턴은 구성의 역연산을 의미하며, 이를 구조 분해라고 부릅니다. 구조 분해는 대상 값을 받아 그 구성 요소를 추출합니다.
객체 구성과 구조 분해 사이의 구문적 유사성은 의도된 것입니다. 또한 할당 대상(쓰기 컨텍스트)이 표현식(읽기 컨텍스트)처럼 보이게 하는 기존 Python식 스타일의 컨텍스트를 따릅니다.
패턴 매칭은 객체를 생성하지 않습니다. 이는 [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 방식으로 작성된 이러한 작업의 한 예이지만, 매칭을 사용하면 작성 과정이 훨씬 덜 번거로워집니다.
이를 수행하는 코드의 상당 부분은 중첩된 if/elif 문의 복잡한 연쇄로 구성되는 경향이 있으며, 여기에는 len(), isinstance()의 여러 호출과 인덱스/키/속성 접근이 포함됩니다. 이러한 분기 내부에서 사용자는 필요한 구성 요소 값을 추출하기 위해 데이터를 더 세부적으로 구조 분해해야 할 때가 있으며, 해당 값은 여러 객체 깊숙이 중첩되어 있을 수 있습니다.
다른 많은 언어에 존재하는 패턴 매칭은 이 문제에 우아한 해결책을 제공합니다. 이러한 언어에는 F# 및 Haskell과 같은 정적으로 컴파일되는 함수형 언어부터 Scala와 Rust와 같은 다중 패러다임 언어, Elixir 및 Ruby와 같은 동적 언어까지 포함되며, JavaScript에도 도입하는 방안이 검토되고 있습니다. Python식 패턴 매칭으로 나아가는 길을 제시해 준 이러한 언어들에 감사하고 있습니다. Python 역시 많은 기능에 대해 여러 다른 언어의 도움을 받았습니다. 많은 기본 구문 기능은 C에서 계승되었고, 예외는 Modula-3에서, 클래스는 C++에서 영감을 받았으며, 슬라이싱은 Icon에서, 정규 표현식은 Perl에서 유래했고, 데코레이터는 Java 어노테이션과 유사한 식으로 구현되었으며, 그 밖에도 많은 예가 있습니다.
이기종 데이터에 대한 일반적인 연산 논리는 다음과 같이 요약할 수 있습니다.
- 데이터의 shape (형식 및 구성 요소)에 대해 일부 분석이 수행됩니다. 여기에는
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 구문은 리스트와 튜플뿐만 아니라 사용자 정의 시퀀스도 허용한다는 점에 유의하십시오.)
어떤 경우에는 정보를 추출하는 것보다 구조를 식별하는 것이 더 중요합니다. 다음 Python 표준 라이브러리의 예를 살펴보십시오.:
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 == ")")
이 예는 중요한 추출 작업을 수행하지 않고 데이터의 “shape”를 알아내는 방법을 보여 줍니다. 이 코드는 읽기가 그다지 쉽지 않으며, 일치시키려는 의도된 형태도 명확하지 않습니다. 제안된 구문을 사용하여 업데이트한 코드와 비교해 보십시오.:
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")
이것도 작동하기는 하지만 제안의 초점이 반드시 여기에 있는 것은 아니며, 새로운 구문은 구조 분해 시나리오를 최대한 지원하도록 설계되었습니다.
더 자세한 사양은 아래의 syntax 섹션을 참조하십시오.
객체의 구조 분해를 새로운 특수 __match_args__속성으로 사용자 지정할 수 있도록 제안합니다. 이 PEP의 일부로, 일부 표준 라이브러리 클래스(명명된 튜플과 데이터클래스를 포함)에 대한 일반 API와 그 구현을 명시합니다. 아래의 runtime 섹션을 참조하십시오.
마지막으로, 정적 타입 검사기와 유사한 도구를 포괄적으로 지원하는 것을 목표로 합니다. 이를 위해 런타임에서는 아무 작업도 수행하지 않지만, 이 클래스의 모든 서브클래스가 동일한 모듈에 정의되어야 함을 정적 도구에 나타내는 @typing.sealed 클래스 데코레이터를 도입할 것을 제안합니다. 이를 통해 효과적인 정적 완전성 검사가 가능해지며, 데이터클래스와 함께 algebraic data types에 대한 기본적인 지원을 제공할 수 있습니다. 자세한 내용은 static checkers 섹션을 참조하십시오.
구문 및 의미론
패턴
pattern은 새로운 구문 구성체이며, 할당 대상의 느슨한 일반화로 볼 수 있습니다. 패턴의 핵심 특성은 패턴이 허용하는 대상의 유형과 형태, 캡처하는 변수, 그리고 대상에서 해당 변수들을 추출하는 방식입니다. 예를 들어, [a, b]패턴은 정확히 2개의 요소로 이루어진 시퀀스만 일치시키며, 첫 번째 요소는 a로, 두 번째 요소는 b로 추출합니다.
이 PEP는 여러 유형의 패턴을 정의합니다. 물론 이것들이 가능한 유일한 패턴은 아니므로, 현재 유용하면서도 보수적인 기능의 일부를 선택하도록 설계 결정이 이루어졌습니다. 이 기능이 더 널리 사용되면 나중에 더 많은 패턴을 추가할 수 있습니다. 자세한 내용은 Rejected Ideas 및 Deferred Ideas 섹션을 참조하십시오.
여기 나열된 패턴은 아래에서 더 자세히 설명하지만, 간단히 하기 위해 이 섹션에서 함께 요약합니다.
- literal pattern은 구조에서 상수 값을 필터링하는 데 유용합니다. 이는 Python 리터럴처럼 보이며
True,False및None과 같은 일부 값도 포함합니다. 리터럴과 같은 객체만 일치시키며, 결코 바인딩하지 않습니다. - capture pattern은
x처럼 보이며 동일한 할당 대상과 동등합니다. 즉, 항상 일치하고 주어진 (단순한) 이름의 변수에 바인딩합니다. - wildcard pattern은 하나의 밑줄인
_입니다. 항상 일치하지만 어떤 변수도 캡처하지 않습니다. 이는_의 다른 사용과의 간섭을 방지하고 일부 최적화를 가능하게 합니다. - constant value pattern은 특정 이름이 지정된 상수에 대해 리터럴과 같은 방식으로 작동합니다. 캡처 패턴과 혼동될 가능성이 있으므로, 정규화된(점으로 구분된) 이름이어야 한다는 점에 유의하십시오.
Color.RED처럼 보이며 해당 값과 같은 값만 일치시킵니다. 결코 바인딩하지 않습니다. - sequence pattern은
[a, *rest, b]처럼 보이며 리스트 언패킹과 유사합니다. 중요한 차이점은 그 안에 중첩된 요소가 이름이나 시퀀스뿐 아니라 어떤 종류의 패턴이든 될 수 있다는 점입니다. 모든 하위 패턴도 일치하는 경우에만 적절한 길이의 시퀀스와 일치합니다. 하위 패턴의 모든 바인딩을 생성합니다. - 매핑 패턴은
{"user": u, "emails": [*es]}과 같은 형태입니다. 제공된 키 집합을 최소한 포함하는 매핑과 일치하며, 모든 하위 패턴이 해당 값과 일치하는 경우에 일치합니다. 키에 대응하는 값과 일치시키는 동안 하위 패턴이 생성하는 모든 바인딩을 생성합니다. 추가 항목을 캡처하도록 패턴 끝에**rest를 추가할 수 있습니다. - class pattern은 앞의 패턴과 비슷하지만 키 대신 속성과 일치합니다.
datetime.date(year=y, day=d)처럼 보입니다. 지정된 속성을 최소한 갖고, 해당 속성이 대응하는 하위 패턴과 일치하는 지정된 타입의 인스턴스와 일치합니다. 주어진 속성의 값과 일치시킬 때 하위 패턴이 생성하는 모든 바인딩을 생성합니다. 선택적 프로토콜을 사용하면 위치 인자와도 일치시킬 수 있습니다. - OR pattern은
[*x] | {"elems": [*x]}처럼 보입니다. 하위 패턴 중 하나라도 일치하면 일치합니다. 일치한 가장 왼쪽 패턴의 바인딩을 사용합니다. - walrus pattern은
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
완전하고 축약되지 않은 문법은 Appendix A에서 확인하십시오. 이 절의 단순화된 문법은 완전한 명세가 아니라 독자의 이해를 돕기 위한 것입니다.
match 연산은 식이 아니라 문이어야 한다고 제안합니다. 많은 언어에서는 이것이 식이지만, 문으로 하는 편이 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 경우는 없습니다. 대신 특수 와일드카드 _를 최종 ‘모두 포괄하는’ 패턴으로 사용할 수 있습니다(capture_pattern섹션 참조).
성공적인 패턴 매칭 중에 생성된 이름 바인딩은 실행된 스위트가 종료된 후에도 유지되며 match 문 이후에 사용할 수 있습니다. 이는 이름을 바인딩할 수 있는 다른 Python 문, 예를 들어 for 루프와 with 문에도 적용되는 논리를 따릅니다. 예를 들어:
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-문자열은 허용되지 않습니다(일반적으로 실제 리터럴이 아니기 때문입니다).
캡처 패턴
간략한 구문:
capture_pattern: NAME
캡처 패턴은 일치하는 표현식의 할당 대상 역할을 합니다.:
match greeting:
case "":
print("Hello!")
case name:
print(f"Hi {name}!")
단일 이름만 허용됩니다(점으로 구분된 이름은 상수 값 패턴입니다). 캡처 패턴은 항상 성공합니다. 스코프에 캡처 패턴이 나타나면 해당 이름은 그 스코프의 지역 이름이 됩니다. 예를 들어, 위 코드 조각 이후에 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
각 케이스 절과 일치시키는 동안 이름은 최대 한 번만 바인딩될 수 있으므로, 이름이 겹치는 두 캡처 패턴을 사용하면 오류입니다.:
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)+
상수 및 열거형 값과 일치시키는 데 사용됩니다. 패턴의 모든 점으로 구분된 이름은 일반적인 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)의 인스턴스가 아니면 매칭이 실패합니다. 그렇지 않으면 매칭이 계속됩니다(runtime 섹션의 세부 사항을 참조하십시오).
지정된 클래스는 type을 상속해야 합니다. 단일 이름 또는 점으로 구분된 이름(예: some_mod.SomeClass 또는 mod.pkg.Class)일 수 있습니다. 선행 이름은 _이어서는 안 되므로, 예를 들어 _(...) 및 _.C(...)는 유효하지 않습니다. 일치한 객체에 foo속성이 있는지 확인하려면 object(foo=_)를 사용하십시오.
기본적으로 사용자 정의 클래스의 하위 패턴은 키워드로만 매칭할 수 있습니다. 위치 하위 패턴을 지원하려면 사용자 정의 __match_args__ 속성이 필요합니다. 런타임은 모든 인스턴스 검사와 속성 조회를 적절하게 연결하여 임의로 중첩된 패턴과의 매칭을 허용합니다.
여러 패턴 결합(OR 패턴)
여러 대안 패턴을 |를 사용하여 하나로 결합할 수 있습니다. 이는 하나 이상의 대안이 일치하면 전체 패턴이 일치한다는 의미입니다. 대안은 왼쪽에서 오른쪽 순서로 시도되며 단락 평가 특성이 있으므로, 하나가 일치하면 이후 패턴은 시도하지 않습니다. 예제입니다.:
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'
...
가드
각 top-level 패턴 뒤에는 if expression형식의 guard가 올 수 있습니다. 패턴이 일치하고 가드가 참인 값으로 평가되면 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의 클래스 객체를 조회하고, 일치시키는 값인obj를 대상으로isinstance(obj, Class)를 호출합니다. 거짓이면 매칭이 실패합니다.- 그렇지 않은 경우, 위치 인자 또는 키워드 인자 형식으로 하위 패턴이 주어지면 다음과 같이 왼쪽에서 오른쪽으로 매칭됩니다. 하위 패턴이 실패하는 즉시 매칭이 실패하며, 모든 하위 패턴이 성공하면 전체 클래스 패턴 매칭이 성공합니다.
- 위치로 매칭하는 항목이 있고 클래스에
__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__를 사용하는 방식으로 대체하지 않습니다 – “모호한 상황에서는 추측하려는 유혹을 거부하십시오.” - 키워드로 매칭하는 항목이 있으면 키워드는 대상의 특성으로 조회됩니다. 조회에 성공하면 해당 값이 대응하는 하위 패턴과 매칭됩니다. 조회에 실패하면 매칭이 실패합니다.
이러한 프로토콜은 유연성 및 성능보다 구현의 단순성을 우선합니다. 고려된 다른 대안은 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가 해당하는 경우입니다. 이 경우에는 단순히 매칭 실패로 처리되고 문장의 나머지 부분은 정상적으로 진행됩니다.
매칭 과정에서 명시적으로 전달되는 유일한 부수 효과는 이름의 바인딩입니다. 그러나 이 과정은 대상과 그 일부 구성 요소에 대한 속성 접근, 인스턴스 검사, len(), 동등성 비교 및 항목 접근에 의존합니다. 또한 상수 값 패턴과 클래스 패턴의 왼쪽 부분을 평가합니다. 일반적으로 이러한 작업은 부수 효과를 일으키지 않지만, 이러한 객체 중 일부는 부수 효과를 일으킬 수 있습니다. 이 제안에서는 어떤 메서드가 호출되는지 또는 몇 번 호출되는지에 대한 사양을 의도적으로 제외합니다. 해당 동작에 의존하는 사용자 코드는 버그가 있는 것으로 간주해야 합니다.
표준 라이브러리
패턴 매칭의 사용을 용이하게 하기 위해 표준 라이브러리에 몇 가지 변경이 이루어집니다.
- 네임드튜플과 데이터클래스에는
__match_args__가 자동으로 생성됩니다. - 데이터클래스의 경우 생성된
__match_args__에서 속성의 순서는 생성된__init__()메서드에서 해당 인자의 순서와 동일합니다. 여기에는 슈퍼클래스에서 속성을 상속하는 경우도 포함됩니다.
또한 기존 표준 라이브러리 클래스를 체계적으로 검토하여 유용해 보이는 경우 __match_args__를 추가하는 작업이 진행됩니다.
정적 검사기 사양
완전성 검사
신뢰성 측면에서 보면, 가능한 데이터 값의 집합을 다룰 때 하나의 케이스를 빠뜨리면 디버깅하기 어려운 문제가 발생한다는 사실이 경험을 통해 드러났습니다. 따라서 사람들은 다음과 같은 안전성 어설션을 추가하게 됩니다.:
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에서는 정적 타입 검사기가 열거형 값과 관련된 조건 검사에서 완전성 검사를 지원해야 한다고 규정합니다. 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.
패턴 매칭과 열거형 값을 함께 사용하는 경우에도 완전성 검사를 적용해야 합니다.:
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
분명히 Matchable 프로토콜은 필요하지 않습니다(PEP 544의 관점에서 볼 때). 모든 클래스는 매칭 가능하므로 위에서 지정한 검사 대상이 되기 때문입니다.
대수적 데이터 타입으로서의 봉인된 클래스
애드혹 유니언 타입을 정의하지 않고 클래스 집합에 완전성 검사를 적용하는 것이 바람직한 경우가 상당히 많습니다. 유니언 정의에서 하나의 클래스가 빠지면 애드혹 유니언 타입 자체도 취약하기 때문입니다. 레코드와 유사한 클래스 그룹을 유니언으로 결합하는 설계 패턴은 패턴 매칭을 지원하는 다른 언어에서 널리 사용되며, algebraic data types라는 이름으로 알려져 있습니다.
런타임에는 아무런 효과가 없지만, 이 클래스의 모든 서브클래스(직접 서브클래스와 간접 서브클래스)를 베이스 클래스와 동일한 모듈에 정의해야 한다는 사실을 정적 타입 검사기에 나타내는 특수 데코레이터 클래스 @sealed를 typing 모듈에 추가할 것을 제안합니다.
모든 서브클래스를 알고 있으므로 타입 검사기가 봉인된 베이스 클래스를 모든 서브클래스의 유니언으로 취급할 수 있다는 것이 이 아이디어의 핵심입니다. 데이터클래스와 함께 사용하면 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 메시지를 생성한다는 점에 유의하십시오.
별표 매칭의 정밀한 타입 검사
타입 검사기는 패턴 매칭에서 별표 항목에 대해 정밀한 타입 검사를 수행하여, PEP 589에 지정된 이질적인 list[T] 타입 또는 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 문을 인식하도록 업데이트해야 합니다.
일반적으로 이러한 도구는 두 가지 범주 중 하나에 속합니다.
Shallow 파서는 Python의 전체 구문을 이해하려고 하지 않고, 대신 소스 코드에서 알려진 특정 패턴을 검색합니다. Visual Studio Code, Emacs 및 TextMate와 같은 IDE는 편집 중인 소스 코드가 유효하지 않은 경우가 많고 엄격한 구문 분석 방식은 실패할 수 있으므로 대체로 이 범주에 속합니다.
이러한 종류의 도구에서는 새 키워드에 대한 지식을 추가하는 일이 비교적 쉽습니다. 단순히 테이블에 항목을 추가하거나 정규 표현식을 수정하면 됩니다.
Deep 파서는 Python의 전체 구문을 이해합니다. 이러한 파서의 예로 자동 포매터 Black_이 있습니다. 이러한 종류의 도구에 필요한 특별한 조건은 현재 Python 버전의 구문뿐만 아니라 이전 Python 버전의 구문도 이해해야 한다는 점입니다.
match 문은 소프트 키워드를 사용하며, 새로운 PEG 파서의 기능을 활용한 최초의 주요 Python 기능 중 하나입니다. 이는 ‘PEG-compatible’가 아닌 서드파티 파서가 새로운 구문을 처리하는 데 어려움을 겪게 된다는 의미입니다.
이러한 서드파티 도구 중 상당수가 공통 구문 분석 라이브러리를 활용한다는 점이 지적되었습니다(예를 들어 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 문에 새로운 case 절을 추가해야 합니다. 따라서 이러한 상황에서는 match를 사용하지 않는 것이 좋습니다.
대신 더 유연한 대입 대상을 허용하십시오.
새로운 종류의 문을 추가하는 대신 반복 가능한 객체 언패킹을 훨씬 더 일반적인 대입 대상으로 일반화하자는 아이디어가 있었습니다. 이 개념은 일부 다른 언어에서 “반박 불가능한 매치”라고 알려져 있습니다. 실제 잠재적 사용 사례를 조사한 결과 대다수의 경우 구조 분해가 if 조건과 관련되어 있었기 때문에 이렇게 하지 않기로 했습니다. 또한 그중 많은 경우가 배타적인 선택의 연속으로 묶여 있습니다.
표현식으로 만드십시오.
대부분의 다른 언어에서 패턴 매칭은 문이 아니라 표현식으로 표현됩니다. 그러나 이를 표현식으로 만들면 Python의 다른 구문적 선택과 일관되지 않습니다. 모든 의사 결정 논리는 거의 전적으로 문으로 표현되므로, 여기서 벗어나지 않기로 했습니다.
하드 키워드를 사용하십시오.
match를 하드 키워드로 만들거나 다른 키워드를 선택하는 방안이 있었습니다. 하드 키워드를 사용하면 단순한 구문 하이라이터의 작업이 간단해지겠지만, 다음과 같은 몇 가지 이유로 하드 키워드를 사용하지 않기로 결정했습니다.
- 무엇보다도 새 파서에서는 이렇게 할 필요가 없습니다. 몇 번의 릴리스 동안 소프트 키워드로 사용되면서 어려움을 초래했던
async와 달리, 여기서는match를 영구적인 소프트 키워드로 만들 수 있습니다. match는 기존 코드에서 매우 흔하게 사용되므로, 거의 모든 기존 프로그램이 중단되고 새 구문의 혜택을 받지 못할 수도 있는 많은 사람이 코드를 수정해야 하는 부담을 지게 됩니다.- 기존 프로그램에서 식별자로 흔히 사용되지 않으면서도 문의 의미를 명확하게 반영할 수 있는 대체 키워드를 찾기는 어렵습니다.
케이스 절에는 case대신 as나 |를 사용하십시오.
여기서 제안하는 패턴 매칭은 다중 분기 제어 흐름(알골에서 파생된 언어의 switch나 Lisp의 cond와 같은 방식)과 함수형 언어에서 볼 수 있는 객체 분해를 결합한 것입니다. 제안된 키워드 case는 다중 분기 측면을 강조하지만, as와 같은 대체 키워드도 분해 측면을 강조하면서 똑같이 사용할 수 있습니다. 예를 들어 as나 with는 이미 파이썬의 키워드라는 장점도 있습니다. 그러나 키워드로서의 case는 match문 내부에서 선행 키워드로만 나타날 수 있으므로, 파서가 키워드로 사용되었는지 변수로 사용되었는지를 쉽게 구분할 수 있습니다.
다른 변형에서는 |나 =>와 같은 기호를 사용하거나, 특수 표식을 전혀 사용하지 않습니다.
파이썬은 알골의 전통을 따르는 문장 지향 언어이고 각 복합 문장이 식별 키워드로 시작하므로, case가 파이썬의 스타일과 전통에 가장 잘 부합하는 것으로 보였습니다.
평면 들여쓰기 방식을 사용하십시오.
대체 들여쓰기 방식을 사용하자는 아이디어가 있었습니다. 예를 들어 각 케이스 절을 처음의 match부분에 비해 들여쓰지 않는 방식입니다.:
match expression:
case pattern_1:
...
case pattern_2:
...
그 근거는 평면 들여쓰기가 가로 공간을 어느 정도 절약하기는 하지만, 다른 모든 곳에서는 콜론 뒤에 들여쓰기가 오기 때문에 파이썬 프로그래머의 눈에는 어색하게 보일 수 있다는 것입니다. 이는 단순한 코드 편집기의 작업도 복잡하게 만듭니다. 마지막으로, match 문에서 “반쪽 들여쓰기”(즉, 네 칸 대신 두 칸)를 허용하면 가로 공간 문제를 완화할 수 있습니다.
이 PEP를 작성하는 과정에서 작성한 match사용 샘플 프로그램에서는 코드 간결성이 눈에 띄게 향상되었으며, 추가된 들여쓰기 수준을 상쇄하고도 남았습니다.
검토된 또 다른 제안은 평면 들여쓰기를 사용하되 match: 다음 줄에 표현식을 배치하는 방식이었습니다. 예를 들면 다음과 같습니다.:
match:
expression
case pattern_1:
...
case pattern_2:
...
이는 첫 번째 블록이 파이썬 문법에서 새로운 형태가 되기 때문에 최종적으로 거부되었습니다. 즉, 문장 시퀀스가 아니라 단일 표현식만을 내용으로 하는 블록이 되기 때문입니다.
상수 값 패턴의 대안
이는 아마도 가장 까다로운 항목일 것입니다. 미리 정의된 일부 상수와 매칭하는 것은 매우 흔하지만, 파이썬의 동적인 특성 때문에 캡처 패턴과 모호해지기도 합니다. 다섯 가지 다른 대안을 검토했습니다.
- 몇 가지 암시적 규칙을 사용합니다. 예를 들어 이름이 전역 범위에서 정의되어 있다면 캡처 패턴을 나타내는 대신 상수를 참조하도록 합니다.:
# 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 문 앞에서 관련 없는 동일한 이름을 정의하면 예기치 않은 결과와 연쇄적인 영향을 초래할 수 있습니다.
- 이름의 대소문자에 기반한 규칙을 사용합니다. 특히 이름이 소문자로 시작하면 캡처 패턴으로 간주하고, 대문자로 시작하면 상수를 참조하도록 합니다.:
match entree[-1]: case SPAM: ... # Compares entree[-1] == SPAM. case side: ... # Assigns side = entree[-1].
이는 상수 이름 지정에 관한 PEP 8의 권장 사항과 잘 맞습니다. 가장 큰 반론은 파이썬 핵심부의 다른 어떤 부분에서도 이름의 대소문자가 의미론적으로 중요하지 않다는 것입니다. 또한 파이썬에서는 식별자에 서로 다른 문자 체계를 사용할 수 있으며, 그중 상당수(예: 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에서는 일반적으로 불가능합니다.
범위 전용 구문을 만드는 대신, 사용자 지정 패턴 객체(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를 제외한 모든 것과 매칭합니다.
이 기능은 거의 유용하지 않다는 documented evidence가 있고(이를 지원하는 언어에서), 변수 범위를 제어하고 변수 바인딩을 방지하기 위해 이중 부정 !!로 사용되기도 하기 때문에 기각되었습니다(이는 Python에는 적용되지 않습니다). 가드 조건을 사용하여 이를 시뮬레이션할 수도 있습니다.
런타임에 완전성 검사
어떤 case 절에도 매칭되는 패턴이 없고 기본 case도 없을 때 어떻게 해야 하는지가 문제입니다. 제안의 초기 버전에서는 이 경우 조용히 다음으로 넘어가는 대신 예외를 발생시키도록 동작을 명시했습니다.
찬반 논의는 많았지만, 결국 EIBTI(Explicit Is Better Than Implicit, 명시적인 것이 암시적인 것보다 낫다) 주장이 채택되었습니다. 프로그래머가 원하는 동작이 예외 발생이라면 예외를 명시적으로 발생시키도록 하는 편이 더 낫습니다.
봉인된 클래스와 열거형처럼 패턴이 모두 이산 집합의 멤버로 알려진 경우 static checkers는 누락된 패턴에 대해 경고할 수 있습니다.
패턴 변수에 대한 타입 어노테이션
제안된 내용은 패턴을 타입 어노테이션과 결합하는 것이었습니다.:
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 401: case 403: case 404: print("Some HTTP error")
이는 case에 대한 C의 폴스루 의미론을 사용하여 수행하는 방식입니다. 그러나
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를 매우 자주 사용하므로, 이것이 불리언 단락 평가와 강하게 연관되어 있다는 인상을 받을 수 있습니다. - 정규 표현식과 EBNF 문법(Python 자체의 문법과 같은)에서는 대안 사이에
|를 사용합니다. |는 비트 단위 OR에만 사용되는 것이 아니라 집합 합집합, 딕셔너리 병합(PEP 584)에도 사용되며,typing.Union의 대안으로 고려되고 있습니다(PEP 604).|는 특히 문자열 사이에서 시각적 구분 기호로 더 잘 작동합니다. 비교하십시오.:case "spam" or "eggs" or "cheese":
다음과:
case "spam" | "eggs" | "cheese":
- 패턴 매칭을 지원하는 다른 많은 언어에서도
else절을 추가하십시오.
여러 가지 이유로 else절을 추가하지 않기로 결정했습니다.
- 이미
case _:를 가지고 있으므로 중복됩니다. else:의 들여쓰기 수준에 대해서는 언제까지나 혼란이 있을 것입니다. case 목록에 맞춰야 합니까, 아니면match키워드에 맞춰야 합니까?- “다른 모든 문에도 하나씩 있다”와 같은 완전성 추구의 주장은 사실이 아닙니다. 새로운 기능을 추가하는 경우에만 해당 문에
else절이 있습니다.
보류된 아이디어
매칭 문법을 확장하자는 여러 제안이 있었지만, 향후 PEP에서 다룰 가능성을 위해 연기하기로 결정했습니다. 이러한 제안은 “멋진 아이디어이지만 필수적이지는 않음”의 영역에 속하며, 일부 제안을 진행하기 전에 match 문이 실제로 어떻게 사용될지에 대한 실세계 데이터를 확보하는 편이 더 나을 수 있다고 판단했습니다.
각 경우에 이 아이디어는 “양방향 문”으로 판단되었다는 점에 유의하십시오. 즉, 나중에 이러한 기능을 추가하더라도 하위 호환성 문제가 없어야 한다는 의미입니다.
일회성 문법 변형
제안된 문법의 혜택을 가장 크게 받을 수 있는 일부 코드베이스를 살펴보는 동안, 단일 절 매치가 비교적 자주 사용될 것이며 대부분 다양한 특수 처리에 사용될 것이라는 사실이 밝혀졌습니다. 다른 언어에서는 이러한 기능을 일회성 매치의 형태로 지원합니다. 이러한 일회성 매치도 지원하자고 제안했습니다.:
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
...
이렇게 하면 완전한 전체 매치를 보완하는 일회성 매치의 목적이 무색해집니다. 이 경우에는 전체 매치를 사용하는 편이 더 낫고 명확합니다.
마찬가지로 if not match도 허용되지 않습니다. match ... as ...는 표현식이 아니기 때문입니다. 패턴 매칭을 지원하는 일부 언어에 존재하는 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__ 메서드로 제어됩니다. 서로 경쟁하는 두 가지 변형이 있었습니다:
- ‘모든 기능을 갖춘’ 매칭 프로토콜로, 매칭할 대상뿐만 아니라 지정된 패턴이 관심을 두는 속성에 대한 자세한 정보도 전달합니다.
- 대상 값만 전달하고, 매칭 가능한 속성을 포함하는 “프록시 객체”(대부분의 경우 대상 자체일 수 있음)를 반환하는 단순화된 매칭 프로토콜입니다.
다음은 제안된 더 복잡한 프로토콜의 한 버전 예입니다.:
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에는 실제 상수가 존재하지 않으므로 미리 계산할 수도 없다는 점입니다. 또한 __match__ 메서드가 Python VM에서 C 코드로 구현되었을 매칭 로직의 상당 부분을 다시 구현해야 한다는 의미이기도 합니다. 그 결과 이 옵션은 이에 대응하는 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로 이동함)
- 초기 섹션을 다시 작성했습니다(syntax 이전의 모든 내용)
- 자세한 설명에 앞서 모든 패턴 유형의 개요를 추가했습니다
- 각 패턴 설명 옆에 간소화된 구문을 추가했습니다
- 와일드카드와 캡처 패턴의 설명을 분리했습니다
- 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.