47.16 구조적 패턴 매칭 사용하기

구조적 패턴 매칭(Structural Pattern Matching)은 C, C++의 switch, case와 유사한 문법이며 좀더 강력한 기능을 제공합니다.

구조적 패턴 매칭은 파이썬 3.10 이상부터 사용 가능

match :
    case 패턴:

지금까지 파이썬에서는 여러 조건을 검사하기 위해 if, elif, else를 사용해야 했습니다. 먼저 if, elif, else로 음료수 자판기를 만들어보겠습니다.

button = int(input())
 
if button == 1:
    print('콜라')
elif button == 2:
    print('사이다')
elif button == 3:
    print('환타')
else:
    print('제공하지 않는 메뉴')

실행 결과

1 (입력)
콜라

button의 값이 1, 2, 3인지 매번 if, elif로 조건식을 작성해야 했습니다. 이제 match, case를 활용하여 좀더 간단하게 여러 조건을 판단할 수 있습니다.

match_case_literal.py

button = int(input())
 
match button:
    case 1:
        print('콜라')
    case 2:
        print('사이다')
    case 3:
        print('환타')
    case _:
        print('제공하지 않는 메뉴')

실행 결과

1 (입력)
콜라

match에 검사하려는 변수를 지정하고, case에는 1, 2, 3과 같이 값을 지정해주면 됩니다(리터럴 패턴). 여기서 1, 2, 3에 해당하지 않는 값을 처리하고 싶을 때는 case _:과 같이 _를 사용하면 됩니다(와일드카드 패턴).

이렇게 case에 지정하는 식을 패턴이라고 부르는데, 단순한 값(리터럴 패턴)뿐만 아니라 시퀀스(리스트, 튜플), 클래스, 딕셔너리, 조건 등도 지정할 수 있습니다.

다음은 case에서 시퀀스 패턴, OR 패턴, 캡쳐 패턴을 사용하는 방법입니다.

match_case_sequence_or_capture.py

values = [
    ['hello'],
    ['Python', '3.x'],
    ['a'],
    ['x', 'y'],
    ['z', 100, 200, 300]
]
 
for value in values:
    match value:
        case ['hello']:           # 'hello' 매칭, 시퀀스 패턴
            print(value)
        case ['Python', '3.x']:   # 'Python'과 '3.x' 모두 매칭, 시퀀스 패턴
            print(value)
        case ['a'] | ['b']:       # 'a' 또는 'b' 매칭, OR 패턴
            print('a or b')
        case ['x', obj]:          # 'x'가 매칭되었을 때 두 번째 값을 obj에 캡쳐, 캡쳐 패턴
            print(f'x, {obj}')
        case ['z', *rest]:        # 매칭되지 않은 나머지 요소를 rest에 저장, 시퀀스 패턴
            print(f'z, {rest}')

실행 결과

['hello']
['Python', '3.x']
a or b
x, y
z, [100, 200, 300]

case에는 case ['hello']:처럼 문자열 한 개가 들어있는 리스트를 지정할 수도 있고, case ['Python', '3.x']:처럼 문자열 두 개가 들어있는 리스트를 지정할 수도 있습니다(시퀀스 패턴). 이때 case ['Python', '3.x']:은 두 문자열이 모두 매칭되었을 때 실행되며, 둘 중 하나라도 매칭되었을 때 실행하고 싶으면 case ['a'] | ['b']:처럼 |를 넣어서 OR 패턴을 사용할 수도 있습니다. 그리고 case ['x', obj]:'x'가 매칭되었을 때 두 번째 값을 캡쳐하여 obj에 저장합니다(캡쳐 패턴). 특히 case ['z', *rest]:처럼 리스트 안에서 *를 붙여주면 매칭되지 않은 나머지 요소를 변수 rest에 저장해줍니다(시퀀스 패턴).

다음은 case에 딕셔너리를 지정하는 방법입니다(매핑 패턴).

match_case_mapping.py

values = [
    {'hello': 'world'},
    {'a': 1, 'b': 2, 'c': 3},
    {'x': 10, 'y': 20, 'z': 30}
]
 
