728x90

원본 글 : Python 3.12: Cool New Features for You to Try – Real Python

Python 3.12는 2023년 10월 2일에 릴리즈되었습니다. 언제나 그렇듯이, 전 세계 자원 봉사자들의 많은 노력 끝에, 새 버전이 10월에 출시됩니다.

새 버전에는 이 튜토리얼에서 살펴볼 몇 가지 새로운 기능과 개선 사항이 포함되어 있습니다. 또한 문서를 자세히 살펴보고 모든 변경 사항의 전체 목록을 확인할 수도 있습니다.

이 튜토리얼에서는 다음과 같은 새로운 기능과 개선 사항에 대해 알아봅니다:

  • 유용한 제안 및 안내가 포함된 개선된 오류 메시지
  • Python의 PEG 구문 분석기로 지원되는 더욱 표현력이 풍부한 f-스트링
  • 인라인 컴프리헨션(Inline Comprehension)을 포함한 최적화로 더욱 빨라진 Python
  • 제네릭에 주석을 다는 데 사용하는 타입 변수(type variables)에 대한 새로운 구문
  • Linux에서 강력한 perf 프로파일러 지원

이 튜토리얼의 예제 중 하나를 시도하려면 Python 3.12를 사용해야 합니다. 튜토리얼 Python 3 설치 및 설정 가이드사전 릴리스 버전의 Python을 어떻게 설치할 수 있나요? 에서는 시스템에 새 버전의 Python을 추가하기 위한 몇 가지 옵션을 안내합니다.

파이썬에 추가되는 새로운 기능에 대해 자세히 알아볼 수 있을 뿐만 아니라 새 버전으로 업그레이드하기 전에 고려해야 할 사항에 대한 몇 가지 조언도 얻을 수 있습니다. 아래 링크를 클릭하면 Python 3.12의 새로운 기능을 보여주는 코드 예제를 다운로드할 수 있습니다:

https://realpython.com/bonus/python-312-code/

개선된 오류 메시지

파이썬은 일반적으로 좋은 초급 언어로 인정받고 있으며, 가독성이 뛰어난 구문으로 찬사를 받고 있습니다. 최근 더욱 사용자 친화적으로 개선된 영역 중 하나는 오류 메시지입니다.

Python 3.10에서는 많은 오류 메시지, 특히 구문 오류에 대한 메시지가 더 많은 정보를 제공하고 정확해졌습니다. 마찬가지로 Python 3.11에서는 트레이스백에 더 많은 정보가 추가되어 문제가 되는 코드를 더 편리하게 찾아낼 수 있습니다.

최신 버전의 Python은 더 나은 오류 메시지를 제공하여 개발자 경험을 개선하는 작업을 계속하고 있습니다. 특히 몇 가지 일반적인 오류 메시지에는 이제 유용한 제안이 함께 제공됩니다. 이 섹션의 나머지 부분에서는 새로 추가되고 개선된 메시지를 살펴봅니다.

개선 사항 중 몇 가지는 모듈 가져오기(import)와 관련이 있습니다. 다음 세 가지 예에서는 math에서 pi를 가져와서 π로 작업하려고 합니다. 각 예제에서 파이썬 3.12의 새로운 기능 중 하나를 볼 수 있습니다. 다음은 첫 번째 예제입니다:

>>> math.pi
Traceback (most recent call last):
  ...
NameError: name 'math' is not defined. Did you forget to import 'math'?

math을 먼저 가져오지 않고 사용하면 기존의 NameError가 발생합니다. 또한 구문 분석기는 math에 액세스하기 전에 math을 임포트해야 함을 알려줍니다.

모듈 임포트를 기억해야 한다는 알림은 표준 라이브러리 모듈에 대해서만 트리거됩니다. 이러한 오류 메시지의 경우 Python 3.12는 사용자가 설치한 타사(third-party) 패키지를 추적하지 않습니다.

from-import 문을 사용하여 모듈에서 특정 이름을 임포트할 수 있습니다. 키워드의 순서를 바꾸면 이제 올바른 구문에 대한 친절한 안내가 표시됩니다:

>>> import pi from math
  ...
    import pi from math
    ^^^^^^^^^^^^^^^^^^^
SyntaxError: Did you mean to use 'from ... import ...' instead?

여기서는 math에서 pi를 가져오려고 했지만 파이썬에서는 명령문의 순서를 바꾸고 from 이후 import 순으로 입력해야 합니다.

세 번째 새로운 오류 메시지를 보려면 math에서 pi가 아닌 py를 가져오면 어떻게 되는지 확인해 보세요:

>>> from math import py
Traceback (most recent call last):
  ...
ImportError: cannot import name 'py' from 'math'. Did you mean: 'pi'?

math에 py라는 이름이 없으므로 ImportError가 발생합니다. 구문 분석기는 py 대신 pi를 의미한다고 제안합니다. 파이썬 3.10에는 파이썬이 비슷한 이름을 찾아주는 유사한 제안 기능이 도입되었습니다. 파이썬 3.12의 새로운 기능은 import하는 동안 이 작업을 수행할 수 있다는 것입니다.

import와 관련된 이 세 가지 개선 사항 외에도 클래스 내부에 정의된 메서드와 관련된 마지막 개선 사항이 있습니다. 다음 Circle 클래스의 구현을 살펴보세요:

# shapes.py

from math import pi

class Circle:
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return pi * radius**2

.area() 내에서 self.radius 대신 radius를 잘못 참조하고 있다는 점에 유의하세요. 이렇게 하면 메서드를 호출할 때 오류가 발생합니다:

>>> from shapes import Circle
>>> Circle(5).area()
Traceback (most recent call last):
  ...
  File "/home/realpython/shapes.py", line 10, in area
    return pi * radius**2
                    ^^^^^^
NameError: name 'radius' is not defined. Did you mean: 'self.radius'?

파이썬은 일반적인 NameError를 발생시키는 대신 .radius가 self에서 사용할 수 있는 어트리뷰트라는 것을 인식합니다. 그런 다음 로컬 변수 radius 대신 인스턴스 어트리뷰트 self.radius를 사용하도록 제안합니다.

이 예제에서 본 기능은 모두 파이썬 3.12의 새로운 기능입니다. 이 기능들은 파이썬을 좀 더 사용자 친화적으로 만들어줍니다. 이러한 오류 메시지 개선 사항과 파이썬 3.12 프리뷰에서 구현된 방법에 대해 자세히 알아볼 수 있습니다: 더욱 개선된 에러 메시지.

더 강력한 f-스트링

형식이 지정된 문자열(formatted strings), 줄여서 f-스트링은 PEP 498과 Python 3.6에 도입되었습니다. f-스트링을 통해 파이썬은 언어에 문자열 보간 기능을 추가했습니다. 다음과 같은 예제에서 f를 선행해 f-스트링을 인식할 수 있습니다:

>>> from datetime import date
>>> major = 3
>>> minor = 11
>>> release = date(2023, 10, 2)
>>> f"Python {major}.{minor + 1} is released on {release:%B %-d}"
'Python 3.12 is released on October 2'

f-스트링에는 중괄호({}) 세 쌍이 포함되어 있습니다. 각 쌍에는 최종 문자열에 보간되는 표현식이 포함되어 있습니다. 첫 번째 표현식은 메이저만 직접 참조하는 반면, 두 번째 표현식은 마이너에 작은 연산을 적용합니다.

세 번째 표현식은 특정 형식 지정자를 추가하여 표현식의 보간을 제어할 수 있음을 보여줍니다. 이 경우 release는 날짜이므로 %B 및 %-d는 날짜 형식 지정자로 해석되어 날짜를 월 일로 서식을 지정합니다.

f-스트링은 원래 전용 파서를 사용하여 구현되었습니다. 즉, f-스트링 내부의 표현식이 일반 파이썬 표현식임에도 불구하고 일반 파이썬 구문 분석기로 구문 분석되지 않았습니다. 대신 개발자는 별도의 구문 분석기를 구현했으며, 이를 유지 관리해야 했습니다.

그 주된 이유는 파이썬의 기존 LL(1) 구문 분석기가 f-스트링을 지원하지 못했기 때문입니다. 파이썬 3.9에 PEG 구문 분석기가 도입된 후에는 더 이상 그렇지 않습니다. 현재 구문 분석기는 f-스트링 내의 표현식을 구문 분석할 수 있습니다.

