파이썬도 패턴 매칭( Pattern Matching) 대세에 들어오다

파이썬도 패턴 매칭( Pattern Matching) 대세에 들어오다

시작

프로그래밍 언어에서 패턴 매칭(Pattern matching)을 아시나요? 문자열의 패턴 매칭을 말하는 것이 아닙니다. 파이썬 3.10부터 구조적 패턴 매칭(Structural Pattern Matching)이 도입되었는데요. 프로그래밍 언어별로 어떻게 패턴 매칭을 사용하는지 알아보겠습니다.

패턴 매칭(Pattern Matching)이란

프로그래밍 언어에서 패턴 매칭(pattern matching)이란 데이터가 특정 패턴(값, 자료구조, 타입, 심지어 함수까지)에 일치하는지 따져 대상을 특정하는 기술입니다.

왜 패턴 매칭인가

C/C++, Java에서 조건을 판단할 때 if-else 문이나 switch-case 문을 많이 사용하죠? 직관적이고 자유롭게 쓸 수 있는게 그들의 장점입니다.

하지만 특정 조건이 누락된다면 대처가 안됩니다. 자유로운 문법 때문이죠. else 없이 if 문을 사용하는 사례가 이 상황에 해당합니다. else 키워드를 빠뜨리면 나중에는 코드를 이해하기 어려워 골치 아픈 상황에 직면하기도 합니다.

또한 if 문을 중첩해서 사용하는 경우입니다. 특히 운영 중인 프로그램을 유지보수할 때 중첩된 if 문으로 상황을 모면하려는 상황이 빈번히 나타나죠. 좋지 않는 방법인 줄 모두 압니다. 하지만 긴박한 상황에 떠밀려 어쩔 수 없이 사용하게 됩니다.

함수형 프로그래밍 언어는 패턴 매칭으로 모든 경우의 수를 나열하도록 안내합니다. 특수한 패턴 하나와 그 외 패턴으로 코드 구조를 잡기에 좋습니다. 그러면 의식하지 않고도 예외 상황 고려하게 되죠.

파이썬의 구조적 패턴 매칭(Structural Pattern Matching)은 모든 경우를 강제하지 않습니다. 하지만 if문 중첩으로 이해하기 어려워 질 수 있는 코드를 작성하기 쉽고, 이해하기 쉽고, 유지보수에도 도움을 줍니다. 패턴을 뽑아 나열하면 되거든요.

코드로 알아보는 패턴 매칭의 유용함

함수형 프로그래밍 언어는 대부분 패턴 매칭을 지원합니다. 파이썬(Python)을 중심으로 하스켈(Haskell)과 러스트(Rust)가 어떻게 패턴 매칭을 언어 차원에서 지원하는지 예제로 알아보겠습니다.

조건문if-else를 대체하는 패턴 매칭

JSON 데이터를 읽어 문자열을 반환하는 함수 say 를 만든다고 가정해보겠습니다.

  • JSON은 키(key)로 name과 languages 를 갖고 있습니다.

  • name의 값이 "Choo" 라면 "Oh!, my load, you can do anything." 을 반환

  • 특히 language가 "Korean" 이라면 "{name}은(는) 한국어를 사용합니다."를 반환

  • 다른 경우는 "{name} can speaks {language}."를 반환

  • 언급한 패턴에 일치 하지 않으며 "It's invalid data." 를 반환

하게끔 만들고자 합니다.

조건문 if-else 로 구현한 예

import json

if __name__ == '__main__':
    people = [
        '{"name": "Choo", "language": "Korean"}',
        '{"name": "Kim", "language": "Korean"}',
        '{"name": "Bob", "language": "English"}',
        '{"longitude": 1, "latitude": 2}',
    ]
    for person in people:
        print(say(person))

    # Oh!, my load, you can do anything.
    # Kim는 한국어를 사용합니다..
    # Bob can speaks English.
    # It's invalid data.


def say(person: str) -> str:
    person_dict = json.loads(person)
    if "name" in person_dict:
        if person_dict["name"] == "Choo":
            return "Oh! my load, you can do anything."
        elif "language" in person_dict:
            if person_dict["language"] == "Korean":
                return f"{person_dict['name']}는 한국어를 사용합니다."
            else:
                return f"{person_dict['name']} can speaks {person_dict['language']}."
    else:
        return "It's invalid data."

함수 안에 if 키워드를 세 번 사용해서 구현했습니다. 언뜻 봐도 코드가 눈에 잘 들어오지 않네요. 어떻게 동작하는지 확이하려면 하나씩 따져봐야 할 것 같습니다.

패턴 매칭으로 구현한 예

import json

