2022년 9월 12일 월요일

파이썬 match case 구조적 패턴 매칭 (3.10이상)

 논란도 많고 말도 많았던, switch-case의 파이썬 버젼입니다. 이게 드디어,파이썬 3.10 부터 지원을 합니다. 대신에 오랫동안 검토가 된 만큼 몇가지 일반적인 switch-case문과는 다른 점들이 있습니다.

기본적인 형식은 아래와 같습니다.

def match_test(cond):
    match cond:
        case 'AAA':
            print ('AAA')
        case 'BBB':
            print ('BBB')
        case 'CCC':
            print ('CCC')
        case _:
            print ('others')

여기서 case _는 다른 언어들의 default와 동일 의미합니다. 아무것도 매치되지 않을 경우 case _의 구문을 수행합니다.

패턴 비교 - 리터럴

기본적인것은 switch와 같이 리터럴(패턴이라고 함)에 하나하나 비교하는 것입니다. 아래 예에서 status가 대상subject이며, 각 case문의 400, 404, 418이 패턴입니다. 401 | 402 | 403 처럼 or 연산은 ‘|’를 이용해서 할 수 있습니다.

def http_error(status):
    match status:
        case 400:
            return "Bad request"
        case 401 | 402 | 403: # 401 or 402 or 403
            return "Permission Denied"
        case 404:
            return "Not found"
        case 418:
            return "I'm a teapot"
        case _:
            return "Something's wrong with the internet"

http_error() 함수는 인자로 받은 status가 각각 400, 404, 418일 때 해당 문자열을 내줍니다. 이때 패턴 매칭은 위에서 아래로 진행됩니다. 일치하는 패턴이 없는 경우는 마지막에 보이는 case _: 구문(_를 와일드카드라고 함)이 적용됩니다.

만일 case _: 구문을 명시하지 않았는데 일치하는 패턴도 없다면 아무 일도 일어나지 않습니다. 공식 문서에서는 No-op, 즉 No operation으로 설명합니다.

case문은 |(파이프)로 아래처럼 여러 개를 하나로 합칠 수도 있습니다. 의미는 or 입니다

이 다만 or로 판정할 경우 구문은 정확히 어느포인트에서 걸렸는지 모르기 때문에 아래와 같이 as구문을 사용해서 값을 받을수 있습니다. 단순 리터럴 보다는 Expr식 같은 부분에 사용하면 할당값을 알아낼 수 있습니다

case 401 | 403 | 404 as code:
    return "Not allowed"

리터럴과 변수가 함께 포함된 패턴

여기서 부터가 다른 언어와는 좀 다른 부분입니다. 언패킹 (unpacking)과 비슷한 방식이며, 하나의 객체를 여러개로 언팩이 가능합니다. 변수를 바인딩할 때 패턴을 사용할 수도 있습니다. 아래 예에서 어떤 점, point는 x 좌표와 y 좌표로 언패킹됩니다.

#point = 10, 10
def print_point(point):
    match point:
        case (0, 0):
            print("Origin")
        case (0, y):
            print(f"Y={y}")
        case (x, 0):
            print(f"X={x}")
        case (x, y):
            print(f"X={x}, Y={y}")
        case _:
            raise ValueError("Not a point")

print_point((0, 0)) # Origin
print_point((4, 0)) # X = 4
print_point([0, 2]) # Y = 2
 
# unpacking이 가능하다면 어떤 꼴이든 상관이 없습니다.
print_point([2] + [4]) # X = 2, Y = 4
print_point([x for x in range(2)]) # Y = 1
print_point(("1", "2")) # X = 1, Y = 2
print_point([1+2, 300+20]) # X = 3, Y = 320
print_point("some thing".split()) # X = some, Y = thing
 
# unpacking이 불가능하거나 어떤 case에도 걸리지 않으면 ValueError와 마주칩니다.
print_point([1])
print_point([1, 0, 3])
print_point(2, 1) # 두 개의 인자가 들어가면 안됩니다!

결과 : 

Origin
X=4
Y=2
X=2, Y=4
Y=1
X=1, Y=2
X=3, Y=320
X=some, Y=thing
Traceback (most recent call last):
  File "C:\Users\xxx\PycharmProjects\pythonProject1\main.py", line 29, in <module>
    print_point([1])
  File "C:\Users\xxx\PycharmProjects\pythonProject1\main.py", line 14, in print_point
    raise ValueError("Not a point")
ValueError: Not a point

아래는 이 코드를 실행한 결과입니다. 직관적이죠?

구체적으로 살펴보겠습니다. 첫 번째 패턴인 (0, 0)은 리터럴이 두 개입니다. 그다음 두 개의 패턴에는 리터럴과 변수가 섞여 있습니다. 이 변수는 대상, 즉 point의 값을 바인딩합니다. 네 번째 패턴은 두 개의 값을 캡처합니다. 개념적으로는 언패킹 대입문, 즉 (x, y) = point와 비슷한 거죠.

패턴과 클래스

클래스 이름에 argument list를 포함시켜서 패턴으로 이용할 수도 있습니다. 이렇게 하면 클래스 속성을 변수로 캡처할 수 있습니다.

class Point:
    x: int
    y: int

