Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 635 – 구조적 패턴 매칭: 동기와 근거

Author:
Tobias Kohn <kohnt at tobiaskohn.ch>, Guido van Rossum <guido at python.org>
BDFL-Delegate:

Discussions-To:
Python-Dev list
Status:
Final
Type:
Informational
Created:
12-Sep-2020
Python-Version:
3.10
Post-History:
22-Oct-2020, 08-Feb-2021
Resolution:
Python-Committers message

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

이 PEP는 PEP 634(“구조적 패턴 매칭: 사양”)에 대한 동기와 근거를 제공합니다. 처음 읽는 독자는 패턴의 개념, 구문 및 의미를 보다 쉽게 소개하는 PEP 636부터 시작하는 것이 좋습니다.

동기

(구조적) 패턴 매칭 구문은 Haskell, Erlang, Scala부터 Elixir와 Ruby에 이르기까지 여러 언어에서 사용됩니다. (JavaScript에 대한 제안도 검토 중입니다.)

Python은 이미 시퀀스 언패킹 할당을 통해 이를 제한적인 형태로 지원하며, 새로운 제안은 이를 활용합니다.

그 밖에도 몇 가지 일반적인 Python 관용구가 관련됩니다.

  • if ... elif ... elif ... else 관용구는 isinstance(x, cls), hasattr(x, "attr"), len(x) == n 또는 "key" in x와 같은 하나 이상의 검사를 가드로 사용하여, 임시방편으로 객체의 유형이나 형태를 알아내고 적용 가능한 블록을 선택하는 데 자주 사용됩니다. 그러면 해당 블록에서는 x가 가드로 검사한 인터페이스를 지원한다고 간주할 수 있습니다. 예를 들어 다음과 같습니다.:
    if isinstance(x, tuple) and len(x) == 2:
        host, port = x
        mode = "http"
    elif isinstance(x, tuple) and len(x) == 3:
        host, port, mode = x
    # Etc.
    

    다음과 같은 코드는 match를 사용하면 더 우아하게 표현할 수 있습니다.:

    match x:
        case host, port:
            mode = "http"
        case host, port, mode:
            pass
        # Etc.
    
  • AST 순회 코드는 주어진 패턴에 일치하는 노드를 찾는 경우가 많으며, 예를 들어 “A + B * C” 형태의 노드를 감지하는 코드는 다음과 같을 수 있습니다.:
    if (isinstance(node, BinOp) and node.op == "+"
            and isinstance(node.right, BinOp) and node.right.op == "*"):
        a, b, c = node.left, node.right.left, node.right.right
        # Handle a + b*c
    

    match를 사용하면 다음과 같이 더 읽기 쉬워집니다.:

    match node:
        case BinOp("+", a, BinOp("*", b, c)):
            # Handle a + b*c
    

패턴 매칭을 Python에 추가하면 Python 사용자가 위와 같은 예는 물론 여러 다른 예에서도 더 깔끔하고 읽기 쉬운 코드를 작성할 수 있다고 생각합니다.

이 제안에 대한 보다 학술적인 논의는 [1]을 참조하십시오.

패턴 매칭과 객체 지향

패턴 매칭은 객체 지향 패러다임을 보완합니다. 객체 지향 프로그래밍과 상속을 사용하면 특정 클래스의 특정 연산에 대한 기본 동작을 정의하는 메서드를 베이스 클래스에 쉽게 정의할 수 있으며, 서브클래스에서 이 기본 동작을 재정의할 수 있습니다. 또한 Visitor 패턴을 사용하여 동작과 데이터를 분리할 수 있습니다.

그러나 이것만으로는 모든 상황에 충분하지 않습니다. 예를 들어 코드 제너레이터가 AST를 소비하고 여러 연산을 수행할 수 있는데, 이때 생성되는 코드는 노드의 클래스뿐만 아니라 위의 BinOp 예시처럼 일부 클래스 속성의 값에 따라서도 달라져야 할 수 있습니다. Visitor 패턴은 클래스에 따라서만 선택할 수 있으므로 이러한 경우에는 충분히 유연하지 않습니다.

완전한 예제를 참조하십시오.

Visitor 패턴과 마찬가지로 패턴 매칭은 관심사의 엄격한 분리를 가능하게 합니다. 즉, 특정 동작이나 데이터 처리는 클래스 계층 구조 또는 조작되는 객체와 독립적입니다. 특히 미리 정의된 클래스나 내장 클래스와 관련된 경우에는 개별 클래스에 추가 메서드를 작성하는 것이 불가능한 경우가 많습니다. 패턴 매칭은 프로그래머나 클래스 설계자가 Visitor 패턴에 필요한 상용구 코드를 작성해야 하는 부담을 덜어 줄 뿐만 아니라, 내장 유형과 직접 작동할 수 있을 만큼 유연합니다. 또한 길이가 서로 다른 시퀀스를 자연스럽게 구분하며, 이러한 시퀀스는 구조가 명백히 다르더라도 모두 동일한 클래스를 공유할 수 있습니다. 더 나아가 패턴 매칭은 상속을 자동으로 고려합니다. C를 상속하는 클래스 D는 기본적으로 C를 대상으로 하는 패턴으로 처리됩니다.

객체 지향 프로그래밍은 단일 디스패치를 중심으로 설계됩니다. 호출할 메서드를 결정하는 것은 하나의 인스턴스 또는 그 유형입니다. 이로 인해 이항 연산자의 경우에는 다소 인위적인 상황이 발생합니다. 두 객체가 어떤 구현을 사용할지 결정하는 데 동등한 역할을 할 수 있기 때문입니다. (Python은 역방향 이항 메서드를 사용하여 이 문제를 해결합니다.) 패턴 매칭은 여러 객체의 유형에 따라 수행할 동작이 동등하게 결정되는 다중 디스패치와 같은 상황을 구조적으로 더 잘 처리합니다.

패턴 매칭과 함수형 스타일

많은 Python 애플리케이션과 라이브러리는 일관된 OO 스타일로 작성되지 않습니다. Java와 달리 Python은 모듈의 최상위 수준에서 함수를 정의하도록 권장하며, 단순한 데이터 구조에는 튜플(또는 네임드 튜플이나 리스트)과 딕셔너리를 단독으로 사용하거나 클래스 또는 데이터 클래스와 혼합하여 사용하는 경우가 많습니다.

패턴 매칭은 이러한 데이터 구조를 분해하는 데 특히 적합합니다. 극단적인 예로, match를 사용하여 JSON 데이터 구조를 분해하는 코드를 쉽게 작성할 수 있습니다.:

match json_pet:
    case {"type": "cat", "name": name, "pattern": pattern}:
        return Cat(name, pattern)
    case {"type": "dog", "name": name, "breed": breed}:
        return Dog(name, breed)
    case _:
        raise ValueError("Not a suitable pet")

함수형 프로그래밍은 일반적으로 데이터 간 관계에 중점을 둔 선언적 스타일을 선호합니다. 부작용은 가능한 경우마다 피합니다. 따라서 패턴 매칭은 함수형 프로그래밍 스타일에 자연스럽게 들어맞으며 이를 강력하게 지원합니다.

근거

이 절에서는 개별 설계 결정의 근거를 제시합니다. 표준 PEP 형식에서 “거부된 아이디어”가 차지하는 부분을 대신합니다. 사양(PEP 634)에 해당하는 절들로 구성되어 있습니다.

개요 및 용어

패턴 매칭의 힘 대부분은 서브패턴의 중첩에서 나옵니다. 패턴 매칭의 성공이 서브패턴의 성공에 직접 좌우된다는 점은 설계의 초석입니다. 그러나 P(Q(), R())와 같은 패턴은 두 서브패턴 Q()R()가 모두 성공하는 경우에만 성공하지만(즉, 패턴 P의 성공은 QR에 좌우되지만), 패턴 P를 먼저 검사합니다. P가 실패하면 Q()R()어느 것도 시도하지 않습니다(이는 P가 실패하면 애초에 Q()R()에 대해 매칭할 대상이 없다는 사실에서 직접 도출되는 결과입니다).

또한 패턴은 할당을 수행하는 대신 이름을 값에 바인딩한다는 점에 유의하십시오. 이는 패턴이 부작용을 일으키지 않는 것을 목표로 한다는 사실을 반영하며, 따라서 Capture 패턴이나 AS 패턴은 속성 또는 첨자에 값을 할당할 수 없습니다. 따라서 전통적인 할당과 패턴에서의 이름 바인딩 사이의 미묘한 차이를 강조하기 위해 ‘assign’ 대신 ‘bind’라는 용어를 일관되게 사용합니다.

