Programming/Python

[Python] 파이썬 3.10 기능 소개

Jonghyuk Baek 2021. 7. 4. 23:46

Introduction

최근에 파이썬 3.10 업데이트 관련 소식이 간간히 들려오는데, 문득 어떤 점이 달라진 건지 궁금해진 차에 한번 관련 내용들을 정리해 보기로 했다.

 

이번 글에서는 파이썬 3.10의 주요 새 기능들을 간략히 정리할 예정이다. 글의 내용은 Python 3.10 beta 3 (3.10.0b3, 2021년 6월 17일 release) 를 기준으로 한다.

 

그리고 대부분의 예제 코드는 공식 도큐멘테이션을 참고했다! 맨 아래에 직접 pre-release 를 다운로드 할 수 있는 링크도 남겨두었으니 한번씩 체험해 봐도 좋을 듯 하다.

 

(다음 글)

[Python] 파이썬 3.11 기능 소개


1. 타입 힌트 관련

파이썬의 타입 힌트 기능에 관련된 기초적인 내용은 여기를 참고하자. 파이썬 자체는 동적 언어이지만, 명시적인 변수 타입을 지정해 줄 수 있는 (기본적으로 타입을 강제해 주지는 않는다) 타입 힌트와 관련된 기능이 꾸준히 추가되고 있는 듯 하다. 본인은 타입 힌트와 연계된 strict한 타입 정적 분석 (static analysis) 까지는 사용하고 있지는 않지만, 독스트링 (참고) 에 구구절절이 타입 관련 내용을 적어넣지 않고도 깔끔하게 Input, Output 타입을 명시해 줄 수 있다는 점 때문에 애용하고 있는 기능이다. 한번 내용을 살펴보자.

 

1.1 타입 유니온 오퍼레이터

기존에는 여러 변수 타입을 동시에 지칭할 수 있도록 하기 위해 typing 기본 모듈에서 Union 을 임포트 해 사용했다. 아래의 예시는 int형과 float형 중 하나를 변수 타입으로 받을 수 있다는 것을 Union을 통해 표현하고 있다.

def square(number: Union[int, float]) -> Union[int, float]:
    return number ** 2

3.10 버전부터는 조금 더 직관적으로 이를 표현할 수 있게 되었다. Union을 임포트 해 사용할 필요 없이, 단순히 X | Y 식으로 작성해 넣으면 된다. 

def square(number: int | float) -> int | float:
    return number ** 2

사실 타입 힌트 기능에 대한 배경 지식이 없는 채로 이전의 코드를 봤다고 하면 이게 어떤 의미인지 바로 파악하기가 어려웠을 테지만 (본인도 처음에 이게 어떤 용도인지 몰라서 검색을 해 봤다), 이제는 훨씬 직관적으로 int or float이 들어가야 한다는 것을 파악할 수 있다.

 

여기에 추가로, isinstance 함수에도 같은 syntax를 적용할 수 있다.

>>> isinstance([1, 2, 3], list | dict)
True

 

1.2 명시적 TypeAlias

User-defined 타입 클래스를 타입 힌트에 사용할 수 있었는데, 이전 버전에서는 만들어 놓은 변수가 타입 힌트 용의 클래스인지 (type alias), 아니면 단순 대입 변수인지를 (ordinary assignment) 파악하기가 어려웠다. 혼동을 줄이고자 이번 버전에서는 TypeAlias 타입을 추가해, 현재 변수가 type alias인지 표현할 수 있는 타입 지정자를 추가했다.

 

아래의 예시를 참고하자.

# 기존
RealNum = Union[int, float]

# TypeAlias 적용
RealNum = TypeAlias : Union[int, float]

def square(number: RealNum) -> RealNum:
    return number ** 2

타입 지정자임을 표현하기 위한 타입 지정자...... 되게 이상한 상황이지만 꼭 필요한 기능이라는 데에는 동감이 간다.

 

1.3 User-defined Type Guards