PEP 701에 따라, f-스트링은 이제 파이썬 3.12에서 파이썬의 문법에 추가되는 것으로 공식화되었습니다. 실제로 이것은 PEG 구문 분석기가 일반 Python 코드와 마찬가지로 f-스트링을 구문 분석한다는 것을 의미합니다.

대부분의 경우 f-스트링에 대한 이러한 변경 사항을 눈치채지 못할 것입니다. 주로 파이썬의 소스 코드를 유지 관리하는 핵심 개발자에게 도움이 됩니다. 그러나 f-스트링을 사용하는 모든 사람이 볼 수 있는 몇 가지 변경 사항이 있습니다.

일반적으로 f-스트링의 새로운 구현은 원래 추가되었던 일부 제한을 해제합니다. 이러한 제한 사항의 대부분은 IDE 및 코드 하이라이터와 같은 외부 도구에서 f-스트링을 더 쉽게 처리할 수 있도록 하기 위해 적용되었습니다. 아래에서 이전에는 불가능했던 몇 가지 예를 살펴보겠습니다.

이제 f-스트링 내부에서 문자열 따옴표 문자를 재사용할 수 있습니다. 예를 들어, 큰따옴표(")로 f- 문자열을 구분한 경우에도 표현식 내부에 " 를 사용할 수 있습니다:

>>> version = {"major": 3, "minor": 12}
>>> f"Python {version["major"]}.{version["minor"]}"
'Python 3.12'

큰따옴표로 f-스트링을 구분하더라도 " 를 사용하여 f-스트링 내부의 키를 지정할 수 있습니다.

지금까지는 f-스트링 표현식 안에 백슬래시 문자(\)를 사용할 수 없었습니다. 앞으로는 다른 표현식과 마찬가지로 f-스트링 표현식에서도 백슬래시를 사용할 수 있습니다:

>>> names = ["Brett", "Emily", "Gregory", "Pablo", "Thomas"]
>>> print(f"Steering Council:\\n {"\\n ".join(names)}")
Steering Council:
 Brett
 Emily
 Gregory
 Pablo
 Thomas

여기서 f-스트링의 문자열 및 표현식 부분 모두에 개행 문자를 나타내는 \\n을 사용합니다. 이전에는 후자는 허용되지 않았습니다.

다른 유형의 중괄호 및 괄호와 마찬가지로 이제 f-스트링에서 표현식을 구분하는 중괄호 안에 줄 바꿈을 추가할 수 있습니다. 또한 표현식에 주석을 추가할 수도 있습니다. 주석은 줄 끝까지 확장되므로 다음 줄 이상에서 표현식을 닫아야 합니다.

이 기능을 확인하려면 위 예제를 이어 아래 예제를 진행하세요:

>>> f"Steering council: {
...   ", ".join(names)  # Steering council members
... }"
'Steering council: Brett, Emily, Gregory, Pablo, Thomas'

두 번째 줄(”,”.join…)은 f-스트링 표현식을 한 줄에 단독으로 표시합니다.

f-스트링에 대한 중요한 변경 사항은 대부분 내부에서 이루어졌지만, 몇 가지 문제가 있던 구석이 개선되고 f-스트링 표현식이 이제 나머지 파이썬과 더 일관성이 높아진 것을 보셨을 것입니다.

f-스트링의 변경 사항을 더 자세히 살펴보고 싶으시다면 Python 3.12 프리뷰를 살펴보세요: 더욱 직관적이고 일관된 f-스트링.

빨라진 파이썬: 더 특별해진 인라인 컴프리헨션

2022년에 파이썬 3.11이 출시되었을 때, 파이썬을 더 빠르게 만드는 인터프리터 최적화에 대해 많은 화제가 있었습니다. 이 작업은 faster-cpython이라는 이름의 지속적인 노력의 일환이었으며, Python 3.12에서도 계속되고 있습니다.

Python 스크립트 실행이 시작되기 전에 코드가 바이트코드로 변환됩니다. 바이트코드는 파이썬 인터프리터가 실행하는 코드입니다. Python 3.11은 자주 발생하는 연산을 최적화하기 위해 실행 중에 바이트코드를 변경하고 조정할 수 있는 특수 적응형 인터프리터를 사용합니다. 이는 두 단계에 따라 달라집니다:

  • 단축(Quickening)은 특정 바이트코드가 여러 번 실행되어 특수화 후보가 되는 것을 알아차리는 과정입니다.
  • 특수화(Specialization)란 인터프리터가 일반 바이트코드를 전문화된 바이트코드로 대체하는 것을 의미합니다. 예를 들어, 두 개의 부동 소수점 숫자를 더하는 연산은 일반적인 덧셈 연산을 대체할 수 있습니다.

파이썬 3.12에서는 파이썬 3.11보다 빨라졌으며, 인터프리터는 이제 많은 새로운 바이트코드를 특수화할 수 있습니다. 단축 및 특수화가 실제로 작동하는 것을 보려면 다음 함수를 정의하십시오:

>>> def feet_to_meters(feet):
...     return 0.3048 * feet
...

feet_to_meters()를 사용하여 피트에서 미터로 변환할 수 있습니다. 인터프리터 내부를 들여다보려면 Python 코드를 분해하고 바이트코드를 볼 수 있는 dis를 사용합니다:

>>> import dis
>>> dis.dis(feet_to_meters, adaptive=True)
  1           0 RESUME                   0

  2           2 LOAD_CONST__LOAD_FAST    1 (0.3048)
              4 LOAD_FAST                0 (feet)
              6 BINARY_OP                5 (*)
             10 RETURN_VALUE

각 줄에는 하나의 바이트코드 명령어에 대한 정보가 표시됩니다. 다섯 개의 열은 줄 번호, 바이트 주소, 작업 코드 이름, 작업 매개변수, 괄호 안의 매개변수에 대한 해석입니다.

이 바이트코드 목록의 세부 사항을 이해할 필요는 없습니다. 하지만 한 줄의 Python 코드가 여러 바이트코드 명령어로 컴파일되는 경우가 많다는 점에 유의하세요. 이 예제에서 return 0.3048 * feet는 4개의 바이트코드 명령어로 변환됩니다.

사실, 더 이상 별도의 단축 단계는 없습니다. 원칙적으로 모든 바이트코드 명령어는 즉시 특수화할 수 있습니다. 파이썬 3.11에서는 바이트코드가 동일한 유형으로 8번 실행된 후에 특수화가 시작되었습니다. 이제는 이미 두 번의 호출 후에 발생합니다:

>>> feet_to_meters(1.1)
0.33528
>>> feet_to_meters(2.2)
0.67056

부동 소수점 인수를 사용하여 feet_to_meters()를 두 번 호출합니다. 그러면 인터프리터는 특수화하여 곱셈이 계속 부동 소수점 숫자끼리 된다고 가정합니다:

>>> dis.dis(feet_to_meters, adaptive=True)
  1           0 RESUME                    0

  2           2 LOAD_CONST__LOAD_FAST     1 (0.3048)
              4 LOAD_FAST                 0 (feet)
              6 BINARY_OP_MULTIPLY_FLOAT  5 (*)
             10 RETURN_VALUE

인터프리터는 원래의 BINARY_OP 명령을 수정하여 두 피연산자가 모두 float일 때 더 빠른 BINARY_OP_MULTIPLY_FLOAT로 대체했습니다.(역주: 런타임 상에서)

인터프리터가 특정 바이트코드를 적용하더라도 파이썬의 동적 특성에 해를 끼치지는 않습니다. 여전히 사용자는 정수 인자와 함께 feet_to_meters()를 사용할 수 있습니다.(역주: 정수 인자를 넣은 경우 BINARY_OP_MULTIPLY 로 명령이 수정돼 실행될 것임) 동일한 데이터 유형을 사용하면 프로그램이 더 빠르게 실행될 수 있다는 장점이 있습니다.(역주: feet_to_meters() 가 항상 부동소수점 곱셈으로 일관되게 소스코드를 작성했다면, 파이썬 인터프리터가 알아서 빠른 BINARY_OP_MULTIPLY_FLOAT으로 최적화해서 계속 실행할 것이다라는 얘기)

자세한 내용은 핵심 개발자 Brandt Bucher의 PyCon 2023 프레젠테이션을 확인해 보세요: 파이썬의 새로운 전문화 적응형 인터프리터 내부를 살펴보세요.