match 문

match 문은 표현식을 평가하여 대상을 만들고, 대상과 일치하는 첫 번째 패턴을 찾은 다음 연결된 코드 블록을 실행합니다. 따라서 구문상 match 문은 표현식과 일련의 case 절을 취하며, 각 case 절은 패턴과 코드 블록으로 구성됩니다.

case 절은 코드 블록으로 구성되므로, 복합문과 유사한 <keyword> ...: <(indented) block>이라는 구문 구조와 기존 들여쓰기 체계를 따릅니다. case라는 키워드는 패턴 매칭 언어에서 널리 사용된다는 점을 반영합니다. |와 같은 기호 등의 다른 구문적 수단을 사용하는 언어는 제외했는데, 그러한 방식은 Python에 확립된 구조에 맞지 않기 때문입니다. 키워드 뒤에 오는 패턴의 구문은 아래에서 설명합니다.

case 절이 복합문의 구조를 따르므로 match 문 자체도 자연스럽게 동일한 구문 구조를 따르는 복합문이 됩니다. 이에 따라 자연스럽게 match <expr>: <case_clause>+로 이어집니다. match 문은 평가된 대상을 유지하는 준 스코프를 결정한다는 점에 유의하십시오(로컬 변수에는 저장되지 않음). 이는 with 문이 블록 실행 중 리소스를 유지하는 방식과 유사합니다. 또한 제어 흐름은 match 문에서 case 절로 이동한 다음 match 문 블록을 빠져나옵니다. 따라서 match 문의 블록은 구문적 의미와 의미론적 의미를 모두 가집니다.

여러 제안에서는 case 절의 코드 블록에서 자연스럽게 발생하는 “이중 들여쓰기”를 제거하거나 피하려고 했습니다. 안타깝게도 이러한 평탄한 들여쓰기 방식에 관한 제안은 모두 Python에 확립된 구조적 패러다임을 위반하는 대가를 치르며, 그 결과 추가적인 구문 규칙이 필요하게 됩니다:

  • 들여쓰지 않은 case 절. 아이디어는 case 절을 match와 나란히 정렬하는 것입니다. 즉,:
    match expression:
    case pattern_1:
        ...
    case pattern_2:
        ...
    

    Python 프로그래머의 눈에는 어색해 보일 수 있습니다. 다른 모든 곳에서는 콜론 다음에 들여쓰기가 오기 때문입니다. match는 단순문이나 복합문의 구문 체계를 따르지 않고, 오히려 자체적인 범주를 확립하게 됩니다.

  • “match” 다음 줄에 식 배치하기. 아이디어는 콜론에도 불구하고 match가 실제 블록을 갖지 않는 특이성을 피하기 위해 대상을 산출하는 식을 문으로 사용하는 것입니다.:
    match:
        expression
    case pattern_1:
        ...
    case pattern_2:
        ...
    

    이는 결국 거부되었습니다. 첫 번째 블록이 Python 문법의 또 다른 새로운 요소가 되기 때문입니다. 즉, 문들의 시퀀스가 아니라 단일 식만을 내용으로 갖는 블록입니다. match: return expression과 같은 방식으로 또 다른 키워드를 추가하거나 용도를 변경하여 이 문제를 수정하려는 시도에서도 만족스러운 해결책이 나오지 않았습니다.

평면 들여쓰기를 사용하면 가로 공간을 어느 정도 절약할 수 있지만, 복잡성이 증가하거나 특이한 규칙을 도입하는 비용이 너무 큽니다. 또한 단순한 코드 편집기의 사용도 복잡하게 만들 것입니다. 마지막으로, match 문에 “반쪽 들여쓰기”(즉, 네 칸 대신 두 칸)를 허용하면 가로 공간 문제를 완화할 수 있습니다(다만 이를 권장하지는 않습니다).

이 PEP를 개발하는 과정에서 작성된 match사용 예제 프로그램에서는 코드가 눈에 띄게 간결해지는 개선이 관찰되며, 이는 추가된 들여쓰기 수준을 충분히 상쇄하고도 남습니다.

문과 식. 일부 제안은 match를 문이 아니라 식으로 만드는 아이디어에 초점을 맞추었습니다. 그러나 이는 문 중심적인 Python의 특성과 잘 맞지 않으며, 비정상적으로 길고 복잡한 식과 새로운 구문 구조를 고안하거나 확립된 구문 규칙을 깨야 할 필요성을 초래합니다. match를 식으로 만들었을 때의 명백한 결과는 case 절에 더 이상 임의의 코드 블록을 연결할 수 없고, 단일 식만 연결할 수 있다는 것입니다. 전반적으로 이러한 강한 제약은 일부 특수한 사용 사례에서 얻는 약간의 단순화로는 결코 상쇄할 수 없습니다.

하드 키워드와 소프트 키워드. match를 하드 키워드로 만들거나 다른 키워드를 선택하는 방안이 있었습니다. 하드 키워드를 사용하면 단순한 구문 강조기의 처리가 간소화되겠지만, 몇 가지 이유로 하드 키워드를 사용하지 않기로 결정했습니다.

  • 무엇보다도 새로운 파서는 이렇게 할 것을 요구하지 않습니다. 소프트 키워드였던 async로 인해 몇 차례 릴리스 동안 어려움이 발생했던 경우와 달리, 여기서는 match를 영구적인 소프트 키워드로 만들 수 있습니다.
  • match는 기존 코드에서 매우 흔하게 사용되므로, 기존 프로그램 대부분이 작동하지 않게 되고 새로운 구문의 혜택을 받지 못할 수도 있는 많은 사람에게 코드를 수정해야 하는 부담을 주게 됩니다.
  • 기존 프로그램에서 식별자로 흔히 사용되지 않으면서도 문의 의미를 명확히 반영하는 대체 키워드를 찾기는 어렵습니다.

case 절에 “case” 대신 “as” 또는 “|”를 사용하십시오. 여기서 제안하는 패턴 매칭은 다중 분기 제어 흐름(Algol 계열 언어의 switch나 Lisp의 cond와 같은 방식)과 함수형 언어에서 볼 수 있는 객체 구조 분해를 결합한 것입니다. 제안된 키워드 case는 다중 분기 측면을 강조하는 반면, as와 같은 대체 키워드는 구조 분해 측면을 강조할 수 있습니다. 예를 들어 aswith는 Python에서 이미 키워드라는 장점도 있습니다. 그러나 case는 키워드로서 match문 내부에서 선행 키워드로만 나타날 수 있으므로, 파서가 이를 키워드로 사용했는지 변수로 사용했는지 쉽게 구별할 수 있습니다.

다른 변형에서는 |=>와 같은 기호를 사용하거나, 특수 표식을 전혀 사용하지 않을 수도 있습니다.

Python은 Algol의 전통을 따르는 문 중심 언어이며 각 복합문은 식별 키워드로 시작하므로, case가 Python의 스타일과 전통에 가장 잘 부합하는 것으로 보였습니다.

매치 의미론

서로 다른 case 절의 패턴은 하나의 대상에 둘 이상의 case 절이 일치할 수 있다는 점에서 서로 겹칠 수 있습니다. 첫 번째 일치 규칙은 주어진 대상에 대한 case 절의 선택이 모호하지 않도록 보장합니다. 또한 case 절은 더 넓은 대상 집합과 일치하는, 점점 더 일반적인 패턴을 가질 수 있습니다. 따라서 첫 번째 일치 규칙은 가장 구체적인 패턴을 선택할 수 있도록 보장합니다(다만 case 절을 올바르게 정렬하는 것은 프로그래머의 책임입니다).

정적으로 타입이 지정된 언어에서는 match 문이 일치하는 패턴을 빠르고 매우 효율적으로 선택할 수 있도록 결정 트리로 컴파일됩니다. 그러나 이렇게 하려면 모든 패턴이 순수하게 선언적이고 정적이어야 하므로, Python의 확립된 동적 의미 체계와 충돌하게 됩니다. 따라서 제안된 의미 체계는 양쪽의 장점을 모두 결합하는 경로를 나타냅니다. 패턴은 엄격하게 순차적인 순서로 시도되므로 각 case 절이 실제 문장을 구성합니다. 동시에 인터프리터가 대상에 관한 정보를 캐시하거나 하위 패턴을 시도하는 순서를 변경하도록 허용합니다. 다시 말해, 인터프리터가 대상이 클래스 C의 인스턴스가 아니라는 사실을 발견했다면, 반복해서 인스턴스 검사를 수행하지 않고 이를 다시 테스트하는 case 절을 직접 건너뛸 수 있습니다. 예를 들어 가드가 변수 x가 양수여야 한다고 지정하는 경우(즉, if x > 0), 인터프리터는 x를 바인딩한 직후이자 추가 하위 패턴을 고려하기 전에 이를 직접 검사할 수 있습니다.