파이썬 코드에서, 타입 힌트의 도움을 받는 정적 분석기를 적용할 때, 종종 단순한 if, while 문을 통해 하위 코드 블록에서 변수의 타입을 한정짓는 type narrowing 테크닉을 이용한다. 그리고 이를 위한 조건문 부분을 보통 type guard라 지칭한다. 

def func(val: Optional[str]):
    # "is None" type guard
    if val is not None:
        # Type of val is narrowed to str
        ...
    else:
        # Type of val is narrowed to None
        ...

하지만 언어의 동적인 특성으로 인해, 이런 식의 type narrowing 테크닉을 정적 분석기에서 제대로 인식하지 못 하는 경우가 있다. 

def is_str_list(val: List[object]) -> bool:
    """Determines whether all objects in the list are strings"""
    return all(isinstance(x, str) for x in val)

def func1(val: List[object]):
    if is_str_list(val):
        print(" ".join(val)) # Error: invalid type

위의 예시에서는 func1 초반의 is_str_list 함수를 통해 입력 argument가 str로 이루어진 list인지를 확인하고 나서 다음 라인을 호출하고 있지만 (런타임에서는 아무런 문제가 없다), 정적 분석기에서는 타입 가드인 is_str_list 함수가 어떤 일을 하고 있는 지 파악할 길이 없으므로 인자가 str의 list 라는 것을 모른 채로 다음 라인의 타입 체크를 진행하고, 문제가 있다고 보고하게 된다.

 

이를 위해 typing 모듈에 TypeGuard 라는 타입을 추가해, 특정한 함수가 type narrowing을 진행하고 있다는 것을 정적 분석기의 타입 체커 (혹은 코드를 읽는 사람) 에게 알려줄 수 있도록 만들었다.

from typing import TypeGuard

def is_str_list(val: List[object]) -> TypeGuard[List[str]]:
    """Determines whether all objects in the list are strings"""
    return all(isinstance(x, str) for x in val)

위의 TypeGuard를 이용한 함수 output 타입 힌트는 output이 bool 타입이라는 것을 알려줌과 동시에, 인자의 타입 정보가 List[object] 로부터 List[str] 로 narrow 되었다는 것을 알려주고, 정적 분석기도 이 사실을 전달받을 수 있다.

 

2. 괄호로 묶인 Context Manager

3.10부터는 여러 개의 context manager를 괄호로 묶어서 사용할 수 있게 되었다. 동시에 여러 개의 context를 유지해야 할 때 훨씬 깔끔하게 코드를 작성할 수 있다. 아래의 예시처럼 다양한 형식으로 이용할 수 있다.

with (CtxManager() as example):
    ...

with (
    CtxManager1(),
    CtxManager2()
):
    ...

with (CtxManager1() as example,
      CtxManager2()):
    ...

with (CtxManager1(),
      CtxManager2() as example):
    ...

with (
    CtxManager1() as example1,
    CtxManager2() as example2
):
    ...

with (
    CtxManager1() as example1,
    CtxManager2() as example2,
    CtxManager3() as example3,
):

 

3. 에러 메시지 개선

에러 메시지가 에러 발생 원인이 되는 코드 영역을 좀 더 잘 짚어주게 되었으며, 디버그에 소소한 도움이 되는 suggestion을 주는 등 자잘하게 개선되었다.

 

3.1 SyntaxErrors