PEP 709는 파이썬 3.12의 새로운 최적화 기능인 인라인 컴프리헨션을 설명합니다. 파이썬은 이터러블을 변환하는 데 사용하는 리스트 컴프리헨션, 딕셔너리 컴프리헨션, 집합 컴프리헨션을 지원합니다. 예를 들어

>>> names = ["Brett", "Emily", "Gregory", "Pablo", "Thomas"]
>>> [name[::-1].title() for name in names]
['Tterb', 'Ylime', 'Yrogerg', 'Olbap', 'Samoht']

여기서는 리스트 컴프리헨션을 사용하여 각 이름을 반전시킵니다. 이러한 컴프리헨션은 현재 중첩 함수(nested function)로 컴파일됩니다. 이를 살펴보기 위해 먼저 컴프리헨션을 함수로 래핑합니다:

>>> def reverse_names(names):
...     return [name[::-1].title() for name in names]
...

위의 예와 마찬가지로 각 이름을 뒤집고 대문자로 시작하게 만듭니다. 이제 dis를 사용해 파이썬 3.11에서 함수를 분해합니다:

>>> import dis
>>> dis.dis(reverse_names)
  1       0 RESUME              0

  2       2 LOAD_CONST          1 (<code object <listcomp> at 0x7f2e61f42d30)
          4 MAKE_FUNCTION       0
          6 LOAD_FAST           0 (names)
          8 GET_ITER
         10 PRECALL             0
         14 CALL                0
         24 RETURN_VALUE

Disassembly of <code object <listcomp> at 0x7f2e61f42d30>:
  2       0 RESUME              0
          2 BUILD_LIST          0
          4 LOAD_FAST           0 (.0)
    >>    6 FOR_ITER           31 (to 70)
          8 STORE_FAST          1 (name)
         10 LOAD_FAST           1 (name)
         12 LOAD_CONST          0 (None)
         14 LOAD_CONST          0 (None)
         16 LOAD_CONST          1 (-1)
         18 BUILD_SLICE         3
         20 BINARY_SUBSCR
         30 LOAD_METHOD         0 (title)
         52 PRECALL             0
         56 CALL                0
         66 LIST_APPEND         2
         68 JUMP_BACKWARD      32 (to 6)
    >>   70 RETURN_VALUE

무시할 수 있는 세부 사항이 많이 있습니다. 주목해야 할 중요한 점은 새로운 listcomp 코드 객체가 생성되었다는 것입니다. 바이트코드 목록의 맨 위 부분에서 이 내부 함수를 로드하고 호출하는 걸 알 수 있습니다.

이렇게 중첩된 함수로 컴파일하면 함수 호출이 변수를 유출하지 않도록 컴프리헨션을 격리하므로 편리합니다. 그러나 이것이 반드시 가장 효율적인 구현은 아닙니다. 특히 작은 이터러블에 대해 컴프리헨션이 실행되는 경우, 중첩 함수 호출의 오버헤드가 눈에 띄게 증가합니다.

파이썬 3.12에서는 컴프리헨션이 바이트코드에 인라인 처리됩니다. 새 버전에서 reverse_names()의 분해 과정을 살펴보세요:

>>> dis.dis(reverse_names)
  1           0 RESUME                   0

  2           2 LOAD_FAST                0 (names)
              4 GET_ITER
              6 LOAD_FAST_AND_CLEAR      1 (name)
              8 SWAP                     2
             10 BUILD_LIST               0
             12 SWAP                     2
        >>   14 FOR_ITER                24 (to 66)
             18 STORE_FAST               1 (name)
             20 LOAD_FAST                1 (name)
             22 LOAD_CONST               0 (None)
             24 LOAD_CONST               0 (None)
             26 LOAD_CONST               1 (-1)
             28 BUILD_SLICE              3
             30 BINARY_SUBSCR
             34 LOAD_ATTR                1 (NULL|self + title)
             54 CALL                     0
             62 LIST_APPEND              2
             64 JUMP_BACKWARD           26 (to 14)
        >>   66 END_FOR
             68 SWAP                     2
             70 STORE_FAST               1 (name)
             72 RETURN_VALUE

다시 말하지만, 바이트코드의 세부 사항은 중요하지 않습니다. 대신 추가 코드 객체가 없고 추가 함수 호출이 발생하지 않는다는 점에 유의하세요.

일반적으로 인라인 컴프리헨션은 이전 구현(파이썬 3.11에서 보여준 윗 예제)보다 빠릅니다. 작은 이터러블에 대해 실행되는 컴프리헨스는 이전보다 최대 두 배까지 빠를 수 있습니다. 컴프리헨션이 수천 개 요소를 가진 더 큰 이터러블에 대해 실행되는 경우, 새 구현이 파이썬 3.11과 비슷하거나 약간 느리다는 것을 알 수 있습니다. 그러나 실제 벤치마크에 따르면 코드 속도가 더 빨라질 수 있습니다.

이 튜토리얼에 다운로드할 수 있는 첨부 자료에서 다양한 이해도를 벤치마킹하는 코드를 찾을 수 있습니다. 이를 사용하여 컴퓨터에서 이해의 성능을 확인하세요:

https://realpython.com/bonus/python-312-code/

파이썬을 더 빠르게 만들기 위한 노력은 계속되고 있으며, 파이썬 3.13에는 이미 많은 아이디어가 예정되어 있습니다.

전용 타입 변수 구문

파이썬은 버전 3.0에서 어노테이션에 대한 지원을 추가했습니다. 타입 힌트가 어노테이션의 동기 중 하나였지만, 정적 타입에 대한 파이썬의 지원은 몇 년 후인 파이썬 3.5에 이르러서야 도입되었습니다.

타입 변수는 파이썬의 타입 시스템에서 중요하고 강력한 부분을 구성합니다. 타입 변수는 정적 타입 검사(런타임 이전에 타입 체킹) 중에 구체적인 타입을 대신할 수 있습니다. 타입 변수를 사용하여 제네릭 클래스와 제네릭 함수를 매개변수화할 수 있습니다. 주어진 리스트에서 첫 번째 요소를 반환하는 다음 예제를 살펴봅시다:

def first(elements):
    return elements[0]

first()의 반환 값 유형은 전달한 목록의 종류에 따라 다릅니다. 예를 들어 elements가 정수 목록인 경우 first()는 int를 반환하고, 문자열 목록인 경우 반환 유형은 str입니다. 이 관계를 표현하기 위해 타입 변수를 사용합니다.

파이썬 3.12에는 타입 변수에 대한 새로운 구문이 도입되었습니다. 새 구문을 사용하면 다음과 같이 작성할 수 있습니다:

def first[T](elements: list[T]) -> T:
    return elements[0]

함수 정의에 대괄호 안에 T를 추가하면 first()가 타입 변수 T로 매개변수화된 일반 함수임을 선언할 수 있습니다.

참고: list 대신 Sequence를 사용하여 매개변수에 어노테이션을 다는 것이 더 합리적일 때가 많습니다. Sequence는 정수 인덱스로 엘리먼트 액세스를 지원하는 이터러블입니다. Sequence를 사용하려면 first()를 다시 작성하면 됩니다:

from collections.abc import Sequence

def first[T](elements: Sequence[T]) -> T:
	return elements[0]

리스트, 튜플, 문자열은 모두 파이썬에서 Sequence의 예시입니다.

first()를 일반 함수로 선언해도 런타임에는 아무런 영향이 없습니다. 대신 편집기나 정적 타입 검사기가 타입을 추적하는 데 도움이 됩니다. 다음 두 가지 예를 살펴보세요:

>>> def first[T](elements: list[T]) -> T:
...     return elements[0]
...

>>> first([2, 3, 1, 0])
2

>>> first(["Brett", "Emily", "Gregory", "Pablo", "Thomas"])
'Brett'

first()를 처음 호출할 때 정수 목록을 전달합니다. 이 경우 타입 검사기는 T가 int일 수 있음을 확인하고 first()가 int를 반환한다고 추론합니다. 두 번째 예제에서는 문자열 이름 목록을 전달합니다. 이 경우 elements는 list[str]이므로 T는 str으로 취급됩니다.

