콘텐츠로 건너뛰기
Home » 파이썬 패턴매칭

파이썬 패턴매칭

파이썬 3.10에서는 구조적 패턴 매칭이라는 새로운 문법이 도입된다. 패턴매칭은 match 문이라는 새로운 구문에 적용된며, 스칼라 같은 다른 언어들에서 영향을 받았다고 한다. match 문은 어떤 값(subject)을 여러 다른 형태(pattern)들과 들어맞을 때 까지 비교한다. 이는 C와 같은 언어의 switch 문과 같은 것이 아니냐고 할 수 있겠지만, switch 문이 여러 케이스의 값을 비교하는 것과 달리, match 문은 값에 대해서 패턴에 들어맞는지를 보는 것이 차이라 하겠다.

파이썬 match 문에서 주어진 값은 다음과 같은 패턴 중 하나를 case 절에 사용하는 것으로 체크할 수 있게 된다.

  • 임의의 단일 값 : C의 switch-case와 같이 주어진 값이 해당 값에 일치하는지를 체크한다.
  • 튜플이나 리스트 : 언패킹이 가능한 연속열이 패턴으로 제시되면, 주어진 값을 언패킹하여 각각의 요소들을 비교한다. 패턴 내 요소가 값이면 일치하는지 검사하고, 변수명이라면 요소 값을 바인딩한다.
  • 키-값 매핑 : 주어진 객체가 패턴으로 제시된 사전과 동일한 키가 있는지, 그리고 그 때의 값은 일치하는지를 검사한다. 값 자리에 변수명이 왔다면 바인딩한다.
  • 어떤 클래스와 그 속성 (주로 @dataclass를 통해서 정의된 클래스)
  • 와일드 카드

다음과 같은 것들이 실제 패턴으로 사용될 수 있는 형태의 예이다.

  • [first, second, *rest]
  • Point2d(x, 0)
  • {"name": "Bruce", "age": age}
  • 42

조금 더 자세한 예를 살펴보자. 다음 예는 좌표를 표현할 수 있는 여러 값을 받아서 3차원 좌표 객체를 생성하는 예를 표현하는 것이다.

def make_point_3d(pt):
    match pt:
        case (x, y):  # pt가 2개짜리 튜플이라면 값을 x, y에 바인딩한다.
            return Point3d(x, y, 0)
        case (x, y, z):  # pt가 3개짜리 튜플이라면 값을 x, y, z에 바인딩한다.
            return Point3d(*pt)
        case Point2d(x, y):  # Point2d 인스턴스라면
            # __init__()의 인자 두 개를 x, y에 매칭한다. 
            return Point3d(x, y, 0)
        case Point3d(_, _, _):  # Point3d 인스턴스이기만 하다면, 속성값은 상관없다.
            return pt
        case _:
            raise TypeError("not a point we support")

또 다른 예를 살펴보자. 사용자로부터 문자열로 된 명령을 처리해야 하는 케이스를 가정해보자. command는 사용자가 입력하는 명령으로 “quit”, “look” 과 같이 한 단어로 된 명령이거나, 두 단어로 된 “get paper”, “go west” 같은 패턴일 수도 있다. 다음 코드는 패턴에 따라서 작동하는 분기를 보여준다. 만약 if 문만으로 하려했다면 상당히 귀찮고 긴 코드가 될 수 있을 것이나, 패턴 매칭으로는 아주 간단하게 분기를 처리할 수 있다.

match command.split():
    case ["quit"]:
        print("Goodbye!")
        quit_game()
    case ["look"]:
        current_room.describe()
    case ["get", obj]:
        character.get(obj, current_room)
    case ["go", direction]:
        current_room = current_room.neighbor(direction)
    case _:
        pass

OR 매칭 및 서브 패턴

패턴에는 OR 매칭이 가능하다. | 를 사이에 두고 패턴을 구분하여, 나열된 패턴 중 하나에 매치하도록 할 수 있다. 앞선 예에서 “go north”와 “north”를 같은 명령으로 처리하려면 다음과 같이 할 수 있을 것이다.

match command.split():
    case ["north"] | ["go", "north"]:
        current_room = current_room.neighbor("north")
    # ....

|는 패턴 사이 뿐만아니라, 패턴 내의 서브 패턴을 구분하는데에도 사용될 수 있다. 4개의 방향에 대해서 “go” 명령을 쓰려면 다음과 같이 패턴을 정의해도 된다.

match command.split():
    case ["go", ("north" | "south" | "east" | "west")]:
        # ....

그런데 위 패턴에서 문제는 방향 값을 매칭을 했지만, 그 아래 코드 블럭에서 어떤 방향이 매치되었는지를 알 수 없다는 문제가 있다. as를 사용해서 서브 패턴을 별도의 변수에 바인딩하면 매칭된 값을 변수로 사용할 수 있게 된다.

match command.split():
    case ["go", ("north" | "south" | "east" | "west") as direction]:
        current_room = current_room.neighbor(direction)

가드

패턴이 매치됐더라도, 특정한 요소의 허용 범위 같은 게 있을 수 있다. 이 경우에는 if 절을 추가하여 추가로 검사할 수 있다. 가드절이 있는 경우에는 가드의 조건까지 통과해야 매치가 완료된 것으로 본다. 아래는 위의 예와 같은 것인데, avaiable_direction 이라는 별도의 리스트에 가능한 방향들을 정의해두고, 유효한 방향 값이 들어왔을 때에만 작동하도록 할 수 있다.

match command.split():
    case ["go", direction] if direction in\
                              current_room.available_direction:
        current_room = current_room.neighbor(direction)

정리

기본적으로 값, 클래스, 연속열, 맵핑의 케이스로 구분되는 패턴들은 다시 패턴속에 패턴이나, as 바인딩, if절에 의한 가드 등으로 얼마든지 확장할 수 있기 때문에 잘 활용한다면 매우 편하게 쓸 수 있을 것 같다.

3.10의 베타가 나온 지금도 (파이썬은 베타에 접어들면 기능에 대한 목록은 확정됐다고 본다) 패턴 매칭에 대한 찬반 논의는 많이 있다. 개인적으로는 다른 함수형 언어에서 볼 수 있던 패턴 매칭 기능이 파이썬에서 적용된다고 해서 매우 반기는 입장이다.