본문 바로가기

match문

1. match문 사용하기

match문은 Python 3.10부터 도입된 패턴 매칭 구문입니다. if-elif보다 가독성이 좋고, '|'로 여러 패턴을 묶거나 '_' 와일드카드로 기본값을 처리할 수 있습니다.

파이썬 3.10 버전부터 추가된 match 문법은 파이썬에 없는 switch문을 사용하기 위해 등장했습니다. 사용자는 match 문법을 이용하여 여러 조건문처럼 여러 경우의 수를 처리할 수 있습니다.

1.1 match문이란?

match문은 여러 조건을 체크하여 해당 조건에 맞는 코드 블록을 실행하는 제어문입니다. switch라는 이름으로 다양한 프로그래밍 언어에서 지원되며, 일반적으로 if-elif-else 문보다 가독성이 좋습니다.

파이썬은 switch 문법을 지원하지 않았습니다. 이로 인해 개발자들은 여러 가지 경우의 수를 처리하기 위해 if-else문이나 dict의 get 메소드를 사용했어야 했습니다. 파이썬에서는 switch문을 if문으로 표현할 수 있으며, 파이썬의 간결성을 해친다고 생각하였습니다. 이러한 논의는 파이썬 3.0부터 진행되어 왔습니다. 보다 자세한 논의는 아래 문서에서 확인할 수 있습니다.

PEP 3103 – A Switch/Case Statement | peps.python.org

오랫동안 이 논의가 활발히 진행되었고 match 문을 Python 3.10 이상에서 사용할 수 있게 되었습니다.

1.2 match 문법 활용 코드

text = 'Hello World'
match text:
    case 'Hello':
        print('Hello')
    case 'World':
        print('World')
    case _:
        print('No Match') # 결과 'No Match'
  • 첫 번째 경우: 'Hello' 문자열과 일치하는지 확인합니다. 일치하지 않으므로 이 경우는 건너뜁니다.
  • 두 번째 경우: 'World' 문자열과 일치하는지 확인합니다. 일치하지 않으므로 이 경우도 건너뜁니다.
  • 세 번째 경우: _는 와일드카드로, 어떤 값과도 일치한다는 것을 나타냅니다. 따라서 이 경우가 실행되며 ‘No Match’라는 문자열이 출력됩니다.

위 코드에서 case _는 일종의 '디폴트 케이스'로 작동하며, 어떠한 값도 받아들일 수 있는 와일드카드 역할을 합니다. 이것은 if-elif-else 구조에서의 else 절과 비슷한 작용을 하는데, 어느 조건도 충족하지 않을 때 해당 코드 블록이 실행됩니다.

즉, 'World'라는 문자열은 앞선 두 케이스 조건, 'Hello'와 'World'에 부합하지 않기 때문에, case _:에 지정된 코드 블록이 실행되어 'No Match'가 출력됩니다. match문에 의해 이 문자열은 평가되며, 해당 조건에 맞는 케이스를 찾아 그에 따른 블록을 실행하게 됩니다.

아래 코드는 text가 ‘World’이기 때문에 case 'World'조건 부합하여 print('World')를 실행합니다.

text = 'World'
match text:
    case 'Hello':
        print('Hello')
    case 'World':
        print('World')
    case _:
        print('No Match')

다른 언어에서 일반적으로 사용되는 구분인 아래와 같은 코드는 Python에서는 작동하지 않습니다.

text = '1'
match text:
    case '1':
    case '2':
        print('1, 2')
    case _:
        print('No Match')

두 조건이 모두 print(’1, 2’)를 수행하길 원했다면 아래와 같은 코드로 작성해야 합니다. 여기서 | 대신 and나 or 연산자는 가능하지 않습니다.

text = '1'
match text:
    case '1' | '2':
        print('1, 2')
    case _:
        print('No Match')

2. match 문법 다양한 예제

다음은 구조적 패턴 매칭을 사용하여 i 변수의 값을 여러 경우에 따라 검사하는 test 함수를 정의하고 있습니다. 여러 case 문을 사용하여 다양한 조건을 검사합니다. 아래 예제에서 case 3 | 4는 '3 또는 4일 때'라는 뜻입니다.

def test(i):
    match i:
        case 1:
            print('One')
        case 2:
            print('Two')
        case 3 | 4:
            print('Three or Four')
        case _ if i > 10:
            print('Greater than ten')
        case _:
            print('Other')

test(1) # 출력: One
test(2) # 출력: Two
test(3) # 출력: Three or Four
test(4) # 출력: Three or Four
test(5) # 출력: Other
test(15) # 출력: Greater than ten

이번에는 값이 아니라 타입에 따라 분기하는 예제입니다. 아래에는 두 개의 함수가 있는데, 하나는 변수 이름 i를 패턴으로 사용했고 다른 하나는 와일드카드 _를 사용했습니다. 둘 다 뒤에 붙은 if 조건으로 실제 판단을 하기 때문에 동작은 같습니다.

여기서 if를 지우면 안 됩니다. case i:나 case _:는 어떤 값이든 받아들이는 패턴이라, 첫 번째 case에서 모든 값을 삼켜버려 뒤의 case에 도달할 수 없기 때문입니다.

def test(i):
    match i:
        case i if type(i) == int:
            print('Integer')
        case i if type(i) == str:
            print('String')
        case i if type(i) == float:
            print('Float')
test(1) # 출력: Integer
test('hello') # 출력: String
test(3.14) # 출력: Float

#############################

def test(i):
    match i:
        case _ if type(i) == int:
            print('Integer')
        case _ if type(i) == str:
            print('String')
        case _ if type(i) == float:
            print('Float')
