개발자

“마침내 파이썬에서도 흐름 제어를” 구조적 패턴 매칭을 사용하는 방법

Serdar Yegulalp | InfoWorld 2023.08.21
파이썬은 강력하고 인기 있는 언어지만, 다른 언어에 있는 흐름 제어, 즉 값을 취해서 가능한 여러 조건 중 하나와 매끄럽게 매칭하는 방법이 오랫동안 없었다. 이것을 C와 C++에서는 switch/case 구조, 러스트에서는 '패턴 매칭(pattern matching)'이라고 한다. 
 
ⓒ Getty Image Bank

파이썬에서 전통적으로 패턴 매칭을 하는 방법은 그리 직관적이지 않다. 하나는 if/elif/else 식 체인을 사용하는 방법이고, 다른 하나는 매칭할 값을 사전에 키로 저장한 다음 이 값을 사용해 예를 들어 함수를 값으로 저장하고 키 또는 다른 변수를 입력으로 사용하는 방법이다. 대부분 잘 작동은 하지만 만들고 유지하기가 번거롭다.

파이썬에 switch/case와 같은 구문을 추가하기 위한 많은 제안이 실패한 후, 최근 파이썬 언어를 만든 귀도 반 로섬과 다른 몇몇 기여자들이 제안한 다른 법이 마침내 파이썬 3.10에 반영됐다. 바로 구조적 패턴 매칭(structural pattern matching)이다. 구조적 패턴 매칭은 간단한 switch/case 스타일의 매칭이 가능할 뿐만 아니라 폭넓은 사용 사례를 지원한다.
 

파이썬 구조적 패턴 매칭 소개 

구조적 패턴 매칭은 파이썬에 match/case 문과 패턴 구문을 도입한다. match/case 문의 기본 개요는 switch/case와 동일하다. 즉, 객체를 받아서 하나 이상의 매칭 패턴을 기준으로 객체를 테스트하고 매칭되는 항목을 찾으면 동작을 수행한다. 
 
match command:
    case "quit":
        quit()
    case "reset":
        reset()
    case unknown_command:
        print (f"Unknown command '{unknown_command}'")

각 case 문 뒤에는 매칭의 기준이 되는 패턴이 온다. 앞선 예제에서는 간단한 문자열을 매칭 타겟으로 사용하지만, 더 복잡한 매칭도 가능하다. 실제로 구조적 패턴 매칭의 주 사용 사례는 값 패턴이 아닌 형식 패턴 매칭이다. 

파이썬은 case 목록을 위에서부터 내려오면서 매칭을 수행한다. 첫 매칭에서 해당 case 블록의 선언을 실행한 다음 match 블록의 끝으로 건너뛰어 프로그램의 나머지 부분으로 진행한다. case 사이에 “폴스루(fall-through)”는 없지만 하나의 case 블록에서 가능한 여러 case를 처리하도록 로직을 설계하는 것은 가능하다. 또한 매칭의 전부 또는 일부를 캡처해서 재사용할 수도 있다. 앞선 예제의 case unknown_command에서 매칭이 없을 경우 값이 unknown_command 변수에 “캡처”되어 재사용할 수 있다. 
 

파이썬 구조적 패턴 매칭 사용해 변수와 매칭하기 

여기서 짚고 넘어가야 할 중요한 내용이 있다. case 문에서 변수 이름을 목록으로 나열한다고 해서 명명된 변수의 내용을 기준으로 매칭이 수행되어야 한다는 의미는 아니다. case의 변수는 매칭되는 값을 캡처하는 데 사용된다. 변수의 내용을 기준으로 매칭하려면 그 변수는 enum과 같이 점으로 구분된 이름으로 표현해야 한다. 예를 들면 다음과 같다. 
 
from enum import Enum
class Command(Enum):
    QUIT = 0
    RESET = 1

match command:
    case Command.QUIT:
        quit()
    case Command.RESET:
        reset()

enum을 꼭 사용할 필요는 없다. 점으로 구분되는 속성 이름이라면 아무거나 된다. enum이 파이썬에서 이 작업을 수행하는 데 있어 가장 친숙하고 자연스러운 방법일 뿐이다. 인덱싱을 통해 변수 내용에 매칭할 수는 없다. 예를 들어 case commands[0]:은 구문 오류로 거부된다. 
 

파이썬 구조적 패턴 매칭을 사용한 여러 요소에 대한 매칭 

패턴 매칭을 가장 효과적으로 사용하기 위한 핵심은 단순한 사전 조회 또는 if/else 체인을 대체하는 용도로 사용하는 것이 아니라, 매칭하고자 하는 대상의 구조를 설명하는 것이다. 이렇게 하면 매칭 대상 요소의 수 또는 그 조합을 기반으로 매칭을 수행할 수 있다. 조금 더 복잡한 예제를 보자. 여기서는 사용자가 명령을 입력하고, 선택적으로 그 뒤에 파일 이름을 붙인다. 
 
command = input("Command:")
match command.split():
    case ["quit"]:
        quit()
    case ["load", filename]:
        load_from(filename)
    case ["save", filename]:
        save_to(filename)
    case _:
        print (f"Command '{command}' not understood")