앞서 언급했듯이 타입 변수는 오랫동안 사용 가능했습니다. 파이썬 3.12에서 새롭게 추가된 것은 타입 변수를 사용하기 위한 새롭고 강력한 구문입니다. 이전에는 타이핑에서 TypeVar를 임포트하여 동일한 예제를 구현했습니다:

from typing import TypeVar

T = TypeVar("T")

def first(elements: list[T]) -> T:
    return elements[0]

새로운 구문에는 두 가지 주요 이점이 있습니다. 우선, 파이썬의 정규 문법에 포함되므로 TypeVar 클래스를 임포트할 필요가 없습니다. 또한 T는 함수 외부가 아닌 함수 정의에서 선언됩니다. 이렇게 하면 타입 변수의 역할이 더 명확해집니다.

지금까지 타입 변수를 가장 간단하게 사용하는 방법을 살펴보았습니다. 새로운 구문은 다른 용도로도 사용할 수 있습니다. 여기에는 일반 클래스 및 일반 유형 별칭 외에도 다중 유형 변수, 제약 유형 변수 및 바인딩된 유형 변수가 포함됩니다. 다음 예제에서 이러한 사용 사례를 살펴보겠습니다.

참고: TypeVar를 사용하는 경우 타입 변수가 공변량(covariant)인지, 역변량(contravariant)인지, 불변량(invariant)인지 지정해야 합니다. 이는 복합 타입 내에서 하위 타입이 상호 작용하는 방식과 관련이 있습니다. 예를 들어 bool은 int의 하위 타입입니다. list[int]와 비교했을 때 list[bool]은 무엇을 의미할까요?

이는 기술적인 질문입니다. 좋은 소식은 새로운 구문을 사용하면 분산(variance)에 대해 명시적으로 설명할 필요가 없다는 것입니다. 대신 타입 검사기가, 필요할 때마다 올바른 타입을 추론할 수 있습니다.

튜플을 사용하여 미리 정해진 수의 요소로 이질적인(heterogeneous) 시퀀스를 표현하는 경우가 많습니다. 기본적인 예로 이름과 나이 쌍으로 사람에 대한 정보를 나타내는 튜플을 들 수 있습니다. 타입 측면에서 이러한 튜플은 tuple[str, int]로 표현할 수 있습니다.

이제 이러한 튜플 쌍의 순서를 뒤집는 함수가 있다고 가정해 보겠습니다. 일반적으로 두 요소의 타입은 서로 다르므로 이를 표현하려면 두 개의 유형 변수가 필요합니다. 대괄호 안에 쉼표로 구분하여 두 개 이상의 타입 변수를 선언할 수 있습니다:

def flip[T0, T1](pair: tuple[T0, T1]) -> tuple[T1, T0]:
    first, second = pair
    return (second, first)

여기서 T0과 T1은 두 개의 독립적인 타입 변수입니다. 서로 다른 타입을 취할 수도 있지만 동일할 수도 있습니다. 예를 들어 한 쌍의 부울을 전달할 수 있습니다.

기본적으로 타입 변수는 모든 타입으로 구체화할 수 있습니다. 하지만 몇 가지 타입 중 하나로 제한되거나 특정 타입의 하위 타입으로 한정되는 유형 관계를 표현하고 싶을 때가 있습니다. 타입 변수 뒤에 콜론으로 구분된 조건을 추가하면 됩니다. 다음 구문을 사용합니다:

>>> def free[T](argument: T) -> T: ...

>>> def constrained[T: (int, float, complex)](argument: T) -> T: ...

>>> def bounded[T: str](argument: T) -> T: ...

지금까지 예제에서는 자유 구문(free syntax)을 사용했습니다. 이는 T가 어떤 타입이든 될 수 있음을 의미합니다. constrained()에서 T는 int, float 또는 complex만 가능한 타입 변수입니다. 리터럴 타입 튜플을 사용하여 이를 표현합니다. bounded()에서 T는 str 또는 str의 하위 클래스일 수 있습니다.

제네릭 클래스를 정의할 수도 있습니다. 제네릭 함수의 경우 대괄호 안에 모든 타입 변수를 선언합니다. 다음 예제는 목록에 기반한 간단한 스택을 구현합니다:

# generic_stack.py

class Stack[T]:
    def __init__(self) -> None:
        self.stack: list[T] = []

    def push(self, element: T) -> None:
        self.stack.append(element)

    def pop(self) -> T:
        return self.stack.pop()

여기서는 클래스 이름에 [T]를 추가했습니다. 그런 다음 클래스 내에서 메서드 매개변수 및 반환 타입에 주석을 달 때 타입 변수를 사용할 수 있습니다. 실제로는 특정 타입이 포함된 스택을 인스턴스화할 수 있습니다. 다음으로 정수 스택을 만들어 보겠습니다:

>>> from generic_stack import Stack
>>> numbers = Stack[int]()
>>> numbers.push(3)
>>> numbers.push(12)

>>> numbers.stack
[3, 12]

>>> numbers.pop()
12

스택을 인스턴스화할 때 [int]를 추가하면 타입 검사기에 데이터 구조가 정수로 구성될 것임을 알릴 수 있습니다. 그러면 다른 타입이 스택 내부에 포함될 수 있는지 경고할 수 있습니다.

스택에서 숫자를 꺼낼 때 마지막으로 밀어 넣은 숫자가 나오는지 관찰하세요. 이를 흔히 선입선출(LIFO)이라고 합니다. 이 아이디어는 부엌 찬장에 있는 접시 더미를 연상시킵니다. 스택은 다양한 컴퓨터 알고리즘에서도 유용하게 사용됩니다.

타입 별칭(type alias)에 새로운 구문을 사용할 수도 있습니다. 타입 별칭은 중첩된 데이터 타입으로 작업을 단순화하거나 데이터 타입을 보다 명확하게 설명하기 위해 특정 타입에 부여하는 이름입니다.

이제 type을 사용하여 타입 별칭을 선언할 수 있습니다:

>>> type Person = tuple[str, int]
>>> type ListOrSet[T] = list[T] | set[T]

여기서 Person은 문자열과 정수로 구성된 튜플로 표현되는 타입입니다. ListOrSet은 목록 또는 집합이 나타내는 일반 타입 별칭입니다. 나중에 인수가 정수 목록 또는 정수 집합이어야 하는 ListOrSet[int]와 같은 식으로 인수를 주석 처리할 수 있습니다.

타 변수에 대한 새로운 구문에 대해 자세히 알아보고 더 많은 실용적인 예제를 보려면 Python 3.12 프리뷰를 참조하세요: 정적 타이핑 개선 사항 및 PEP 695.

리눅스에서 강력한 perf 프로파일러 지원

프로파일러(profiler)는 스크립트 및 프로그램의 성능을 모니터링하고 진단하기 위한 도구입니다. 코드를 프로파일링하면 구현을 조정하는 데 사용할 수 있는 정확한 측정값을 얻을 수 있습니다.

Python은 표준 라이브러리에서 timeit 및 cProfile과 같은 도구를 통해 오랫동안 프로파일링을 지원해 왔습니다. 또한 line-profiler, Pyinstrument, Fil과 같은 서드파티 도구도 다른 기능을 제공합니다.

perf 프로파일러는 Linux 커널에 내장된 프로파일러입니다. Linux에서만 사용할 수 있지만 하드웨어 이벤트 및 시스템 호출부터 실행 중인 라이브러리 코드에 이르기까지 모든 것에 대한 정보를 제공할 수 있는 인기 있고 강력한 도구입니다.

지금까지는 Python에서 perf 실행이 제대로 작동하지 않았습니다. CPython 인터프리터는 일반적으로 Python 코드를 실행하는 프로그램입니다. Python 코드는 _PyEval_EvalFrameDefault()라는 이름의 C 함수로 평가되며, Python 프로그램의 일반적인 프로필에는 _PyEval_EvalFrameDefault()에 대부분의 시간을 소비한 것으로만 표시될 것입니다.

파이썬 3.12는 perf에 대한 적절한 지원을 추가하고 트램폴린 계측(trampoline instrumentation)이라는 기술을 통해 파이썬 함수를 모니터링할 수 있는 기능을 제공합니다. 이를 통해 perf가 생성하는 프로파일링 보고서에 개별 Python 함수가 표시될 수 있습니다:

Samples: 10K of event 'cycles', Event count (approx.): 34217160648