for value in values:
    match value:
        case {'hello': 'world'}:
            print('hello, world')
        case {'a': 1, 'b': 2} as ab:  # 매칭된 값을 ab에 저장, AS 패턴
            print(ab)
        case {'y': 20, **rest}:  # 매칭되지 않은 나머지 키를 rest에 저장, 매핑 패턴
            print(rest)

실행 결과

hello, world
{'a': 1, 'b': 2, 'c': 3}
{'x': 10, 'z': 30}

다른 방법과 마찬가지로 case {'hello': 'world'}처럼 case에 매칭하고자 하는 딕셔너리를 지정해주면 됩니다. case {'a': 1, 'b': 2} as ab:은 매칭된 값을 변수 ab에 저장하는 방법입니다(AS 패턴). 그리고 case {'y': 20, **rest}:와 같이 딕셔너리 안에서 **를 붙여주면 매칭되지 않은 나머지 키를 변수 rest에 저장해줍니다(매핑 패턴).

이번에는 클래스 패턴을 사용하는 방법입니다.

match_case_class.py

class Point2D:
    __match_args__ = ('data', 'position')  # 위치 인수 사용, 매칭 순서 설정
    def __init__(self, position, data):
        self.position = position
        self.data = data
 
points = [
    Point2D((10, 20), 'hello'),
    Point2D((300, 400), 'world'),
    Point2D((70, 80), 'Python')
]
 
for point in points:
    match point:
        case Point2D('Python', (70, 80)):      # __match_args__의 순서대로 속성 지정
            print(point.position, point.data)
        case Point2D('world', value):          # 매칭된 값의 속성 position을 value에 저장
            print(value, point.data)
        case Point2D(position=(10, 20)):       # __match_args__의 순서에 따라 매칭되지 않음
            print(point.position, point.data)
        case Point2D(data='hello'):            # __match_args__의 순서에 따라 먼저 매칭됨
            print(point.position, point.data)

실행 결과

(10, 20) hello
(300, 400) world
(70, 80) Python

먼저 클래스 패턴은 다음과 같이 클래스를 정의하여 match, case에 사용할 수 있습니다.

class Point2D:
    __match_args__ = ('data', 'position')  # 위치 인수 사용, 매칭 순서 설정
    def __init__(self, position, data):
        self.position = position
        self.data = data

Point2D 클래스의 인스턴스를 생성하면서 속성 positiondata를 저장하도록 만들었습니다. 특히, __match_args__match, case를 위한 문법이며 위치 인수(positional argument)를 사용할 수 있게 해주고, 매칭 순서를 설정합니다.

case에서는 case Point2D('Python', (70, 80)):과 같이 __match_args__의 순서대로 매칭할 속성을 지정해줍니다. 만약, __match_args__를 설정하지 않았다면 case Point2D(data='Python', position=(70, 80)):과 같이 키워드 인수(keyword argument)만 사용할 수 있습니다. case Point2D('world', value):__match_args__의 순서에 따라 속성 data'world'인 값을 매칭하고, 매칭된 값의 속성 positionvalue에 저장하는 방법입니다.

특히 match에 일치하는 조건의 case가 여러 개 있을 경우 __match_args__의 순서에 따라 매칭이 됩니다. 즉, __match_args__ = ('data', 'position')로 설정이 되어 있으므로, case Point2D(position=(10, 20)):과 Point2D(data='hello'): 중에서 Point2D(data='hello'):이 먼저 매칭됩니다.

마지막으로 case에 조건을 지정하는 방법입니다.

match_case_condition.py

class Point2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y
 
points = [
    Point2D(10, 10),
    Point2D(10, 20)
]
 
for point in points:
    match point:
        case Point2D(x=x, y=y) if x == y:  # 캡쳐된 x와 y가 같을 때
            print(f'{x}, {y}')

실행 결과

10, 10

클래스 Point2D는 속성 xy를 저장하도록 정의했습니다. 그리고 case Point2D(x=x, y=y) if x == y:과 같이 case에 조건을 지정할 수 있습니다. 먼저 Point2D(x=x, y=y)에서 속성 x는 변수 x에, 속성 y는 변수 y에 캡쳐하여 저장합니다. 그리고 if x == y와 같이 변수 xy가 같을 때만 매칭되도록 작성할 수 있습니다. 이때 if를 가드(guard)라고 부릅니다.