def location(point):
    match point:
        case Point(x=0, y=0):
            print("Origin is the point's location.")
        case Point(x=0, y=y):
            print(f"Y={y} and the point is on the y-axis.")
        case Point(x=x, y=0):
            print(f"X={x} and the point is on the x-axis.")
        case Point():
            print("The point is located somewhere else on the plane.")
        case _:
            print("Not a point")

Wildcard(*) 를 리스트에 넣는 경우

아래와 같이 둘 이상의 리스트가 들어오는 경우, 이것들을 아래와 같이 iterator로 처리할수 있습니다.

def alarm(item):
    match item:
        case [time, action]:
            print(f'Good {time}! It is time to {action}!')
        case [time, *actions]:
            print('Good morning!')
            for action in actions:
                print(f'It is time to {action}!')

alarm(['afternoon', 'work'])
alarm(('morning', 'have breakfast', 'brush teeth', 'work'))

출력:

Good afternoon! It is time to work!
Good morning!
It is time to have breakfast!
It is time to brush teeth!
It is time to work!

Sub-patterns

패턴에 패턴을 가지는 경우, 정해진 패턴에서 반응하는 경우 아래 코드에서는 괄호를 이용하여 일치시키려는 “패턴”을 둘러싸고 파이프(|)를 사용하여 이러한 패턴을 분리 할 수 있음을 보여줍니다.

def alarm(item):
    match item:
        case [('morning' | 'afternoon' | 'evening'), action]:
            print(f'Good (?)! It is time to {action}!')
        case _:
            print('The time is invalid.')

alarm(['arvo','work'])
alarm(['afternoon','work'])

이건 다른예입니다, list안에 class도 들어갈수 있습니다. 패턴은 어떤 식으로도 중첩할 수 있습니다. 아래는 점 리스트인 points를 대조하는 예입니다.

match points:
    case []:
        print("No points in the list.")
    case [Point(0, 0)]:
        print("The origin is the only point in the list.")
    case [Point(x, y)]:
        print(f"A single point {x}, {y} is in the list.")
    case [Point(0, y1), Point(0, y2)]:
        print(f"Two points on the Y axis at {y1}, {y2} are in the list.")
    case _:
        print("Something else is found in the list.")

조건이 추가된 분기, 가드

아래와 같이 if문을 붙일수도 있다. 패턴에 if절을 넣을 수 있는데, 이를 가리켜 가드라고 부릅니다. 가드가 거짓이면 match문은 순서상 그다음 case문을 실행합니다. 이때 값이 먼저 캡처된 후에 가드의 참, 거짓이 판단됩니다.

def alarm(item):
    match item:
        case ['evening', action] if action not in ['work', 'study']:
            print(f'You almost finished the day! Now {action}!')
        case ['evening', _]:
            print('Come on, you deserve some rest!')
        case [time, action]:
            print(f'Good {time}! It is time to {action}!')
        case _:
            print('The time is invalid.')

출력:

alarm(['evening','work'])
alarm(['evening','drive'])
alarm(['afternoon','work'])

Come on, you deserve some rest!
You almost finished the day! Now drive!
Good afternoon! It is time to work!

아래는 다른 예제입니다.

match point:
    case Point(x, y) if x == y:
        print(f"The point is located on the diagonal Y=X at {x}.")
    case Point(x, y):
        print(f"Point is not on the diagonal.")

복잡한 패턴과 와일드카드(_)

일반적으로 와일드카드(_)는 맨 아래 case문에 단독으로 사용합니다. 그런데 다음처럼 사용할 수도 있습니다.

match test_variable:
    case ('warning', code, 40):
        print("A warning has been received.")
    case ('error', code, _):
        print(f"An error {code} occurred.")

여기서 test_variable은 (‘error’, code, 100)과 (‘error’, code, 800)의 경우에 해당합니다.

패턴 매칭의 패턴

Example

Description

case “a”

단일 값 “a”에 대해 매칭한다.

case [“a”, “b”]

집합 [“a”, “b”]에 대해 매칭한다.

case [“a”, value]

2개 값을 가진 집합에 대해 매칭을 하고,
2번째 값을 캡처 변수인 value에 위치시킨다.

case [“a”, *values]

1개 이상의 값을 가진 집합에 대해 매칭한다. 다른 값들은 values에 저장한다.
집합 당 별표 표시된 항목을 하나만 포함할 수 있다
(파이썬 함수에서 별표 인수를 사용할 수 있기 때문임).

case (‘a’|’b’|’c’)

or 연산자(

case (‘a’|’b’|’c’) as letter

매칭한 항목을 변수 letter에 넣는 것을 제외하고는 위와 동일하다.

case [‘a’, value] if <expression>

expression이 ‘참’인 경우에만 캡처를 매칭한다. 표현식에 캡처 변수를 사용할 수 있다.
예를 들어 if value in valid_values를 사용한다면, case는 캡처한 값 value가 집합인 valid_values에 실제 위치한 경우에만 유효하다.

case [“z”,_]

“z” 로 시작하는 모든 항목의 집합이 매칭한다

결론

match state문의 용법은 기본적으로 코드를 간결하게 해주는 목적이 있습니다. if-else문으로는 최대한 비슷하게 하더라도 가독성면에서는 좋지 않은 부분이 있기 때문에 match문은 나름 이를 해결할 방법중의 하나이기도 하고, 리스트라든지 클래스등을 받을수 있기때문에 활용도 측면에서도 나쁘지 않을것으로 기대합니다.

댓글 없음:

댓글 쓰기