-  100.00%     10432
   -   99.16%     /home/realpython/python-custom-build/bin/python3.12
        __libc_start_main
      - Py_BytesMain
         - 99.89% pymain_run_python.constprop.0
              _PyRun_AnyFileObject
              _PyRun_SimpleFileObject
              run_mod
              run_eval_code_obj
              PyEval_EvalCode
              py::<module>:/home/realpython/project/script.py
              _PyEval_EvalFrameDefault
              PyObject_Vectorcall
              py::main:/home/realpython/project/script.py
              _PyEval_EvalFrameDefault
            - PyObject_Vectorcall
               + 66.61% py::slow_function:/home/realpython/project/script.py
               + 33.39% py::fast_function:/home/realpython/project/script.py
   +    0.84%     /proc/kcore

Linux를 실행 중이고 코드 프로파일링에 관심이 있다면 perf를 사용해 보세요. 시스템에서 perf를 설정하고 코드를 프로파일링하는 방법을 비롯한 자세한 내용은 Python 3.12 프리뷰를 참조하세요: Linux perf 프로파일러 지원.

기타 괜찮은 기능들

지금까지 파이썬 3.12의 가장 큰 변화와 개선 사항을 살펴보았습니다. 하지만 살펴봐야 할 것이 훨씬 더 많습니다. 이 섹션에서는 헤드라인 아래에 숨어 있을 수 있는 몇 가지 새로운 기능을 살펴보겠습니다. 여기에는 인터프리터에 대한 내부 변경 사항, 새로운 타이핑 기능, 이터러블 그룹화 및 파일 나열을 위한 새로운 함수가 포함됩니다.

인터프리터별 GIL

파이썬에는 인터프리터의 많은 내부 코드를 간소화하는 전역 인터프리터 잠금(GIL)이 있습니다. 동시에 GIL은 동시 실행되는 Python 코드 실행에 몇 가지 제한을 부과합니다. 특히, 일반적으로 한 번에 하나의 스레드만 실행할 수 있으므로 병렬 처리가 번거로워집니다.

시간이 지나 GIL을 제거하려는 여러 시도가 있었습니다. 최근에는 PEP 703nogil 프로젝트가 많은 화제를 불러일으켰고, 파이썬에서 GIL을 제거하기 위한 로드맵이 있습니다.

관련 시도가 Python 3.12에서 빛을 보고 있습니다. PEP 684는 인터프리터별 GIL에 대해 설명합니다. 파이썬 인터프리터는 파이썬 스크립트 및 프로그램을 실행하는 프로그램입니다. 소위 서브 인터프리터라고 하는 새로운 인터프리터를 생성할 수 있지만, 이는 C API를 통한 확장 모듈에서만 가능합니다.

참고: 파이썬 코드에 서브인터프리터를 노출하는 새로운 표준 라이브러리 모듈인 interpreters에 대한 작업이 진행 중입니다. 이는 PEP 554와 실제 파이썬의 서브인터프리터 가이드에 설명되어 있습니다.

인터프리터별 GIL이 있다는 것은 각 서브인터프리터에 대해 별도의 인터프리터 잠금이 있다는 것을 의미합니다. 이는 최신 컴퓨터에서 볼 수 있는 다중 코어를 더 잘 활용하는 새롭고 효율적인 파이썬 병렬 처리 방법의 가능성을 열어줍니다. 이러한 흥미로운 모델 중 하나는 순차적 프로세스(CSP, Communicating Sequential Processes)를 통신하는 것으로, 이는 Erlang과 Go를 비롯한 여러 언어에서 동시성에 영감을 주었습니다.

인터프리터별 GIL을 달성하기 위해 핵심 개발자들은 CPython 내부의 여러 부분을 리팩터링했습니다. Python에는 글로벌 상태 저장소인터프리터별 저장소가 모두 있습니다. 이 시도에서는 이전에 전역 상태로 저장되었던 많은 부분이 이제 각 인터프리터에 대해 저장됩니다.

아마도 파이썬 3.12를 실행할 때 이 변경 사항을 눈치채지 못할 것입니다. 일반 파이썬 사용자에게는 변경 사항이 노출되지 않습니다. 대신 향후 병렬성을 구현하는 새로운 방법을 위한 토대를 마련하고 있습니다.

파이썬 3.13에서 서브인터프리터에 대한 접근성을 높이기 위한 계획을 포함하여 서브인터프리터에 대한 자세한 내용은 파이썬 3.12 프리뷰에서 확인할 수 있습니다: 서브인터프리터.

불멸 객체(Immortal Objects)

Python에 불멸 객체를 도입하는 것은 CPython 인터프리터를 개선하고 향후 새로운 개발을 위한 길을 준비하는 또 다른 내부 기능입니다.

효율성을 위해 파이썬의 여러 객체는 싱글톤입니다. 예를 들어, 프로그램 실행 중에는 코드에서 None을 참조하는 횟수와 관계없이 None 객체가 하나만 존재합니다. 이러한 최적화가 가능한 이유는 None이 불변이기 때문입니다. 그 값은 절대 변하지 않습니다.

파이썬의 관점에서 볼 때 None은 불변이지만 CPython 인터프리터가 처리하는 None 객체는 변경된다는 것이 밝혀졌습니다. 인터프리터는 객체의 데이터와 함께 객체의 참조 횟수를 포함하는 구조체의 모든 객체를 나타냅니다. 참조 횟수는 코드에서 객체를 참조할 때마다 변경됩니다.

객체의 참조 횟수는 직접 확인할 수 있습니다:

>>> import sys
>>> a = 3.12
>>> sys.getrefcount(a)
2

>>> b = a
>>> sys.getrefcount(a)
3

>>> del b
>>> sys.getrefcount(a)
2

sys.getrefcount()를 사용하여 객체의 참조 수를 검사합니다. 여기서 a는 실수 객체 3.12를 참조합니다. b가 동일한 객체를 참조할 때 참조 횟수가 증가하는 것을 볼 수 있습니다. 마찬가지로 이름 b가 삭제되면 참조 횟수가 감소합니다.

참고: sys.getrefcount()를 처음 호출하면 2가 반환되지만 a에 대한 참조는 하나만 생성됩니다. 그러나 getrefcount()에 인자로 a를 전달하면 두 번째 참조가 생성됩니다. 일반적으로 반환되는 개수는 예상보다 하나 더 많은 경우가 많습니다.

참조 횟수는 파이썬의 메모리 관리에 중요합니다. CPython은 참조 횟수가 0이 되면 메모리에서 객체를 제거하는 가비지 수집기를 사용합니다.

불멸 객체는 인터프리터 내부를 포함하여 실제로 변경되지 않는 객체입니다. 즉, 참조 횟수가 변경되지 않습니다. 불멸 객체는 참조 횟수를 특수 플래그로 설정하여 식별할 수 있습니다. 이는 C-API의 이전 버전과의 호환성을 유지하고 불멸 객체를 대부분 일반 객체와 동일하게 취급하기 위해 수행됩니다.

불멸 객체의 참조 횟수를 보면 이 메커니즘을 확인할 수 있습니다:

>>> import sys
>>> sys.getrefcount(None)
4294967295

>>> a = None
>>> sys.getrefcount(None)
4294967295

처음에는 None이 40억 번 이상 참조된 것 같은 인상을 받습니다. 하지만 4,294,967,295는 None이 불멸의 객체임을 나타내는 특수 값입니다. None에 대한 새 참조를 생성해도 이 숫자는 변경되지 않습니다.

이 특수 플래그는 무작위로 선택되지 않습니다. 32비트 시스템이 기본적으로 나타낼 수 있는 가장 큰 정수인 16진수 FFFFFFFF에 해당합니다:

>>> hex(sys.getrefcount(None))
'0xffffffff'

즉, 불멸 플래그 값은 인터프리터가 일반적인 사용에서 참조 카운트에 도달하지 않을 정도로 충분히 큽니다. 그리고 테스트하기에 효율적인 표현을 가지고 있습니다.

불멸 객체는 인터프리터의 구현 세부 사항이지만 몇 가지 장점이 있습니다:

  • 진정한 불변 객체는 캐시 동작이 더 우수합니다. 특정 코드에서는 None, True, False 등과 같은 싱글톤으로 작업하는 것이 더 효율적입니다.
  • 불멸 객체는 GIL의 보호가 필요하지 않습니다. 따라서 앞서 배운 인터프리터별 GIL과 함께 잘 작동합니다. 또한 파이썬에서 GIL을 제거하기 위한 일부 작업을 간소화합니다.