바인딩 및 스코핑. 많은 패턴 매칭 구현에서는 각 case 절이 자체적으로 별도의 스코프를 설정합니다. 그러면 패턴으로 바인딩된 변수는 해당 case 블록 내부에서만 표시됩니다. 그러나 Python에서는 이것이 타당하지 않습니다. 별도의 스코프를 설정한다는 것은 본질적으로 각 case 절이 주변 스코프의 변수에 직접 접근할 수 없는 별도의 함수가 된다는 의미입니다(말하자면 nonlocal을 사용해야 하는 경우를 제외하면 그렇습니다). 더욱이 case 절은 더 이상 return이나 break와 같은 표준 문장을 통해 주변 제어 흐름에 영향을 줄 수 없습니다. 따라서 이러한 엄격한 스코핑은 직관에 어긋나고 예상하기 어려운 동작을 초래합니다.

이에 따른 직접적인 결과로 모든 변수 바인딩은 해당 case 문이나 match 문보다 오래 존속합니다. 대상을 부분적으로만 일치시키는 패턴도 지역 변수를 바인딩할 수 있습니다(이는 실제로 가드가 제대로 작동하는 데 필요합니다). 그러나 이러한 변수 바인딩 의미 체계는 for 루프나 with 문과 같은 기존 Python 구조와 일치합니다.

가드

일부 제약 조건은 패턴만으로 적절하게 표현할 수 없습니다. 예를 들어 ‘보다 작음’ 또는 ‘보다 큼’ 관계는 패턴의 일반적인 ‘같음’ 의미 체계에 맞지 않습니다. 더욱이 서로 다른 하위 패턴은 독립적이므로 서로를 참조할 수 없습니다. 가드를 추가하면 이러한 제약이 해결됩니다. 가드는 패턴에 연결된 임의의 표현식이며, 패턴이 성공하려면 “참 같은” 값으로 평가되어야 합니다.

예를 들어 case [x, y] if x < y:는 가드(if x < y)를 사용하여 서로 별개인 두 캡처 패턴 xy사이의 ‘보다 작음’ 관계를 표현합니다.

개념적으로 패턴은 선언적 방식으로 대상에 대한 구조적 제약을 설명하며, 이상적으로는 부작용이 없어야 합니다. 특히 패턴은 표현식과 명확히 구별되며, 서로 다른 목적과 의미 체계를 따릅니다. 가드는 부작용이 있을 수 있는 임의의 표현식을 사용하여 고도로 제어된 방식으로 case 블록을 향상합니다. 전체 기능을 정적 구조 부분과 동적으로 평가되는 부분으로 나누면 가독성에 도움이 될 뿐만 아니라, 컴파일러 최적화의 가능성도 크게 높일 수 있습니다. 이러한 명확한 분리를 유지하기 위해 가드는 case 절 수준에서만 지원되며 개별 패턴에는 지원되지 않습니다.

가드를 사용하는 예제:

def sort(seq):
    match seq:
        case [] | [_]:
            return seq
        case [x, y] if x <= y:
            return seq
        case [x, y]:
            return [y, x]
        case [x, y, z] if x <= y <= z:
            return seq
        case [x, y, z] if x >= y >= z:
            return [z, y, x]
        case [p, *rest]:
            a = sort([x for x in rest if x <= p])
            b = sort([x for x in rest if p < x])
            return a + [p] + b

패턴

패턴은 두 가지 목적을 충족합니다. 대상에 구조적 제약을 부과하고, 대상에서 추출하여 변수에 바인딩해야 하는 데이터 값을 지정합니다. Python의 패턴 매칭의 원형으로 볼 수 있는 이터러블 언패킹에는 시퀀스를 표현하는 구조적 패턴이 하나만 있는 반면, 특정 변수나 필드에 값을 할당하는 바인딩 패턴은 매우 다양합니다. 완전한 패턴 매칭은 구조적 패턴의 종류가 더 다양하지만 바인딩 패턴은 최소한만 제공된다는 점에서 이와 다릅니다.

패턴은 할당 대상(이터러블 언패킹에서와 같은)과 두 가지 점에서 다릅니다. 패턴은 대상의 구조에 추가적인 제약을 부과하며, 대상은 어느 시점에서든 특정 패턴과의 매칭에 안전하게 실패할 수 있습니다(이터러블 언패킹에서는 이것이 오류를 구성합니다). 후자는 패턴이 가능한 한 부작용을 피해야 함을 의미합니다.

부작용을 피하려는 이러한 요구는 캡처 패턴이 속성이나 서브스크립트에 값을 바인딩하는 것을 허용하지 않는 이유 중 하나입니다. 포함하는 패턴이 이후 단계에서 실패한다면 이러한 바인딩을 되돌리기 어렵기 때문입니다.

패턴 매칭의 초석은 패턴을 임의로 중첩할 수 있음입니다. 이러한 중첩을 통해 깊은 트리 구조(중첩된 클래스 패턴의 예는 위의 동기 부여 섹션을 참조하십시오)와 대안을 표현할 수 있습니다.

패턴은 표면적으로 표현식처럼 보일 수 있지만, 둘 사이에 명확한 구분이 있음을 명심하는 것이 중요합니다. 사실 어떤 패턴도 표현식이 아니며 표현식을 포함하지도 않습니다. 패턴을 함수 정의의 형식 매개변수와 유사한 선언적 요소로 생각하는 편이 더 생산적입니다.

AS 패턴

패턴은 두 가지 범주로 나뉩니다. 대부분의 패턴은 대상이 충족해야 하는 (구조적) 제약을 부과하는 반면, 캡처 패턴은 대상의 구조나 실제 값과 관계없이 대상을 이름에 바인딩합니다. 따라서 패턴은 제약을 표현하거나 값을 바인딩할 수 있지만, 둘 다 할 수는 없습니다. AS 패턴은 사용자가 일반 패턴을 지정하는 동시에 대상을 변수에 캡처할 수 있도록 하여 이 간극을 메웁니다.

AS 패턴의 일반적인 사용 사례에는 바인딩 이름과 함께 사용하는 OR 패턴 및 클래스 패턴이 포함됩니다. 예를 들어 case BinOp('+'|'-' as op, ...): 또는 case [int() as first, int() as second]:와 같습니다. 후자는 대상이 [first, second][int(), int()]라는 서로 다른 두 패턴을 모두 충족해야 한다는 의미로 이해할 수 있습니다. 따라서 AS 패턴은 ‘and’ 패턴의 특수한 경우로 볼 수 있습니다(‘and’ 패턴에 대한 추가 논의는 아래의 OR 패턴을 참조하십시오).

이전 버전에서는 AS 패턴을 ‘Walrus 패턴’으로 고안하여 case [first:=int(), second:=int()]와 같이 작성했습니다. 그러나 as를 사용하는 방식은 :=보다 몇 가지 장점이 있습니다.

  • Walrus 연산자 :=는 오른쪽의 표현식 결과를 캡처하는 데 사용되는 반면, as는 일반적으로 import foo as bar 또는 except E as err:에서처럼 어떤 형태의 ‘처리’를 나타냅니다. 실제로 패턴 P as x은 패턴 Px에 할당하는 것이 아니라, P와 성공적으로 매칭되는 대상을 할당합니다.
  • as는 왼쪽에서 오른쪽으로 더 일관된 데이터 흐름을 허용합니다(클래스 패턴의 속성도 왼쪽에서 오른쪽으로 데이터가 흐릅니다).
  • Walrus 연산자는 클래스 패턴에서 속성을 매칭하는 구문과 매우 유사해 혼동을 일으킬 수 있습니다.

AS 패턴을 사용한 예시:

def simplify_expr(tokens):
    match tokens:
        case [('('|'[') as l, *expr, (')'|']') as r] if (l+r) in ('()', '[]'):
            return simplify_expr(expr)
        case [0, ('+'|'-') as op, right]:
            return UnaryOp(op, right)
        case [(int() | float() as left) | Num(left), '+', (int() | float() as right) | Num(right)]:
            return Num(left + right)
        case [(int() | float()) as value]:
            return Num(value)