if __name__ == '__main__':
    people = [
        '{"name": "Choo", "language": "Korean"}',
        '{"name": "Kim", "language": "Korean"}',
        '{"name": "Bob", "language": "English"}',
        '{"longitude": 1, "latitude": 2}',
    ]
    for person in people:
        print(say(person))

    # Oh!, my load, you can do anything.
    # Kim는 한국어를 사용합니다..
    # Bob can speaks English.
    # It's invalid data.


def say(person: str) -> str:
    person_dict = json.loads(person)
    match person_dict:
        case {"name": "Choo", "language": _}:
            return "Oh!, my load, you can do anything."
        case {"name": name, "language": "Korean"}:
            return f"{name}는 한국어를 사용합니다.."
        case {"name": name, "language": language}:
            return f"{name} can speaks {language}."
        case _:
            return "It's invalid data."

같은 기능을 패턴 매칭으로 구현해봤습니다. Dictionary 자료 구조의 값에 따라 어떻게 처리하는지 쉽게 알아볼 수 있겠네요. 중첩된 코드도 없어 코드를 따라 읽어가기에도 쉽습니다.

팩토리얼(factorial) 함수

그러면 다른 프로그래밍 언어는 패턴 매칭을 어떻게 지원하고 있을까요? 팩토리얼 함수를 파이썬, 하스켈, 러스트로 구현한 예를 살펴 보겠습니다. 프로그래밍 언어에 따라 패턴 매칭을 지원하는 여러 가지 방법이 있는데 구조가 비슷한 문법인 match, case 키워드를 사용한 예만 듭니다.

파이썬(Python)

if __name__ == '__main__':
    num: int = 7
    ret: int = factorial(num)
    print(f"The value of {num} factorial is {ret}")
    # The value of 7 factorial is 5040

def factorial(x: int) -> int:
    match x:
        case 0 | 1:
            return 1
        case _:
            return x * factorial(x - 1)

하스켈(Haskell)

main :: IO ()
main = do
  let num = 7
  let ret =  factorial(num)
  putStrLn $ "The value of " ++ show num ++ " factorial is " ++ show ret
  -- The value of 7 factorial is 5040

factorial :: Int -> Int
factorial x = case x of
                0 -> 1
                _ -> x * factorial(x - 1)

러스트(Rust)

fn main() {
    let num: u32 = 7;
    let ret: u32 = factorial(num);
    println!("The value of {num} factorial is {ret}");
    // The value of 7 factorial is 5040
}

fn factorial(x: u32 ) -> u32 {
    match x {
        0 | 1 => 1,
        _ => x * factorial(x - 1),
    }
}

리스트에서 제일 마지막 값 리턴하는 함수 구현

또 다른 예를 들어보겠습니다. 범용 프로그래밍 언어는 목록을 처리하는 자료 구조나 키워드는 하나씩 갖고 있습니다. 파이썬도 list 라는 자료 구조를 갖고 있는데요, 목록에서 제일 마지막에 있는 값을 반환하는 함수를 만들어 보고자 합니다. 패턴 패칭을 이용해 재귀적으로 처리하도록 구현해 봤습니다.

참고) 본 예제는 하스켈의 last 함수 기능을 따라 구현한 사례입니다.

파이썬(Python)

if __name__ == "__main__":
    print(last([1,2,3,4,5]))    # 5

def last(l: list[int]) -> int:
    match l:
        case []:
            raise ValueError("It's empty")
        case [x]:
            return x
        case [x, *xs]:
            return last(xs)

하스켈(Haskell)

main :: IO ()
main = do
    print $ last' [1, 2, 3, 4, 5]   -- 5

last' :: [Int] -> [Int]
last' l = case l of
            []     -> error "It's empty"
            [x]    -> x
            (_:xs) -> last' xs

러스트(Rust)

fn main() {
    let v2 = vec![1, 2, 3, 4, 5];
    println!("{:?}", last(v2));     // 5 
}

fn last(v: Vec<i32>) -> i32 {
    match v.as_slice() {
        []           => panic!("It's empty"),
        [x]          => *x,
        [_, xs @ ..] => last(xs.to_vec()),
    }
}

마무리

Erlang, Elixir, LISP, F#, C#, Swift 등 많은 프로그래밍 언어도 패턴 매칭을 지원합니다. 패턴 매칭을 지원하지 않거나 패턴 매칭을 지원할 생각이 없는 언어도 많습니다.

만약 본인이 사용하는 프로그래밍 언어가 패턴 매칭을 지원하는지 확인해보세요. 패턴 매칭을 쓸 수 있다면 if-else 문 보다 패턴 매칭을 사용하길 권합니다. 안전한 코드를 만들게 되어 삶이 바뀌게 된 것을 느끼실 겁니다.