test(1) # 출력: Integer
test('hello') # 출력: String
test(3.14) # 출력: Float

해당 예제는 i 변수의 값을 여러 경우에 따라 검사하는 test 함수를 정의하고 있습니다. 여러 case문을 사용하여 다양한 조건을 검사합니다. 다음과 같이 case block 내부에서도 if-else 문을 사용할 수 있습니다.

def test(i):
    match i:
        case 1:
            print('One')
        case 2:
            print('Two')
        case _:
            if i > 2:
                print('Greater than two')
            else:
                print('Less than or equal to two')
test(1) # 출력: One
test(2) # 출력: Two
test(3) # 출력: Greater than two
test(4) # 출력: Greater than two

해당 예제에서는 status라는 매개변수를 받아와 그 값이 1, 2, 3 중 하나인지 확인한 후, 해당 값이 맞다면 문자열에 그 값을 포함하여 반환하고, 그렇지 않다면 'hello'를 반환하는 함수를 정의하고 있습니다. as 키워드를 사용하여 변수명으로 사용할 수 있습니다.

def test(status):
    match status:
        case (1 | 2 | 3) as code:
            return f'value : {code}'
        case _:
            return 'hello'
print(test(2))  # 출력: value : 2

해당 예제는 status라는 매개변수의 값을 검사하고, 그 값이 1, 2, 3 중 하나이며 동시에 짝수인지 확인합니다. 해당 조건이 맞다면 'hello'를 반환하고, 그렇지 않다면 'world'를 반환하는 함수를 정의하고 있습니다.

def test(status):
    match status:
        case (1 | 2 | 3) if status % 2 == 0:
            return 'hello'
        case _:
            return 'world'
print(test(3)) # 출력: world

3. 나아가기

match 문법은 조건이 많아질수록 if-elif 나열보다 읽기 편합니다. 성능은 어떨까요? 조건 10개를 두고, 첫 번째 조건에서 바로 걸리는 경우(Best Case)와 마지막 조건까지 가야 하는 경우(Worst Case)를 각각 100만 번씩 측정해보았습니다.

if와 match는 어느 쪽이든 차이가 크지 않습니다. 앞에서 걸릴 때는 if가 조금 빠르고, 끝까지 갈 때는 match가 조금 빠른 정도입니다. 반면 dict.get()은 조건이 몇 번째에 있든 시간이 거의 같습니다. 해시 테이블로 한 번에 찾아가기 때문입니다.

정리하면, 단순히 값 하나를 다른 값으로 바꾸는 분기라면 dict.get()이 가장 유리하고, if와 match 사이에서는 성능보다 가독성을 기준으로 고르시면 됩니다. 아래 측정 결과는 colab 환경에서 얻은 것이며, 실행할 때마다 그리고 환경에 따라 달라질 수 있습니다.

# match, if, dict.get문 성능 비교

테스트 키: key_0
If-Elif 방식 (10 조건): 0.159554 초
Match-Case 방식 (10 조건): 0.170885 초
Dict.get() 방식 (10 항목): 0.205562 초

테스트 키: key_9
If-Elif 방식 (10 조건): 0.496979 초
Match-Case 방식 (10 조건): 0.461650 초
Dict.get() 방식 (10 항목): 0.203502 초

테스트 코드입니다.

import timeit

# 10개의 키-값 쌍 생성
data = {f"key_{i}": f"value_{i}" for i in range(10)}

# 테스트할 키
# Best
# test_key = 'key_0'
# Worst
test_key = 'key_9'

def using_if(key):
    if key == "key_0":
        return "value_0"
    elif key == "key_1":
        return "value_1"
    elif key == "key_2":
        return "value_2"
    elif key == "key_3":
        return "value_3"
    elif key == "key_4":
        return "value_4"
    elif key == "key_5":
        return "value_5"
    elif key == "key_6":
        return "value_6"
    elif key == "key_7":
        return "value_7"
    elif key == "key_8":
        return "value_8"
    elif key == "key_9":
        return "value_9"
    else:
        return "Not found"

def using_match(key):
    match key:
        case "key_0":
            return "value_0"
        case "key_1":
            return "value_1"
        case "key_2":
            return "value_2"
        case "key_3":
            return "value_3"
        case "key_4":
            return "value_4"
        case "key_5":
            return "value_5"
        case "key_6":
            return "value_6"
        case "key_7":
            return "value_7"
        case "key_8":
            return "value_8"
        case "key_9":
            return "value_9"
        case _:
            return "Not found"

def using_dict_get(key):
    return data.get(key, "Not found")

# 벤치마크 함수
def benchmark_if():
    return using_if(test_key)

def benchmark_match():
    return using_match(test_key)

def benchmark_dict_get():
    return using_dict_get(test_key)

# 벤치마크 실행
number = 1000000
if_time = timeit.timeit(benchmark_if, number=number)
match_time = timeit.timeit(benchmark_match, number=number)
dict_get_time = timeit.timeit(benchmark_dict_get, number=number)

print(f"테스트 키: {test_key}")
print(f"If-Elif 방식 (10 조건): {if_time:.6f} 초")
print(f"Match-Case 방식 (10 조건): {match_time:.6f} 초")
print(f"Dict.get() 방식 (10 항목): {dict_get_time:.6f} 초")

# 결과 검증
print("\n결과 검증:")
print(f"If-Elif 결과: {using_if(test_key)}")
print(f"Match-Case 결과: {using_match(test_key)}")
print(f"Dict.get() 결과: {using_dict_get(test_key)}")