OR 패턴

OR 패턴을 사용하면 ‘구조적으로 동등한’ 대안들을 새로운 패턴으로 결합할 수 있습니다. 즉, 여러 패턴이 공통 핸들러를 공유할 수 있습니다. OR 패턴의 하위 패턴 중 하나라도 대상과 매칭되면 전체 OR 패턴이 성공합니다.

정적 타입 언어에서는 변수의 타입과 관련된 잠재적 충돌 때문에 OR 패턴 내부에서 이름을 바인딩하는 것(캡처 패턴)을 금지합니다. 동적 타입 언어인 Python은 여기서 덜 제한적으로 동작하여 OR 패턴 내부의 캡처 패턴을 허용할 수 있습니다. 그러나 정의되지 않을 가능성이 있는 이름을 남기지 않으려면 각 하위 패턴이 동일한 변수 집합을 바인딩해야 합니다. 두 대안 P | Q가 있을 때, 이는 P가 변수 uv를 바인딩한다면 Q도 정확히 동일한 변수 uv를 바인딩해야 한다는 의미입니다.

대안을 구분할 때 막대 기호 |를 사용할지 or 키워드를 사용할지에 대한 논의가 있었습니다. OR 패턴은 이 두 기호 중 어느 쪽의 기존 의미론 및 용법에도 완전히 들어맞지 않습니다. 그러나 |는 OR 패턴을 지원하는 모든 프로그래밍 언어에서 선택되는 기호이며, Python의 정규 표현식에서도 그러한 용도로 사용됩니다. 또한 형식 문법( Python의 형식 문법 포함)에서 대안 사이를 구분하는 전통적인 구분자이기도 합니다. 더욱이 |는 비트 OR뿐만 아니라 집합 합집합과 딕셔너리 병합(PEP 584)에도 사용됩니다.

다른 대안들도 고려되었지만, 그중 어느 것도 OR 패턴을 다른 패턴 내부에 중첩할 수 있도록 하지는 못합니다.

  • 쉼표 사용:
    case 401, 403, 404:
        print("Some HTTP error")
    

    이는 튜플과 너무 비슷해 보입니다. 튜플을 표기하는 다른 방법을 찾아야 하며, 클래스 패턴의 인자 목록 내부에서 이 구문을 괄호로 묶어야 합니다. 일반적으로 쉼표는 Python에서 이미 여러 가지 의미를 가지므로, 의미를 더 추가해서는 안 됩니다.

  • 사례를 나란히 사용하기:
    case 401:
    case 403:
    case 404:
        print("Some HTTP error")
    

    이는 C에서 case에 대한 폴스루 의미론을 사용하여 수행하는 방식입니다. 그러나 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")
    

AND 및 NOT 패턴