다음 case를 순서대로 살펴보자. 
 
  • case ["quit"] : 매칭 대상이 입력 분할로 파생되는 "quit" 항목만 있는 목록인지 테스트한다. 
  • case ["load", filename] : 첫 번째 분할 요소가 문자열 "load"인지, 그리고 그 뒤에 두 번째 문자열이 있는지를 테스트한다. 있으면 두 번째 문자열을 filename 변수에 저장해서 추가 작업에 사용한다. case ["save", filename]:도 마찬가지다. 
  •  case _ : 와일드카드 매치. 이 지점에 이르기까지 다른 매칭이 없는 경우 매칭된다. 밑줄 변수(_)는 실제로 어떤 것에도 바인딩되지 않고 match 명령에 해당 case가 와일드카드임을 알리는 신호로 사용된다. 이것이 case 블록의 본문에서 command 변수를 참조한 이유다. 즉, 아무것도 캡처되지 않았다.
 

파이썬 구조적 패턴 매칭의 패턴 

패턴은 단순한 값일 수도 있고 복잡한 매칭 로직을 포함할 수도 있다. 몇 가지 예를 들어보자. 
 
  • case "a" : 단일 값 "a"에 매칭한다. 
  • case ["a","b"] : ["a","b"]라는 컬렉션에 매칭한다. 
  • case ["a", value1] : 두 개의 값이 있는 컬렉션에 매칭하고, 두 번째 값을 캡처 변수 value1에 저장한다. 
  • case ["a", *values] : 하나 이상의 값이 있는 컬렉션에 매칭한다. 다른 값이 있는 경우 그 값은 values에 저장된다. 참고로 컬렉션당 별표 항목은 하나만 포함할 수 있다(파이썬 함수의 별표 인수와 마찬가지). 
  • case ("a"|"b"|"c") : or 연산자(|)를 사용해서 하나의 case 블록에서 여러 case를 처리할 수 있다. 여기서는 "a","b" 또는 "c"에 매칭한다. 
  • 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"로 시작하는 모든 컬렉션 또는 항목이 매칭된다. 
 

파이썬 구조적 패턴 매칭을 사용한 객체에 대한 매칭 

파이썬 구조적 패턴 매칭 시스템의 가장 고급 기능은 특정 속성을 가진 객체에 대한 매칭 기능이다. media_object라는 이름의 객체를 다루는 애플리케이션이 있다고 하자. 이 객체를 .jpg  파일로 변환하고 함수에서 반환하려고 한다. 
 
match media_object:
    case Image(codec="jpg"):
        # Return as-is
        return media_object
    case Image(codec="png") | Image(codec="gif"):
        return render_as(media_object, "jpg")
    case Video():
        raise ValueError("Can't extract frames from video yet")
    case other_type:
        raise Exception(f"Media object {media_object} 
            of type {codec} can't be handled yet")

이 사례의 각 case에서는 특정 종류의 객체, 경우에 따라 특정 속성이 있는 객체를 찾는다. 첫 번째 case는 codec 속성이 “jpg”로 설정된 Image 객체에 매칭한다. 두 번째 case는 type이 “png” 또는 “gif”인 경우 매칭된다. 세 번째 case는 속성에 관계없이 형식이 Video인 객체를 매칭한다. 마지막 case는 다른 모든 방법이 실패할 때의 대비책으로 매칭에 _ 대신 실제 이름을 사용하여 캡처한다. 객체 매칭으로 캡처를 수행할 수도 있다. 
 
match media_object:
    case Image(codec=media_type):
        print (f"Image of type {media_type}")
 

파이썬 구조적 패턴 매칭을 효과적으로 사용하기 

파이썬 구조적 패턴 매칭의 핵심은 매칭 기준으로 사용하려는 구조적 case를 포괄하는 매칭을 작성하는 것이다. 상수에 대한 간단한 테스트도 괜찮지만 그게 전부라면 간단한 사전 조회가 더 나은 선택일 수 있다. 구조적 패턴 매칭의 진정한 가치는 하나의 특정 객체나 객체 모음이 아닌 객체의 패턴에 대해 매칭할 수 있다는 것이다. 

유의해야 할 또 다른 중요한 사항은 매칭의 순서다. 어떤 매칭을 처음 테스트하는지에 따라 전체적인 매칭의 효율성과 정확성이 영향을 받는다. 장황한 if/elif/else 체인을 구축한 사람들은 대부분 이 부분을 인식하겠지만, 패턴 매칭의 경우 잠재적인 복잡성 때문에 순서에 대해 더욱 신중하게 생각해야 한다. 가장 구체적인 매칭을 맨 앞에, 가장 일반적인 매칭을 마지막에 배치한다. 마지막으로, 간단한 if/elif/else 체인이나 사전 조회로 해결할 수 있는 문제가 있다면 그 방법을 사용하는 것이 더 좋다. 패턴 매칭은 강력하지만 만능은 아니다. 문제에 대해 가장 적합한 해결 방법일 때만 사용해야 한다. 
editor@itworld.co.kr
Sponsored

회사명 : 한국IDG | 제호: ITWorld | 주소 : 서울시 중구 세종대로 23, 4층 우)04512
| 등록번호 : 서울 아00743 등록발행일자 : 2009년 01월 19일

발행인 : 박형미 | 편집인 : 박재곤 | 청소년보호책임자 : 한정규
| 사업자 등록번호 : 214-87-22467 Tel : 02-558-6950

Copyright © 2024 International Data Group. All rights reserved.