불멸 객체에 대한 자세한 내용은 PEP 683에서 확인할 수 있습니다. 또한 파이썬용 불멸 객체 소개에서는 구현에 대한 몇 가지 컨텍스트와 배경을 제공하며, 파이썬 3.12의 불멸 객체 이해에서는 구현 자체에 대해 설명합니다.

명시적 상속을 위한 데코레이터 오버라이딩

Python은 객체 지향 언어로 클래스 작업과 상속을 잘 지원합니다. 각 클래스는 타입 힌트에서 사용할 수 있는 타입도 정의하기 때문에 파이썬의 클래스와 정적 타입 시스템 사이에는 밀접한 관련이 있습니다.

파이썬 3.12의 새로운 타이핑 기능 중 하나는 @override입니다. 이 데코레이터를 사용하여 부모 클래스의 메서드를 재정의하는 서브클래스의 메서드를 표시할 수 있습니다. 이 기능은 부분적으로 Java 및 C++와 같은 다른 언어의 유사한 메커니즘에서 영감을 받았습니다.

@override를 사용하면 특히 코드를 리팩터링할 때 특정 종류의 버그를 방지하는 데 도움이 됩니다. 정적 타입 검사기는 다음과 같은 경우에 경고를 표시할 수 있습니다:

  • 메서드의 이름을 변경했지만 서브클래스에서 해당 메서드의 이름을 변경하는 것을 잊은 경우
  • 부모 메서드를 재정의해야 하는 하위 클래스 메서드의 이름을 잘못 입력한 경우
  • 클래스에 새 메서드를 추가할 때 실수로 하위 클래스의 기존 메서드를 재정의하는 경우

지금까지 타입 검사기는 메서드가 다른 메서드를 재정의하는지 여부를 알 수 있는 방법이 없었습니다. 파이썬 3.12에서는 @override가 타이핑에 추가되었습니다. 이전 버전의 Python에서는 서드파티 typing-extensions 라이브러리에서 @override를 가져올 수 있습니다.

새 데코레이터를 사용하는 방법의 예로 은행 계좌를 나타내는 두 개의 클래스를 구현해 보겠습니다. BankAccount는 기본 은행 계좌를 나타내고, SavingsAccount는 몇 가지 특수 기능을 갖춘 BankAccount의 서브클래스가 됩니다.

간단하게 하기 위해 데이터 클래스를 사용해 은행 계좌 클래스를 정의하겠습니다. 일반 은행 계좌 클래스부터 시작하겠습니다:

# accounts.py

import random
from dataclasses import dataclass
from typing import Self

def generate_account_number() -> str:
    """Generate a random eleven-digit account number"""
    account_number = str(random.randrange(10_000_000_000, 100_000_000_000))
    return f"{account_number[:4]}.{account_number[4:6]}.{account_number[6:]}"

@dataclass
class BankAccount:
    account_number: str
    balance: float

    @classmethod
    def from_balance(cls, balance: float) -> Self:
        return cls(generate_account_number(), balance)

    def deposit(self, amount: float) -> None:
        self.balance += amount

    def withdraw(self, amount: float) -> None:
        self.balance -= amount

은행 계좌에는 두 가지 일반 메서드인 .deposit() 및 .withdraw()가 있습니다. 또한 초기 잔액이 주어지면 새 은행 계좌를 생성할 수 있는 대체 생성자 .from_balance()를 추가할 수 있습니다. 이렇게 하면 generate_account_number() 유틸리티 함수가 임의의 계좌 번호를 생성합니다.

이 은행 계좌는 다음과 같이 사용할 수 있습니다:

>>> from accounts import BankAccount
>>> account = BankAccount.from_balance(1000)
>>> account.withdraw(123.45)
>>> account
BankAccount(account_number='2801.83.60525', balance=876.55)

여기서 초기 잔액이 $1000인 새 은행 계좌를 시작합니다. 123.45달러를 인출하고 나면 계좌에 $876.55달러가 남습니다.

다음으로 저축 계좌(saving account)를 추가합니다. 일반 은행 계좌와 비교하여 저축 계좌는 이자가 적립될 수 있습니다. 또한 은행은 $100보다 큰 인출 금액을 가장 가까운 달러 금액으로 반올림하여 계좌 소유자에게 작은 보너스를 제공합니다.

저축 계좌를 BankAccount의 서브 클래스로 구현하고 부모 메서드를 재정의하는 메서드를 표시합니다. from typing import overrice 를 추가해야 합니다:

# accounts.py

import random
from dataclasses import dataclass
from typing import Self, override

# ...

@dataclass
class SavingsAccount(BankAccount):
    interest: float

    def add_interest(self) -> None:
        self.balance *= (1 + self.interest / 100)

    @classmethod
    @override
    def from_balance(cls, balance: float, interest: float = 1.0) -> Self:
        return cls(generate_account_number(), balance, interest)

    @override
    def withdraw(self, amount: float) -> None:
        self.balance -= int(amount) if amount > 100 else amount

먼저 .interest를 새 속성으로 추가하고 .add_interest()를 저축 계좌에 이자를 추가하는 새 메서드로 추가합니다. 그런 다음 .from_balance()를 업데이트하여 이자 지정을 지원합니다. 이 생성자는 BankAccount의 해당 메서드를 재정의하므로 @override로 표시합니다.

참고: 오버라이드는 메서드에 .__**override__** 속성을 추가하는 것 외에는 런타임에 영향을 주지 않습니다. @override와 @classmethod를 결합할 때는 @override를 마지막에 지정해야 합니다.

이는 정적 타입 검사기에는 중요하지 않지만 .__override__라는 일관된 런타임 시맨틱을 설정할 수 있습니다.

또한 .withdraw()를 재정의하여 계정 소유자에게 소정의 보너스를 추가할 수도 있습니다. 고객이 $100 이상을 인출하는 경우 잔액에서 가장 가까운 달러로 반올림한 금액만 차감합니다:

>>> from accounts import SavingsAccount
>>> savings = SavingsAccount.from_balance(1000, interest=2)
>>> savings.withdraw(123.45)
>>> savings
SavingsAccount(account_number='5283.78.04835', balance=877, interest=2)

여기에서는 $123.45를 인출했지만 잔액에서 $123만 차감했습니다. 이것은 SavingsAccount가 재정의된 메서드를 사용한다는 것을 보여줍니다. 예제를 계속 진행하여 새 메서드를 테스트할 수 있습니다:

>>> savings.add_interest()
>>> savings
SavingsAccount(account_number='5283.78.04835', balance=894.54, interest=2)

>>> savings.withdraw.__override__
True

계좌에 이자를 추가한 후에는 .withdraw()를 재정의로 태그했는지 확인합니다. __.**override__** 속성을 추가했더라도 이 속성은 아무런 효과가 없습니다. 대신 정적 타입 검사기를 사용하여 오버라이드 메서드에서 발생하는 오류를 포착해야 합니다. 타입 검사기를 활성화하는 방법에 대한 자세한 내용은 파이썬 3.12: 정적 타이핑 개선 사항을 참조하세요.

런타임에 유사한 문제를 포착하는 데 관심이 있다면 서드파티 라이브러리 overrides 기능을 확인해 보세요.

Day, Month 관련 상수 다루

파이썬의 달력 모듈(calendar)은 오랫동안 포함됐습니다. 일반적으로 날짜 작업할 때, 날짜와 타임스탬프가 있는 날짜를 각각 나타내는 날짜(date) 및 날짜 시간(datetime) 클래스를 제공하는 datetime 모듈을 사용하게 됩니다.

캘린더를 사용하면 날짜/시간의 기술적 사용과 사용자 친화적인 달력 표현 사이의 간극을 메울 수 있습니다. 예를 들어 캘린더를 사용하여 터미널에 캘린더를 빠르게 표시할 수 있습니다:

$ python -m calendar 2023 10
    October 2023
Mo Tu We Th Fr Sa Su
                   1
 2  3  4  5  6  7  8
 9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31

모듈을 -m과 함께 실행하면 캘린더가 표시됩니다. 위의 예에서는 2023년 10월을 표시하고 10월 2일이 월요일인 것을 관찰합니다.