File "example.py", line 1
    expected = {9: 1, 18: 2, 19: 2, 27: 3, 28: 3, 29: 3, 36: 4, 37: 4,
               ^
SyntaxError: '{' was never closed

기존에 괄호나 스트링 리터럴을 제대로 닫지 않았을 경우 해당 부분과 상관 없는 엄한 곳을 가르키며 SyntaxError: invalid syntax 만을 출력해 원인을 찾는 데 애를 먹였다면, 이제는 문제가 되는 부분을 정확히 짚으며 괄호 혹은 스트링 리터럴이 제대로 닫히지 않았다고 출력해 준다.

 

>>> foo(x, z for z in range(10), t, w)
  File "<stdin>", line 1
    foo(x, z for z in range(10), t, w)
           ^^^^^^^^^^^^^^^^^^^^
SyntaxError: Generator expression must be parenthesized

또한 SyntaxError 공통적으로, 이제는 에러가 감지된 지점을 하이라이트 해 주는 것이 아니라 에러의 원인이 된 코드 영역 부분을 직접적으로 하이라이트 해 준다.

 

마지막으로 특정한 SyntaxError들에 대해 고유의 에러 메시지가 추가되면서 에러 상황을 더 잘 짚어주게 되었다. 아래에 몇 가지 예시를 들어 놓았다.

 

1. "=="가 들어가야 할 자리에 "="를 적음

>>> if rocket.position = event_horizon:
  File "<stdin>", line 1
    if rocket.position = event_horizon:
                       ^
SyntaxError: cannot assign to attribute here. Maybe you meant '==' instead of '='?

 

2. try 이후 except 혹은 finally가 오지 않음

>>> try:
...     x = 2
... something = 3
  File "<stdin>", line 3
    something  = 3
    ^^^^^^^^^
SyntaxError: expected 'except' or 'finally' block

 

3. 여러 exception 타입을 괄호로 묶지 않고 사용함

>>> try:
...     build_dyson_sphere()
... except NotEnoughScienceError, NotEnoughResourcesError:
  File "<stdin>", line 3
    except NotEnoughScienceError, NotEnoughResourcesError:
           ^
SyntaxError: multiple exception types must be parenthesized

 

3.2 IndentationErrors

이제는 정확히 어떤 라인에서 indentation이 누락되었는 지를 설명해 준다.

>>> def foo():
...    if lel:
...    x = 2
  File "<stdin>", line 3
    x = 2
    ^
IndentationError: expected an indented block after 'if' statement in line 2

 

3.3 AttributeErrors & NameErrors

이제는 프로그램 내에서 사용되고 있는 비슷한 이름의 변수 / 오브젝트 이름을 추천해 준다.

>>> collections.namedtoplo
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'collections' has no attribute 'namedtoplo'. Did you mean: namedtuple?


>>> schwarzschild_black_hole = None
>>> schwarschild_black_hole
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'schwarschild_black_hole' is not defined. Did you mean: schwarzschild_black_hole?

 

4. Structural Pattern Matching

마지막으로 짚을 변경 사항은, C나 자바에서 switch 문의 형태로 흔하게 볼 수 있던 패턴 매칭 기능이다.

 

파이썬에서의 패턴 매칭은 match statementcase statement로 이루어져 있으며, 기본 데이터형 뿐만 아니라 시퀀스나 클래스 인스턴스를 패턴으로써 사용할 수 있다.

match subject:
    case <pattern_1>:
        <action_1>
    case <pattern_2>:
        <action_2>
    case <pattern_3>:
        <action_3>
    case _:
        <action_wildcard>

기본적으로는, 위와 같은 형태로 패턴 매칭 구문을 작성하게 된다. 코드를 실행하게 되면 데이터 (subject) 가 주어졌을 때, 각 케이스를 순차적으로 방문하면서 데이터가 각 케이스에 배정된 패턴과 일지하는 지를 확인하고, 매칭이 된다면 해당 case 문에 적혀있는 action 코드 블록을 실행시킨다. 만약 해당되는 패턴이 없었다면, 마지막의 wildcard 패턴 ( _ )이 매칭되어 해당 action을 수행하거나, wildcard 패턴을 작성해 놓지 않았으면 아무런 작업도 수행하지 않고 지나간다. (loop에서의 pass와 같다)

 

한번 간단한 예시들을 통해 패턴 매칭에 친숙해져 보자. 

 

4-1: Basic example, wildcard

def barking_dog_dialogue(name_human: str):
    match name_human:
        case "Tom" | "Henry" | "Paul" | "Helen":
            print(f"Hi {name_human}.")
        case _:
            print("Woof.")

barking_dog_dialogue("Tom")
>>
Hi Tom.

barking_dog_dialogue("Jenny")
>>
Woof.

위 코드에서는 name_human 스트링을 인자로 받아, 특정 값에 해당되는 지를 확인해 그에 맞는 행동을 취해 주고 있다. 위의 예시처럼 여러 패턴을 | 오퍼레이터를 통해 함께 사용하는 것도 가능하다.

 

4-2: Unpacking

def barking_dog_dialogue(name_human: str):
    match name_human:
        case "Tom" | "Henry" | "Paul" | "Helen":
            print(f"Hi {name_human}.")
        case (human_A, human_B):
            print(f"Hi {human_A} and {human_B}.")
        case _:
            print("Woof.")

barking_dog_dialogue(("Tom", "Jenny"))
>>
Hi Tom and Jenny.

패턴을 언패킹 대입 구문처럼 (참고) 사용할 수도 있는데, 위의 예시에서는 튜플 형태의 데이터가 입력되면, 이를 두 개의 변수로 언패킹 대입을 시켜 이후에 뒤따라오는 액션에서 사용하고 있다. 

 

4-3: Starred unpacking

def barking_dog_dialogue(name_human: str):
    match name_human:
        case "Tom" | "Henry" | "Paul" | "Helen":
            print(f"Hi {name_human}.")
        case (human_A, *other_humans):
            print(f"Hi {human_A} and others.")
        case _:
            print("Woof.")

barking_dog_dialogue(("Tom", "Jenny", "Mario"))
>>
Hi Tom and others.

언패킹 단독 사용시와 동일하게, 위와 같이 별표 오퍼레이션을 통해 나머지 인자를 리스트로 묶어 받는 것도 가능하다. 

 

4-4: Partial assignment

def barking_dog_dialogue(name_human: str):
    match name_human:
        case "Tom" | "Henry" | "Paul" | "Helen":
            print(f"Hi {name_human}.")
        case ("Sophia", *others):
            print(f"Hi Sophia and her friends.")
        case _:
            print("Woof.")

barking_dog_dialogue(("Sophia", "Jenny", "Mario"))
>>
Hi Sophia and her friends.

그리고 위와 같이 일부 인자들에 대해서는 값을 비교하고, 일부 인자들은 변수에 대입시키는 식으로 응용할 수도 있다. 

 

4-5: Class & Class attribute matching

class HumanWithSnack:
    name: str
    snack_name: str

def barking_dog_dialogue(unknown_human: HumanWithSnack):
    match unknown_human:
        case HumanWithSnack(name=name, snack_name=snack_name):
            print(f"Welcome {snack_name} with {name}.")

human_with_snack = HumanWithSnack()
human_with_snack.name = "Tom"
human_with_snack.snack_name = "Jerky"

barking_dog_dialogue(human_with_snack)
>>
Welcome Jerky with Tom.

그리고 맨 처음에 언급했듯이, 클래스에 대해서도 패턴 매칭이 가능하다. 다만 복잡한 수준의 매칭까지는 아니고 클래스의 종류와 클래스 어트리뷰트가 일치하는 지 정도를 이용할 수 있는 것으로 보인다. 위 예시에서는 인자가 HumanWithSnack 클래스라면, 내부의 name과 snack_name 인자를 받아와 특정 액션을 취하고 있다. 

 

case 쪽의 구문은 언뜻 클래스 생성자처럼 보이지만 실제로는 데이터가 클래스에 해당되는지를 확인한 후, 각 클래스 어트리뷰트들에 대해 앞선 튜플 예시와 같은 패턴 매칭을 수행하고 있다는 것을 염두에 두어야 한다. 

 

어트리뷰트 이름을 같이 제공하지 않으면, 클래스 내부의 어트리뷰트 순서에 따라서 패턴 매칭을 수행하며 이름을 같이 제공할 시 내부 순서에 맞추지 않아도 된다.

 

만약 클래스 이름 이후의 괄호 안에 아무런 내용을 적지 않으면, 데이터가 클래스에 해당하는 지 만을 체크한다.

 

4-6: Complex pattern with wildcard

class HumanWithSnack:
    name: str
    snack_name: str

def barking_dog_dialogue(unknown_human: HumanWithSnack):
    match unknown_human:
        case HumanWithSnack(name=_, snack_name="Bone stick"):
            print(f"Dang.")

human_with_snack = HumanWithSnack()
human_with_snack.name = "Jason"
human_with_snack.snack_name = "Bone stick"

barking_dog_dialogue(human_with_snack)
>>
Dang.

그리고 와일드카드는 단독 case문으로 사용하는 것도 가능하지만, 위와 같이 패턴의 일부로써 와일드카드를 사용할 수도 있다. 위 예시에서는 name 어트리뷰트는 신경쓰지 않고 snack_name 어트리뷰트만을 확인하고 있다. 

 

4-7: Guard

class HumanWithSnack:
    name: str
    snack_name: str

def barking_dog_dialogue(unknown_human: HumanWithSnack, is_sleep: bool):
    match unknown_human:
        case HumanWithSnack(name=_, snack_name="Bone stick") if not is_sleep:
            print(f"Dang.")
        case _:
            print("...")

human_with_snack = HumanWithSnack()
human_with_snack.name = "Jason"
human_with_snack.snack_name = "Bone stick"

barking_dog_dialogue(human_with_snack, is_sleep=True)
>>
...

마지막으로, case 구문 뒷 단에 if문을 함께 사용해 뒤쪽 구문이 True로 판정되어야 패턴 매칭을 수행하도록 만들 수 있다. 이렇게 사용하는 if문을 Guard라고 지칭하는 듯 하다. 

 


이번에는 파이썬 3.10에서 새로 추가된 기능들을 간략하게 짚어보았다. 내용이 꽤 길었는데, 대부분 소소하게 도움이 되거나, 미리 알아두면 이후에 쓸모가 있을 법한 기능이었던 것 같다. 또한 이번에 추가되는 structural pattern matching 기능은 다양한 데이터 타입을 짧은 구문을 통해 효과적으로 처리할 수 있어, 사용법을 숙지해 놓는다면 더욱 좋은 코드를 작성하는 데에 도움이 될 것 같다. 구현된 문법이 언패킹 대입 쪽과 밀접한 관련을 가지고 있기 때문에 이쪽도 같이 참고해 보는 것도 좋을 것이다.

 

그리고 항상 하는 말이지만, 글에서 충분히 다루거나 전달하지 못 한 부분이 있기 때문에 꼭 한번쯤은 공식 도큐멘테이션도 함께 훑어보는 것을 권한다. 특히 PEP (Python Enhancement Proposals) 도큐멘테이션에는 파이썬 기능 개선을 제안하게 된 배경과 세부 사항들을 상세하게 설명해 주고 있기 때문에, 잘 이해가 되지 않는 개선 사항이 있다면 PEP 도큐멘테이션을 같이 보는 것도 좋다. 아래에 관련 링크들을 걸어 두었다.

 

Python 3.10 Installation (Windows)

혹시라도 미리 써 보고 싶은 분들을 위해 Windows 용 installer를 받을 수 있는 링크를 남겨둔다.

https://www.python.org/downloads/windows/

 

위 링크에서 본인의 환경에 맞는 인스톨러 버전을 다운로드해 설치하면 된다.

 

아래는 참고할 만한 사이트이다.

References

What's New In Python 3.10

https://docs.python.org/ko/3.10/whatsnew/3.10.html

PEP 613 -- Explicit Type Aliases

https://www.python.org/dev/peps/pep-0613/

PEP 647 -- User-Defined Type Guards

https://www.python.org/dev/peps/pep-0647/

[Python] 언패킹 (Unpacking)

https://jh-bk.tistory.com/7

Python Enhancement Proposals (PEPs)

https://www.python.org/dev/peps/

반응형