이 제안은 여러 대안 중 하나와 일치하는 OR 패턴(|)을 정의하므로, AND 패턴(&)이나 심지어 NOT 패턴(!)도 정의하면 어떻겠습니까? 특히 다른 언어(예를 들어 F#)에서는 AND 패턴을 지원한다는 점을 고려하면 더욱 그렇습니다.

그러나 이것이 얼마나 유용할지는 분명하지 않습니다. 딕셔너리, 객체 및 시퀀스를 일치시키는 의미론에는 이미 암묵적인 ‘and’가 포함되어 있습니다. 일치가 성공하려면 언급된 모든 속성과 요소가 존재해야 합니다. 가드 조건은 가상의 ‘and’ 연산자가 사용될 많은 사용 사례도 지원할 수 있습니다.

연산자 !를 접두사로 사용하는 매치 패턴의 부정은 패턴 자체가 일치하지 않을 때 정확히 일치합니다. 예를 들어 !(3 | 4)3 또는 4를 제외한 모든 항목과 일치합니다. 그러나 evidence from other languages에 따르면 이는 거의 유용하지 않으며, 주로 변수 스코프를 제어하고 변수 바인딩을 방지하기 위한 이중 부정 !!으로 사용됩니다(Python에는 해당되지 않습니다). 다른 사용 사례는 가드를 사용하여 더 잘 표현할 수 있습니다.

결국 이는 상당한 이점을 추가하지 않으면서 구문을 더 복잡하게 만들 것이라는 결론이 내려졌습니다. 나중에 언제든 추가할 수 있습니다.

예제를 사용하는 OR 패턴:

def simplify(expr):
    match expr:
        case ('/', 0, 0):
            return expr
        case ('*'|'/', 0, _):
            return 0
        case ('+'|'-', x, 0) | ('+', 0, x) | ('*', 1, x) | ('*'|'/', x, 1):
            return x
    return expr

리터럴 패턴

리터럴 패턴은 유형이나 구조가 아니라 대상의 값에 제약을 부과하는 편리한 방법입니다. 또한 패턴 매칭을 사용하여 switch 문을 모방할 수 있도록 합니다.

일반적으로 대상은 표준 동등성(x == y는 Python 구문입니다)을 사용하여 리터럴 패턴과 비교합니다. 따라서 리터럴 패턴 1.01은 정확히 동일한 객체 집합과 일치하며, 즉 case 1.0:case 1:은 완전히 상호 교환할 수 있습니다. 원칙적으로 TrueTrue == 1이 성립하므로 동일한 객체 집합과 일치합니다. 그러나 case True:가 대상 1.0과 일치하여 미묘한 버그와 복잡한 우회 방법이 발생한다면 많은 사용자가 놀랄 것이라고 판단합니다. 따라서 세 싱글턴 패턴 None, False, True는 동등성이 아니라 식별성(x is y는 Python 구문입니다)에 따라 일치한다는 규칙을 채택했습니다. 따라서 case True:True만 일치시키고 다른 어떤 것도 일치시키지 않습니다. 다만 case 1:은 여전히 True와 일치한다는 점에 유의하십시오. 리터럴 패턴 1은 식별성이 아니라 동등성에 따라 동작하기 때문입니다.

case 1.0이 정수 1과 부동 소수점 수 1.0 모두와 일치하고, 반면 case 1:은 정수 1과만 일치하도록 수에 계층 구조를 도입하자는 초기 아이디어는 결국 더 단순하고 일관성 있는 동등성 기반 규칙을 채택하면서 폐기되었습니다. 또한 대상이 numbers.Integral의 인스턴스인지 확인하는 추가 검사는 본질적으로 Python에 새로운 개념을 도입하기 위한 것이므로 높은 런타임 비용을 초래합니다. 필요한 경우 명시적 구문 case int(1):을 사용할 수 있습니다.

리터럴 패턴은 표현식이 아니라 특정 값을 직접 나타낸다는 점을 기억하십시오. 실용적인 관점에서는 음수와 심지어 복소수 값도 리터럴 패턴으로 사용할 수 있도록 하고 싶지만, 이러한 값은 원자적 리터럴이 아닙니다(부호 없는 실수 및 허수만 원자적 리터럴입니다). 예를 들어 -3+4j는 문법적으로 BinOp(UnaryOp('-', 3), '+', 4j) 형식의 표현식입니다. 표현식은 패턴의 일부가 아니므로, 전체 표현식을 사용하지 않고 이러한 값을 지원하기 위한 명시적인 문법 지원을 추가해야 했습니다.

반면 보간된 f-문자열은 겉보기와 달리 리터럴 값이 아니므로 리터럴 패턴으로 사용할 수 없습니다(하지만 문자열 연결은 지원됩니다).

리터럴 패턴은 그 자체로 패턴으로 나타날 뿐만 아니라, 매핑 패턴의 키로도 나타납니다.

범위 매칭 패턴. 이를 사용하면 1...6과 같은 패턴을 작성할 수 있습니다. 그러나 여기에는 여러 모호성이 있습니다.

  • 범위는 열린 범위입니까, 반열린 범위입니까, 아니면 닫힌 범위입니까? (즉, 위 예에서 6이 포함됩니까, 포함되지 않습니까?)
  • 범위는 하나의 수와 일치합니까, 아니면 범위 객체와 일치합니까?
  • 범위 매칭은 문자 범위(‘a’…’z’)에 자주 사용되지만, Python에는 문자 데이터 형식이 없고 문자열만 있으므로 작동하지 않습니다.
  • 점프 테이블을 미리 만들 수 있다면 범위 매칭은 상당한 성능 최적화가 될 수 있지만, 이름을 동적으로 다시 바인딩할 수 있기 때문에 Python에서는 일반적으로 불가능합니다.

범위를 위한 특수 목적 문법을 만드는 대신, 사용자 지정 패턴 객체(InRange(0, 6))를 허용하는 편이 더 유연하고 모호성이 적다고 결정했습니다. 그러나 이러한 아이디어는 당분간 보류되었습니다.

리터럴 패턴을 사용한 예시:

def simplify(expr):
    match expr:
        case ('+', 0, x):
            return x
        case ('+' | '-', x, 0):
            return x
        case ('and', True, x):
            return x
        case ('and', False, x):
            return False
        case ('or', False, x):
            return x
        case ('or', True, x):
            return True
        case ('not', ('not', x)):
            return x
    return expr

캡처 패턴

캡처 패턴은 모든 값을 받아들여 이를 (지역) 변수에 바인딩하는 이름의 형태를 취합니다(단, 해당 이름이 nonlocal 또는 global로 선언된 경우는 제외합니다). 그런 의미에서 캡처 패턴은 함수 정의의 매개변수와 유사합니다(함수가 호출되면 각 매개변수는 해당 인자를 함수의 스코프에 있는 지역 변수에 바인딩합니다).

캡처 패턴에 사용되는 이름은 같은 패턴의 다른 캡처 패턴과 일치해서는 안 됩니다. 이는 각 매개변수 이름이 매개변수 목록 안에서 고유해야 하는 매개변수와도 다시 유사합니다. 그러나 이는 대상에 변수 이름을 반복해서 사용하는 것이 허용되는 이터러블 언패킹 대입과는 다릅니다(예: x, x = 1, 2). 패턴에서 (x, x)를 지원하지 않는 근거는 이를 모호하게 해석할 수 있기 때문입니다. 즉, x에 대한 두 번째 바인딩만 남는 이터러블 언패킹에서처럼 볼 수 있습니다. 그러나 두 개의 동일한 요소를 가진 튜플을 표현하는 것으로도 볼 수 있으며, 여기에도 고유한 문제가 따릅니다. 필요성이 생긴다면 나중에 이름의 반복 사용을 지원하는 것도 여전히 가능합니다.

캡처 패턴을 명시적으로 표시하여 바인딩 대상으로 식별하자는 요구가 있었습니다. 그 생각에 따르면 캡처 패턴은 예를 들어 ?x, $x 또는 =x로 작성됩니다. 이러한 명시적 캡처 표시는 표시되지 않은 이름을 값 패턴으로 사용할 수 있게 하는 것을 목표로 합니다(아래 참조). 그러나 이는 패턴 매칭이 switch 문을 확장한 것이라는 오해에 기반하며, (순서형) 값을 빠르게 전환하는 데 중점을 둡니다. 이러한 switch 문은 실제로 과거에 Python에 제안된 적이 있습니다(PEP 275PEP 3103 참조). 반면 패턴 매칭은 이터러블 언패킹의 일반화된 개념을 구축합니다. 데이터 구조에서 추출한 값을 바인딩하는 것이 이 개념의 핵심이며, 따라서 가장 일반적인 사용 사례입니다. 따라서 캡처 패턴에 대한 명시적 표시는 제안된 패턴 매칭 구문의 목적을 저해하고, 핵심 사례에 추가적인 구문적 잡음을 초래하는 대신 부차적인 사용 사례를 단순화하게 됩니다.

캡처 패턴은 전혀 필요하지 않다는 제안도 있었습니다. AS 패턴과 와일드카드 패턴을 결합하면 동일한 효과를 얻을 수 있기 때문입니다(예: case _ as xcase x와 동일합니다). 그러나 캡처 패턴이 매우 일반적으로 사용될 것으로 예상된다는 점을 고려하면 이는 불필요하게 장황합니다.

예제를 사용하는 캡처 패턴:

def average(*args):
    match args:
        case [x, y]:           # captures the two elements of a sequence
            return (x + y) / 2
        case [x]:              # captures the only element of a sequence
            return x
        case []:
            return 0
        case a:                # captures the entire sequence
            return sum(a) / len(a)

와일드카드 패턴

와일드카드 패턴은 ‘캡처’ 패턴의 특수한 경우입니다. 모든 값을 허용하지만 변수에 바인딩하지는 않습니다. 이 규칙의 취지는 패턴에서 와일드카드를 반복해서 사용할 수 있도록 하는 것입니다. (x, x)는 오류이지만 (_, _)는 유효합니다.

특히 더 큰 (시퀀스) 패턴에서는 패턴이 실제로 중요한 값에 집중하고 나머지는 무시할 수 있도록 하는 것이 중요합니다. 와일드카드가 없다면 여러 개의 지역 변수를 ‘만들어 내야’ 하며, 이 변수들은 바인딩되지만 결코 사용되지 않습니다. 관련 없는 값의 이름을 지정할 때 명명 규칙을 따르고 예를 들어 _1, _2, _3를 사용하더라도 이는 여전히 시각적 잡음을 만들고 성능을 저하시킬 수 있습니다(시퀀스 패턴 (x, y, *z)(_, y, *_)를 비교하십시오. 전자는 *z가 인터프리터가 잠재적으로 매우 긴 시퀀스를 복사하도록 강제하는 반면, 후자는 단순히 y = seq[1]와 같은 형태의 코드로 컴파일됩니다).

밑줄을 _와일드카드 패턴으로 선택하여 이 이름을 비바인딩으로 만드는 것에 관해 많은 논의가 있었습니다. 그러나 밑줄은 이미 이터러블 언패킹에서 ‘무시할 값’을 나타내는 표식으로 널리 사용되고 있습니다. 와일드카드 패턴 _는 절대 바인딩되지 않으므로, 밑줄을 이렇게 사용해도 REPL 내부나 gettext 모듈에서의 사용과 같은 다른 용도와 충돌하지 않습니다.

... (즉, 줄임표 토큰) 또는 * (별표)를 와일드카드로 사용하자는 제안이 있었습니다. 그러나 이 둘은 모두 임의 개수의 항목이 생략된 것처럼 보입니다.:

case [a, ..., z]: ...
case [a, *, z]: ...

두 예 모두 첫 번째 값과 마지막 값을 캡처하면서 두 개 이상의 항목으로 이루어진 시퀀스와 일치할 것처럼 보입니다. 이것이 궁극적인 “와일드카드”일 수는 있지만, 원하는 의미론을 전달하지는 않습니다.

임의의 항목 수를 암시하지 않는 대안으로는 ?가 있습니다. 이는 PEP 640에서 패턴 매칭과 독립적으로 제안되기도 했습니다. 그러나 ?를 특수한 “할당” 대상으로 사용하는 것이 _를 사용하는 것보다 Python 사용자에게 더 혼란스러울 가능성이 높다고 생각합니다. 이는 일반적인 영어 사용이나 고등학교 수학에서 사용되는 방식과 유사한 방식으로만 구두점 문자를 사용한다는 Python의 (분명 모호하기는 하지만) 원칙을 위반합니다. 단, 다른 프로그래밍 언어에서 해당 사용법이 매우 확립되어 있는 경우(예를 들어 멤버 접근에 점을 사용하는 경우)는 예외입니다.

물음표는 두 기준 모두를 충족하지 못합니다. 다른 프로그래밍 언어에서 물음표가 사용되는 방식은 “질문”이라는 개념에서 막연하게 파생된 용법들을 마구 모아 놓은 것에 불과합니다. 예를 들어 셸 글로빙에서는 “임의의 문자”, 정규 표현식에서는 “아마도”, C 및 C에서 파생된 여러 언어에서는 “조건식”, Scheme에서는 “술어 함수”, Rust에서는 “오류 처리 수정”, TypeScript에서는 “선택적 인자”와 “선택적 체이닝”을 의미합니다(후자의 의미는 PEP 505에서 Python에도 제안되었습니다). 아직 이름이 정해지지 않은 한 PEP에서는 선택적 타입을 표시하기 위해 이를 제안합니다. 예를 들면 int?입니다.

프로그래밍 시스템에서 ?의 또 다른 일반적인 용도는 “도움말”입니다. 예를 들어 IPython과 Jupyter Notebooks, 그리고 여러 대화형 명령줄 유틸리티에서 사용됩니다.

또한 이는 Python을 상당히 독특한 위치에 놓게 됩니다. 우리가 찾을 수 있었던 패턴 매칭을 지원하는 모든 프로그래밍 언어(C#, Elixir, Erlang, F#, Grace, Haskell, Mathematica, OCaml, Ruby, Rust, Scala, Swift, Thorn 포함)에서 밑줄은 와일드카드 패턴입니다. 많은 Python 사용자가 다른 프로그래밍 언어도 사용하고, Python을 배울 때 이전 경험을 보유하며, Python을 배운 후 다른 언어로 넘어갈 수도 있다는 점을 고려하면, 이러한 확립된 표준은 가독성과 학습 용이성 측면에서 중요하고 관련성이 있다고 생각합니다. 이 와일드카드가 일반 이름에 특별한 처리를 한다는 우려는 Python을 특별하게 만드는 문법을 도입할 만큼 충분히 강하지 않다고 생각합니다.

Else 블록. 가드가 없고 패턴이 단일 와일드카드(즉, case _:)인 case 블록은 어떤 대상이든 변수에 바인딩하거나 다른 작업을 수행하지 않고 허용합니다. 따라서 지원된다고 가정하면 의미상 else:와 동등합니다. 그러나 이러한 else 블록을 match 문법에 추가하더라도 다른 컨텍스트에서 와일드카드 패턴이 필요한 것은 없어지지 않습니다. 이에 반대하는 또 다른 주장은 else 블록에 대해 가능한 들여쓰기 수준이 두 가지라는 것입니다. case와 정렬하거나 match와 정렬할 수 있습니다. 저자들은 어느 들여쓰기 수준을 선호할지에 대해 상당한 논쟁이 있음을 확인했습니다.

와일드카드 패턴을 사용하는 예제:

def is_closed(sequence):
    match sequence:
        case [_]:               # any sequence with a single element
            return True
        case [start, *_, end]:  # a sequence with at least two elements
            return start == end
        case _:                 # anything
            return False

값 패턴

매개변수화된 값에 이름이 있는 상수를 사용하거나 특정 값의 의미를 명확히 하는 것은 좋은 프로그래밍 방식입니다. 예를 들어 case (200, body):보다 case (HttpStatus.OK, body):라고 작성하는 것이 분명히 바람직합니다. 여기서 발생하는 주요 문제는 캡처 패턴(변수 바인딩)과 값 패턴을 구별하는 방법입니다. 이 문제를 둘러싼 일반적인 논의에서는 매우 다양한 선택지가 제시되었으며, 이를 모두 여기에서 완전히 나열할 수는 없습니다.

엄밀히 말하면 값 패턴은 실제로 필요하지 않지만, 가드를 사용하여 구현할 수 있습니다. 즉, case (status, body) if status == HttpStatus.OK:와 같이 구현할 수 있습니다. 그럼에도 값 패턴의 편리함은 의심의 여지 없이 분명합니다.

상수는 대체로 대문자로 작성되거나 열거형과 유사한 네임스페이스에 모인다는 관찰은 상수를 구문적으로 식별하기 위한 가능한 규칙을 시사합니다. 그러나 대문자와 소문자를 표지로 사용하자는 생각은 회의적인 반응을 받았습니다. 핵심 Python에는 유사한 선례가 없기 때문입니다(다른 언어에서는 일반적이기는 합니다). 따라서 점으로 구분된 이름(즉, 속성 접근)은 값 패턴으로 해석한다는 규칙만 채택했습니다. 예를 들어 위의 HttpStatus.OK가 이에 해당합니다. 특히 이는 현재 모듈에 정의된 지역 변수와 전역 변수가 상수로 동작하는 것을 배제합니다.

이러한 목적에 선행 점을 사용하자는 제안(예: .CONSTANT)은 점이 그 목적을 나타내는 표지로 충분히 눈에 띄지 않는다고 여겨졌기 때문에 비판받았습니다. 다른 프로그래밍 언어에서 발견되는 형태에서 부분적으로 영감을 받아 여러 가지 표지/기호가 제안되었습니다(예: ^CONSTANT, $CONSTANT, ==CONSTANT, CONSTANT? 또는 백틱으로 감싼 단어). 그러나 명백하거나 자연스러운 선택지는 없었습니다. 따라서 현재 제안에서는 이러한 ‘상수’ 표지에 대한 논의와 도입 가능성을 향후 PEP의 과제로 남겨 둡니다.

이름이 전역 변수인지 여부에 따라 이름의 의미를 구별하면(즉, 컴파일러가 전역 변수를 캡처 패턴이 아니라 상수로 취급하게 하면) 여러 문제가 발생합니다. 모듈에서 전역 변수를 추가하거나 변경하면 패턴에 의도하지 않은 부작용이 발생할 수 있습니다. 또한 모든 변수가 전역 변수가 되므로 캡처 패턴을 사용할 수 없게 되어, 모듈의 스코프 내부에서 패턴 매칭을 직접 사용할 수 없습니다.

값 패턴을 사용한 예제:

def handle_reply(reply):
    match reply:
        case (HttpStatus.OK, MimeType.TEXT, body):
            process_text(body)
        case (HttpStatus.OK, MimeType.APPL_ZIP, body):
            text = deflate(body)
            process_text(text)
        case (HttpStatus.MOVED_PERMANENTLY, new_URI):
            resend_request(new_URI)
        case (HttpStatus.NOT_FOUND):
            raise ResourceNotFound()

그룹 패턴

사용자가 그룹화를 명시적으로 지정할 수 있도록 하면 OR 패턴의 경우에 특히 유용합니다.

시퀀스 패턴

시퀀스 패턴은 이미 확립된 이터러블 언패킹의 구문과 의미를 가능한 한 밀접하게 따릅니다. 물론 서브패턴은 할당 대상(변수, 속성 및 서브스크립트)을 대신합니다. 또한 시퀀스 패턴은 신중하게 선택된 가능한 대상 집합에만 일치하는 반면, 이터러블 언패킹은 모든 이터러블에 적용할 수 있습니다.

  • 이터러블 언패킹에서와 마찬가지로 ‘튜플’ 표기와 ‘리스트’ 표기를 구분하지 않습니다. [a, b, c], (a, b, c)a, b, c는 모두 동등합니다. 이는 중복된 표기가 존재하고 리스트나 튜플인지 특별히 확인하려면 더 많은 작업이 필요하다는 의미이지만(예: case list([a, b, c])), 가능한 한 이터러블 언패킹을 모방합니다.
  • 별표 패턴은 임의 길이의 하위 시퀀스를 캡처하며, 다시 이터러블 언패킹을 반영합니다. 어떤 시퀀스 패턴에도 별표 항목은 하나만 있을 수 있습니다. 이론적으로 (*_, 3, *_)과 같은 패턴은 값 3을 포함하는 모든 시퀀스를 표현하는 것으로 이해할 수 있습니다. 그러나 실제로는 이는 매우 제한된 사용 사례에서만 작동하며, 그렇지 않으면 비효율적인 백트래킹이나 모호성을 초래합니다.
  • 시퀀스 패턴은 이터러블 대상 순회하지 않습니다. 모든 요소는 서브스크립팅과 슬라이싱을 통해 액세스되며, 대상은 collections.abc.Sequence의 인스턴스여야 합니다. 여기에는 물론 리스트와 튜플이 포함되지만, 집합과 딕셔너리 등은 제외됩니다. 문자열과 바이트도 포함되지만, 이에 대해서는 예외를 둡니다(아래 참조).

시퀀스 패턴은 임의의 이터러블 객체를 단순히 순회할 수 없습니다. 전체 패턴이 실패하면 순회 중 요소를 소비한 작업을 되돌려야 하는데, 이는 실현 가능하지 않습니다.

시퀀스를 식별할 때 len()과 서브스크립팅 및 슬라이싱에만 의존할 수는 없습니다. 이 점에서 시퀀스는 매핑과 이러한 프로토콜을 공유하기 때문입니다(예: dict). 시퀀스 패턴이 딕셔너리나 매핑 프로토콜(즉, __getitem__)을 구현하는 다른 객체에도 일치한다면 의외일 것입니다. 따라서 인터프리터는 해당 대상이 실제로 시퀀스인지(알려진 형식의 시퀀스인지) 확인하기 위해 인스턴스 검사를 수행합니다. (가장 일반적인 경우를 최적화하기 위해 대상이 정확히 리스트나 튜플이면 인스턴스 검사를 생략할 수 있습니다.)

문자열과 바이트 객체는 이중적인 성격을 가집니다. 즉, 그 자체로 ‘원자적’ 객체인 동시에 시퀀스이며(문자열이 문자열의 시퀀스라는 점에서 강한 재귀적 특성을 가집니다), 시퀀스이기도 합니다. 문자열과 바이트의 일반적인 동작 및 사용 사례는 튜플과 리스트의 경우와 충분히 달라 명확한 구분이 필요합니다. 실제로 문자열이 시퀀스로 취급되는 것은 직관에 어긋나고 의도하지 않은 경우가 많으며, 이는 반복되는 질문과 불만에서도 확인할 수 있습니다. 따라서 문자열과 바이트는 시퀀스 패턴과 일치하지 않으며, 시퀀스 패턴을 ‘시퀀스’에 대한 매우 구체적인 이해로 제한합니다. 내장 bytearray 형식은 bytes의 변경 가능한 버전이므로 역시 예외로 취급할 만합니다. 그러나 바이트를 표현하는 데 사용될 수 있는 다른 모든 형식(예: memoryviewarray.array의 일부 인스턴스)을 열거할 의도는 없습니다.

매핑 패턴

딕셔너리 또는 일반적인 매핑은 Python에서 가장 중요하고 가장 널리 사용되는 데이터 구조 중 하나입니다. 시퀀스와 달리 매핑은 키로 식별되는 임의의 요소에 빠르게 직접 접근할 수 있도록 만들어졌습니다. 대부분의 경우 딕셔너리에서 요소를 가져올 때는 동일한 딕셔너리에 저장된 키-값 쌍이나 순서와 관계없이 알고 있는 키를 사용합니다. 특히 문자열 키가 흔히 사용됩니다.

매핑 패턴은 딕셔너리 조회의 일반적인 사용 방식을 반영합니다. 즉, 상수 키 또는 알려진 키를 사용하여 매핑에서 일부 값을 추출하고, 해당 값이 주어진 하위 패턴과 일치하도록 합니다. 주제에 추가 키가 있더라도 **rest가 없으면 무시됩니다. 이는 추가 항목이 있으면 매칭이 실패하는 시퀀스 패턴과 다릅니다. 그러나 매핑은 실제로 시퀀스와 다릅니다. 매핑에는 자연스러운 구조적 서브타이핑 동작이 있으므로, 어딘가에 추가 키가 있는 딕셔너리를 전달해도 대체로 정상적으로 작동합니다. 매핑에 상한을 설정하고 추가 키가 없도록 해야 하는 경우에는 일반적인 더블 스타 패턴 **rest를 사용할 수 있습니다. 그러나 와일드카드를 사용하는 특수한 경우인 **_는 아무런 효과가 없으므로 지원되지 않으며, 매핑 패턴의 의미를 잘못 이해하게 만들 수 있습니다.

지나치게 비용이 많이 드는 매칭 알고리즘을 피하려면 키는 리터럴 또는 값 패턴이어야 합니다.

__getitem__(key)를 호출한 뒤 AttributeError를 확인하는 대신 get(key, default)를 사용하는 데에는 미묘한 이유가 있습니다. 주제가 defaultdict인 경우 존재하지 않는 키에 대해 __getitem__을 호출하면 해당 키가 추가되기 때문입니다. get()을 사용하면 이 예상하지 못한 부작용을 피할 수 있습니다.

매핑 패턴을 사용하는 Example::

def change_red_to_blue(json_obj):
match json_obj:
case { ‘color’: (‘red’ | ‘#FF0000’) }:
json_obj[‘color’] = ‘blue’
case { ‘children’: children }:
for child in children:
change_red_to_blue(child)

클래스 패턴

클래스 패턴은 주어진 주제가 실제로 특정 클래스의 인스턴스인지 확인하고, 주제의 특정 속성에서 데이터를 추출하는 두 가지 목적을 충족합니다. 일화적인 증거에 따르면 isinstance()는 프로그램에서 정적으로 나타나는 횟수를 기준으로 Python에서 가장 자주 사용되는 함수 중 하나입니다. 이러한 인스턴스 검사는 일반적으로 객체에 저장된 정보에 이어서 접근하거나 해당 정보를 조작하기 전에 수행됩니다. 일반적인 패턴은 다음과 같은 형태일 수 있습니다.:

def traverse_tree(node):
    if isinstance(node, Node):
        traverse_tree(node.left)
        traverse_tree(node.right)
    elif isinstance(node, Leaf):
        print(node.value)

많은 경우 클래스 패턴은 동기 부여에서 제시된 예에서처럼 중첩되어 나타납니다.:

if (isinstance(node, BinOp) and node.op == "+"
        and isinstance(node.right, BinOp) and node.right.op == "*"):
    a, b, c = node.left, node.right.left, node.right.right
    # Handle a + b*c

클래스 패턴을 사용하면 인스턴스 검사와 관련 속성(추가 제약 조건이 있을 수 있음)을 모두 간결하게 지정할 수 있습니다. 따라서 위의 첫 번째 경우에는 case Node(left, right):를, 두 번째 경우에는 case Leaf(value):를 작성하고 싶은 유혹을 강하게 받습니다. 이는 엄격한 대수적 데이터 타입을 사용하는 언어에서는 실제로 잘 작동하지만, Python 객체의 구조에서는 문제가 됩니다.

일반적인 Python 객체를 다룰 때는 순서가 정해지지 않은 속성이 잠재적으로 매우 많다는 문제에 직면합니다. Node의 인스턴스에는 많은 속성이 포함되며, 그 대부분은 __repr__과 같은 ‘특수 메서드’입니다. 게다가 인터프리터는 속성의 순서를 안정적으로 추론할 수 없습니다. 예를 들어 원을 나타내는 객체의 경우, x, yradius라는 속성의 순서에는 본질적으로 명백한 기준이 없습니다.

이 문제를 처리하는 방법으로 두 가지 가능성을 고려할 수 있습니다. 관심 있는 속성을 명시적으로 이름을 지정하거나, 어떤 속성을 어떤 순서로 추출할지 인터프리터에 알려 주는 추가 매핑을 제공하는 것입니다. 두 접근 방식 모두 지원됩니다. 또한 관심 있는 속성을 명시적으로 이름 지정하면 객체에 필요한 구조를 더욱 구체적으로 지정할 수 있습니다. 객체에 패턴으로 지정된 속성이 없으면 매칭이 실패합니다.

  • 명시적으로 이름이 지정된 속성에는 명명된 인자의 구문이 적용됩니다. 위와 같이 Node클래스의 객체에 leftright라는 두 속성이 있으면, Node(left=x, right=y)패턴은 두 속성의 값을 추출하여 각각 xy에 할당합니다. 왼쪽에서 오른쪽으로 흐르는 데이터 흐름은 특이해 보이지만, 매핑 패턴과 일치하며 with- 또는 import-문에서 as를 통한 할당(실제로 AS 패턴)과 같은 선례가 있습니다.

    문제의 속성을 명시적으로 이름 붙이는 방식은 위치 형식(아래)이 불충분한 더 복잡한 경우에 주로 사용됩니다.

  • 클래스 필드 __match_args__는 여러 속성과 그 순서를 지정하므로, 해당 속성의 이름을 명시적으로 지정하지 않고도 클래스 패턴이 위치 하위 패턴에 의존할 수 있습니다. 이는 관심 있는 속성이 매우 명확하고 대개 잘 정의된 순서를 가지는 작은 객체나 데이터 클래스의 인스턴스에 특히 유용합니다. 어떤 면에서 __match_args__는 형식 매개변수 선언과 유사하며, 모든 매개변수의 이름을 지정하는 대신 위치 인자로 함수를 호출할 수 있게 합니다.

    이는 클래스 속성입니다. 클래스 패턴에 지정된 클래스에서 조회해야 하며 대상 인스턴스에서 조회해서는 안 되기 때문입니다.

클래스 패턴의 구문은 분해가 구성의 구문을 반영한다는 개념에 기반합니다. 이는 할당 대상, 함수 정의 또는 이터러블 언패킹을 막론하고 거의 모든 Python 구성에서 이미 적용되는 방식입니다. 이러한 모든 경우에 데이터를 보내는 구문과 데이터를 받는 구문이 거의 동일하다는 것을 알 수 있습니다.

  • 변수, 속성 및 첨자와 같은 할당 대상: foo.bar[2] = foo.bar[3];
  • 함수 정의: def foo(x, y, z=6)로 정의된 함수는 예를 들어 foo(123, y=45)처럼 호출되며, 호출 위치에서 제공된 실제 인자는 정의 위치의 형식 매개변수와 대응됩니다;
  • 이터러블 언패킹: a, b = b, a 또는 [a, b] = [b, a] 또는 (a, b) = (b, a), 동등한 몇 가지 가능성만 예로 들면 다음과 같습니다.

읽기와 쓰기, l값과 r값 또는 구성과 분해에 동일한 구문을 사용하는 방식은 데이터와 데이터의 흐름 및 조작을 이해하는 데 유익하다는 점에서 널리 받아들여지고 있습니다. 이는 인스턴스를 명시적으로 구성하는 경우에도 동일하게 적용되며, 클래스 패턴 C(p, q)는 의도적으로 인스턴스를 생성하는 구문을 반영합니다.

내장 클래스 bool, bytearray등의 특수한 경우(예를 들어 str(x)가 대상 값을 x에 담는 경우)는 다음과 같이 사용자 정의 클래스로 모방할 수 있습니다:

class MyClass:
    __match_args__ = ["__myself__"]
    __myself__ = property(lambda self: self)

패턴 변수의 타입 어노테이션. 제안은 패턴을 타입 어노테이션과 결합하는 것이었습니다:

match x:
    case [a: int, b: str]: print(f"An int {a} and a string {b}:")
    case [a: int, b: int, c: int]: print("Three ints", a, b, c)
    ...

이 아이디어에는 많은 문제가 있습니다. 우선 콜론은 대괄호나 괄호 안에서만 사용할 수 있으며, 그렇지 않으면 구문이 모호해집니다. 또한 Python에서는 제네릭 타입에 대한 isinstance()검사를 허용하지 않으므로, 제네릭을 포함하는 타입 어노테이션은 예상대로 작동하지 않습니다.

역사와 맥락

패턴 매칭은 1970년대 후반에 튜플 언패킹의 형태로 등장했으며, 연결 리스트나 트리와 같은 재귀적 데이터 구조를 처리하는 수단으로도 등장했습니다(객체 지향 언어에서는 일반적으로 재귀적 데이터 구조를 처리할 때 방문자 패턴을 사용합니다). 패턴 매칭의 초기 옹호자들은 구조화된 데이터를 Cstruct나 이후에 도입된 객체가 아니라 ‘태그가 붙은 튜플’로 구성했습니다. 예를 들어 이진 트리의 노드는 왼쪽 및 오른쪽 가지를 각각 나타내는 두 요소와 Node 태그를 가진 튜플이며, Node(left, right)로 작성됩니다. Python에서는 동일한 효과를 얻기 위해 태그를 ('Node', left, right)와 같이 튜플 안에 넣거나 데이터 클래스 Node를 정의할 것입니다.

현대적인 구문을 사용하면 깊이 우선 트리 순회는 다음과 같이 작성됩니다:

def traverse(node):
    match node:
        case Node(left, right):
            traverse(left)
            traverse(right)
        case Leaf(value):
            handle(value)

패턴 매칭으로 재귀적 데이터 구조를 처리한다는 개념은 곧 더 일반적인 재귀적 ‘패턴’(즉, 재귀적 데이터 구조를 넘어선 재귀)을 패턴 매칭으로 처리한다는 발상으로 이어졌습니다. 따라서 패턴 매칭은 다음과 같은 재귀 함수를 정의하는 데에도 사용됩니다:

def fib(arg):
    match arg:
        case 0:
            return 1
        case 1:
            return 1
        case n:
            return fib(n-1) + fib(n-2)

패턴 매칭이 새롭게 등장하는 프로그래밍 언어에 반복적으로 통합되면서 그 구문은 조금씩 발전하고 확장되었습니다. 위의 fib예제에서 처음 두 경우는 case 0 | 1:처럼 더 간결하게 작성할 수 있으며, 여기서 |는 대안 패턴을 나타냅니다. 또한 밑줄 _은 와일드카드로 널리 채택되었으며, 패턴의 일부에서 구조나 값 어느 쪽도 중요하지 않을 때 사용하는 채움 요소입니다. 밑줄은 Python의 이터러블 언패킹에서도 이미 동일한 용도로 자주 사용되므로(예: _, _, third, _* = something), 이러한 보편적인 관례를 유지했습니다.

패턴 매칭이라는 개념이 함수라는 개념과 항상 밀접하게 연결되어 왔다는 점은 주목할 만합니다. 서로 다른 case 절은 패턴 변수가 매개변수의 역할을 하는 준독립적인 함수와 같은 것으로 항상 간주되어 왔습니다. 이는 패턴 매칭을 다음과 같은 형태의 오버로드된 함수로 작성할 때 가장 분명하게 드러납니다(Standard ML).:

fun fib 0 = 1
  | fib 1 = 1
  | fib n = fib (n-1) + fib (n-2)

Python에서는 이러한 case 절을 독립적인 함수로 엄격하게 분리하는 방식이 적용되지는 않지만, 패턴이 매개변수와 많은 구문 규칙을 공유한다는 것을 알 수 있습니다. 예를 들어 인자를 한정되지 않은 이름에만 바인딩하거나, 특정 패턴/함수에서 변수 이름과 매개변수 이름을 반복해서 사용해서는 안 된다는 규칙이 이에 해당합니다.

추상화와 캡슐화를 중시하는 객체 지향 프로그래밍은 패턴 매칭에 심각한 도전 과제를 제기했습니다. 간단히 말해, 객체 지향 프로그래밍에서는 더 이상 객체를 태그가 붙은 튜플로 볼 수 없습니다. 생성자에 전달되는 인자가 객체의 속성이나 필드를 반드시 지정하는 것은 아닙니다. 게다가 객체 필드의 엄격한 순서도 더 이상 존재하지 않으며, 일부 필드는 비공개이므로 접근할 수 없을 수도 있습니다. 또한 주어진 객체가 구조가 약간 다른 서브클래스의 인스턴스일 수도 있습니다.

이러한 문제를 해결하기 위해 패턴은 원래의 튜플 생성자와 점점 더 독립적인 형태가 되었습니다. Node(left, right)와 같은 패턴에서 Node는 더 이상 수동적인 태그가 아니라, 주어진 객체가 올바른 구조를 갖추었는지 능동적으로 확인하고 leftright 필드를 추출할 수 있는 함수입니다. 다시 말해, Node-태그는 객체를 튜플로 변환하거나, 변환할 수 없는 경우 일부 실패 표시를 반환하는 함수가 됩니다.

Python에서는 isinstance()를 클래스의 __match_args__필드와 함께 간단히 사용하여 객체가 올바른 구조를 갖추었는지 확인한 다음 일부 속성을 튜플로 변환합니다. 예를 들어 위의 Node 예제에서는 이 두 속성을 추출하여 튜플을 구성해야 함을 나타내기 위해 __match_args__ = ('left', 'right')로 지정합니다. 즉, case Node(x, y)는 먼저 주어진 객체가 Node의 인스턴스인지 확인한 다음 leftx에, righty에 각각 할당합니다.

그러나 ‘덕 타이핑’을 통한 Python의 동적 특성에 경의를 표하여, 특정 속성의 존재 여부나 제약 조건을 지정하는 더 직접적인 방법도 추가했습니다. Node(x, y) 대신 object(left=x, right=y)라고 작성할 수도 있으며, 이렇게 하면 isinstance() 확인을 사실상 제거하여 leftright 속성을 가진 모든 객체를 지원합니다. 또는 이러한 아이디어를 결합하여 Node(right=y)를 작성함으로써 Node의 인스턴스를 요구하면서 right 속성의 값만 추출하도록 할 수도 있습니다.

하위 호환성

이 제안은 “소프트 키워드”와 새로운 PEG 파서(PEP 617)를 사용하므로 완전한 하위 호환성을 유지합니다. 그러나 LL(1) 파서를 사용하여 Python 소스 코드를 구문 분석하는 타사 도구는 동일한 기능을 지원하기 위해 파서 기술을 변경해야 할 수 있습니다.

보안 관련 사항

이 언어 기능으로 인한 보안 관련 영향은 없을 것으로 예상합니다.

참조 구현

기능이 완전히 구현된 CPython 구현을 GitHub에서 사용할 수 있습니다.

위 구현을 기반으로 하는 대화형 플레이그라운드가 Binder [2]와 Jupyter [3]를 사용하여 만들어졌습니다.

참고 문헌