사용자 정의 캘린더를 작성해야 하는 경우 자체 코드에서 캘린더를 사용할 수도 있습니다. 새 Python 버전에서는 요일 및 월 작업에 대한 지원이 더욱 강화되었습니다. 먼저 요일 및 월 이름 목록과 같이 오랫동안 사용 가능했던 기능을 살펴보세요:

>>> import calendar

>>> ", ".join(calendar.month_abbr)
', Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec'

>>> calendar.weekday(2023, 10, 2)
0
>>> calendar.day_name[0]
'Monday'

먼저 모든 월을 축약하여 나열합니다. 월 목록에는 13개의 요소가 있으며 첫 번째 요소는 빈 문자열입니다. 이는 인덱스가 일반적인 월 번호와 일치하도록 하기 위한 것으로, 1월은 1에 해당합니다.

두 번째 예에서는 calendar.weekday()를 사용하여 2023년 10월 2일이 어느 요일에 해당하는지 계산합니다. 그런 다음 날짜 이름 목록에서 해당 답변(0)을 조회하여 10월 2일이 월요일임을 확인합니다.

지금까지 살펴본 모든 것은 이전 버전의 파이썬에서 가능합니다. Python 3.12에서는 열거형 Day와 Month라는 두 가지 사소한 기능이 추가되었습니다. 열거형은 공통 네임스페이스에 있는 상수 값의 모음입니다. 이를 통해 요일과 월을 더 편리하게 처리할 수 있습니다.

열거형 멤버는 상수입니다. 몇 가지 다른 방법으로 열거형 멤버에 액세스할 수 있습니다:

>>> calendar.Month(10)
calendar.OCTOBER

>>> calendar.Month.OCTOBER
calendar.OCTOBER

>>> calendar.Month["OCTOBER"]
calendar.OCTOBER

먼저 열거형을 호출하여 열 번째 달을 요청합니다. 그런 다음 점 표기법을 사용하여 .OCTOBER 상수를 구체적으로 요청합니다. 마지막으로, 마치 Month의 키인 것처럼 "OCTOBER"를 조회합니다.

이 세 가지 모두 값이 10인 상수인 calendar.OCTOBER를 반환합니다. 열거형과 해당 값을 서로 바꿔서 사용할 수 있습니다:

>>> october = calendar.Month(10)
>>> october
calendar.OCTOBER

>>> october.name
'OCTOBER'

>>> october.value
10

>>> october + 1
11

>>> calendar.month_abbr[october]
'Oct'

.name 및 .value를 사용하여 열거형의 이름과 값을 각각 명시적으로 조회할 수 있습니다. 또한 계산을 하거나 표현식에서 열거형을 일반 값처럼 사용할 수 있습니다.

Day는 Month와 비슷하게 작동하지만 요일 상수를 포함합니다. 월요일은 0으로, 화요일은 1로, 일요일은 6에 해당하는 식으로 표시됩니다. Python 3.12에서 weekday() 함수는 Day 열거형을 반환합니다:

>>> release_day = calendar.weekday(2023, 10, 2)
>>> release_day
calendar.MONDAY

>>> release_day.name
'MONDAY'

>>> release_day.value
0

여기서는 release_day를 Day 열거형으로 표현합니다. 위와 같이 이름과 값을 들여다볼 수 있습니다.

참고: Month은 일반적인 사용과 일관성을 유지하기 위해 1부터 번호 매기기를 시작하며, 1월이 첫 번째 월입니다. Day은 다르며 0부터 번호를 매기기 시작합니다. 평일은 일상적으로 번호가 매겨지지 않으므로 번호 매기기 체계는 날짜/시간 모듈 및 일반적인 0 인덱싱을 사용하는 Python의 특성과 일관성을 유지하는 것을 목표로 합니다.

열거형의 마지막 기능은 열거형을 반복할 수 있다는 것입니다. 다음 예제에서는 월을 반복하여 빠른 판매 보고서를 만듭니다:

>>> sales = {1: 5, 2: 9, 3: 6, 4: 14, 5: 9, 6: 8, 7: 15, 8: 22, 9: 20, 10: 23}
>>> for month in calendar.Month:
...     if month in sales:
...         print(f"{month.value:2d} {month.name:<10} {sales[month]:2d}")
...
 1 JANUARY     5
 2 FEBRUARY    9
 3 MARCH       6
 4 APRIL      14
 5 MAY         9
 6 JUNE        8
 7 JULY       15
 8 AUGUST     22
 9 SEPTEMBER  20
10 OCTOBER    23

각 month은 열거 상수입니다. month이 매출의 키인지 확인하고 month을 매출의 인덱스로 사용할 때 상수를 일반 정수처럼 처리하는 방법에 유의하세요.

참고: 열거 상수를 판매 sales의 키로 사용할 수도 있습니다. 편의를 위해 각 월 상수는 calendar에서 바로 속성으로 사용할 수 있습니다:

>>> sales = {
...     calendar.JANUARY: 5,
...     calendar.FEBRUARY: 9,
...     calendar.MARCH: 6,
...     # ...
... }

이처럼 가독성 높힐 수가 있습니다.

월과 마찬가지로 일에도 반복할 수 있습니다:

>>> ", ".join(day.name for day in calendar.Day)
'MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY'

이 예에서는 모든 요일의 이름을 문자열로 결합합니다. 그러나 요일 이름으로 작업해야 하는 경우에는 calendar.day_name 또는 calendar.day_abbr을 사용하는 것이 더 나은 옵션일 수 있습니다. 이러한 이름은 현지화된 이름을 제공하므로 현지 언어로 번역됩니다.

itertools.batched() : Iterable로 항목 그룹짓기

itertools 표준 라이브러리 모듈에는 이터러블 작업 및 조작을 위한 여러 가지 강력한 기능이 포함되어 있습니다. 예를 들어 두 요일의 모든 조합을 계산할 수 있습니다:

>>> import itertools
>>> list(itertools.combinations(["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"], 2))
[('Mo', 'Tu'), ('Mo', 'We'), ('Mo', 'Th'), ('Mo', 'Fr'), ('Mo', 'Sa'),
 ('Mo', 'Su'), ('Tu', 'We'), ('Tu', 'Th'), ('Tu', 'Fr'), ('Tu', 'Sa'),
 ('Tu', 'Su'), ('We', 'Th'), ('We', 'Fr'), ('We', 'Sa'), ('We', 'Su'),
 ('Th', 'Fr'), ('Th', 'Sa'), ('Th', 'Su'), ('Fr', 'Sa'), ('Fr', 'Su'),
 ('Sa', 'Su')]

여기에는 평일 두 개를 페어링하는 21가지 방법이 나열되어 있습니다. 이터러블을 효율적으로 사용하기 위해 사용할 수 있는 다른 많은 함수가 있습니다.

자주 요청되는 함수는 이터레이터의 요소를 지정된 크기의 일괄 처리로 그룹화할 수 있는 batched()입니다. 지금까지는 공식 itertools 레시피를 기반으로 batched()를 직접 구현하거나 서드파티의 more-itertools 라이브러리를 사용할 수 있었습니다.

Python 3.12에서는 batched()가 itertools에 포함되어 더 쉽게 사용할 수 있게 되었습니다. 이 함수는 주어진 길이의 튜플을 생성하는 이터레이터를 반환합니다. 최종 배치는 다른 배치보다 작을 수 있습니다. 다음 예에서는 처음 10개의 정수를 4개의 숫자로 구성된 배치로 그룹화합니다:

>>> import itertools
>>> for batch in itertools.batched(range(10), n=4):
...     print(batch)
...
(0, 1, 2, 3)
(4, 5, 6, 7)
(8, 9)

처음 두 배치는 각각 4개 숫자로 구성됩니다. 그러나 마지막 배치에는 범위에서 남은 두 개 숫자만 포함됩니다.

대부분의 경우, 일괄 처리로 깔끔하게 분할되는 이터러블로 작업하게 됩니다. 이전 섹션의 예를 들어, 월을 분기로 그룹화할 수 있습니다. 다음과 같이 batched()를 사용할 수 있습니다:

>>> import calendar
>>> import itertools
>>> for quarter in itertools.batched(calendar.Month, n=3):
...     print([month.name for month in quarter])
...
['JANUARY', 'FEBRUARY', 'MARCH']
['APRIL', 'MAY', 'JUNE']
['JULY', 'AUGUST', 'SEPTEMBER']
['OCTOBER', 'NOVEMBER', 'DECEMBER']

새 월 열거를 반복하고 월을 3개월 단위로 그룹화합니다. 리스트 컴프리헨션을 사용하여 각 월의 이름을 출력합니다. 출력된 각 리스트는 분기의 월을 나타냅니다.

새로운 batched() 함수는 이터툴 라이브러리에 또 하나의 강력한 도구를 추가합니다. Iterable로 작업하는 경우 itertools의 기능에 익숙해져야 합니다.

Path.walk() : 파일 및 하위 디렉터리 목록

표준 라이브러리의 또 다른 유용한 모듈은 pathlib입니다. pathlib를 사용하면 파일 시스템의 경로로 작업할 수 있으며, 파일을 효율적으로 읽고 쓸 수 있습니다:

>>> from pathlib import Path
>>> names = ["Brett", "Emily", "Gregory", "Pablo", "Thomas"]

>>> council_path = Path("council.txt")
>>> council_path.write_text("\\n".join(names), encoding="utf-8")
32

여기서 작업 디렉터리에 council.txt라는 이름의 파일 경로를 만듭니다. 그런 다음 해당 파일에 개행으로 구분된 이름 목록을 작성합니다.

Path 개체에는 경로를 조작하고 새 경로를 만드는 많은 메서드가 있지만 파일 및 디렉터리 목록만 제한적으로 지원합니다. .glob()을 사용할 수 있지만 이 메서드는 특정 패턴과 일치하는 이름의 파일 및 디렉터리를 찾을 때 유용합니다:

>>> for path in Path.cwd().glob("*.txt"):
...     print(path)
...
/home/realpython/council.txt

.glob()을 사용하면 현재 작업 디렉터리(cwd)에서 .txt 접미사로 끝나는 모든 파일을 나열할 수 있습니다. 재귀적 대응 함수인 .rglob()을 사용하여 하위 디렉터리에 있는 모든 파일을 나열하고 필터링할 수도 있습니다.

파이썬 3.12에서는 새로운 .walk() 메서드를 사용하여 파일 및 디렉터리로 작업할 수 있습니다. 다음 파일 계층 구조에 뮤지션에 대한 정보가 저장되어 있다고 가정해 보겠습니다:

musicians/
│
├── trumpet/
│   ├── armstrong.txt
│   └── davis.txt
│
├── vocal/
│   └── fitzgerald.txt
│
└── readme.txt

먼저 .rglob()을 사용하여 모든 파일과 디렉터리를 재귀적으로 나열합니다:

>>> for path in sorted(Path.cwd().rglob("*")):
...     print(path)
...
/home/realpython/musicians
/home/realpython/musicians/readme.txt
/home/realpython/musicians/trumpet
/home/realpython/musicians/trumpet/armstrong.txt
/home/realpython/musicians/trumpet/davis.txt
/home/realpython/musicians/vocal
/home/realpython/musicians/vocal/fitzgerald.txt

이렇게 하면 계층 구조의 각 파일 또는 디렉토리에 대해 하나의 경로가 제공됩니다. 일반적으로 .glob() 및 .rglob()을 사용할 때 경로 순서는 결정적이지 않습니다. 파일 목록을 재현 가능하게 유지하는 한 가지 방법은 sorted()를 사용하여 정렬하는 것입니다.

새로운 .walk() 메서드는 디렉터리에 초점을 맞추기 때문에 약간 다르게 작동합니다:

>>> for path, directories, files in Path.cwd().walk():
...     print(path, directories, files)
...
/home/realpython ['musicians'] []
/home/realpython/musicians ['trumpet', 'vocal'] ['readme.txt']
/home/realpython/musicians/trumpet [] ['armstrong.txt', 'davis.txt']
/home/realpython/musicians/vocal [] ['fitzgerald.txt']

.walk()는 세 개의 요소로 구성된 튜플을 생성합니다. path는 항상 디렉터리를 참조합니다. 마지막 두 요소는 각각 하위 디렉터리와 해당 디렉터리 바로 안의 파일 목록입니다. top_down 매개변수를 사용하여 디렉터리가 나열되는 순서를 제어할 수 있습니다.

새로운 메서드는 os.walk()를 기반으로 합니다. 가장 큰 차이점은 새로운 .walk()는 Path 객체를 반환한다는 것입니다.

파이썬 3.12로 업그레이드를 해야할까?

지금까지 파이썬 3.12의 멋진 새 기능과 개선 사항을 살펴봤습니다. 다음 질문은 새 버전으로 업그레이드해야 하는지, 업그레이드해야 한다면 언제 업그레이드해야 하는지일 수 있습니다.

언제나 그렇듯이, 그다지 도움이 되지 않는 대답, 상황에 따라 다르다는 것입니다!

우선 현재 시스템과 함께 Python 3.12를 설치하는 것이 좋습니다. 이렇게 하면 로컬 개발을 할 때 새로운 기능을 사용해 볼 수 있습니다. 버그가 발생하더라도 그 영향이 제한적일 것이므로 위험 부담이 적습니다. 동시에 개선된 오류 메시지와 인터프리터 최적화를 활용할 수 있습니다.

프로덕션 환경에서는 버그와 오류의 결과가 더 심각하므로 제어하는 모든 프로덕션 환경을 업데이트할 때 더욱 주의해야 합니다. 모든 새로운 Python 릴리스는 베타 단계에서 충분한 테스트를 거칩니다. 그래도 전환하기 전에 처음 몇 번의 버그 수정 릴리스가 나올 때까지 기다리는 것이 좋습니다.

Python 3.12로 업데이트할 때 발생할 수 있는 한 가지 문제는 새 버전에 대해 준비되지 않은 타사 라이브러리에 의존한다는 것입니다. 특히 C 확장을 사용하는 패키지는 3.12 버전에 맞게 특별히 컴파일해야 하므로 시간이 다소 걸릴 수 있습니다. 다행히도 점점 더 많은 패키지 관리자가 릴리스에 앞서 패키지를 업데이트하기 때문에 예전보다는 덜 문제가 됩니다.

지금까지 새 인터프리터를 언제부터 사용할 수 있는지 살펴보았습니다. 또 다른 중요한 질문은 업데이트된 구문과 언어의 새로운 기능을 언제부터 활용할 수 있는지입니다. 이전 버전의 Python을 지원해야 하는 라이브러리를 유지 관리하는 경우 새로운 유형 변수 구문이나 개선된 f- 문자열을 사용할 수 없습니다. 이전 버전과 호환되는 코드를 고수해야 합니다.

애플리케이션이 실행되는 환경을 제어하는 애플리케이션을 개발하는 경우에는 상황이 달라집니다. 이 경우 종속성을 사용할 수 있게 되는 즉시 환경을 Python 3.12로 업그레이드한 다음 새 구문을 사용하기 시작할 수 있습니다.

결론

파이썬의 새 버전은 언제나 여러분이 좋아하는 언어와 그 개발에 시간과 노력을 쏟은 모든 자원 봉사자들을 축하할 수 있는 좋은 기회입니다. 많은 개발자의 노력 덕분에 Python 3.12는 몇 가지 개선 사항을 제공합니다.

이 튜토리얼에서는 다음과 같은 새로운 기능과 개선 사항을 살펴보았습니다:

  • 유용한 제안과 안내가 포함된 개선된 오류 메시지
  • Python의 PEG 구문 분석기로 지원되는 더욱 표현력이 풍부한 f-스트링
  • 인라인 이해를 포함한 최적화를 통해 Python을 더 빠르게 실행할 수 있습니다.
  • 제네릭에 주석을 다는 데 사용하는 타 변수에 대한 새로운 구문
  • Linux에서 강력한 perf 프로파일러 지원

이 모든 기능을 바로 활용할 수는 없겠지만, 파이썬 3.12를 설치하여 사용해 볼 수 있습니다. 새 릴리스에 대한 자세한 정보가 궁금하다면 각각 하나의 새로운 기능에 초점을 맞춘 이 튜토리얼을 확인해 보세요:

또한 기존 코드가 미래에 대비할 수 있도록 Python 3.12에서 테스트를 시작하기에 좋은 시기입니다.

728x90

+ Recent posts