728x90
728x90
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
728x90
python3 -m pipdeptree --warn
728x90
728x90
sudo apt-get install libmysqlclient-dev

를 선행해주면 설치가 된다.

728x90
728x90
pip list --format=freeze > ./requirements.txt

버전명이 명시되고 그 결과를 ./requirements.txt에 작성

728x90
728x90

기초

동기와 비동기

  • 동기 : 호출대상(=함수나 메서드)를 호출했을 때, 그 처리가 완료될 때까지 호출자는 다음 처리를 하지 않는 것
  • 비동기 : 호출대상(=함수나 메서드)를 호출했을 때, 호출자는 다음 처리를 진행할 수 있는 것
    • 스레드 기반 비동기를 다중스레드라 한다.
    • 프로세스 기반 비동기를 다중프로세스라 한다.
    • asyncio를 기반으로도 비동기가 가능하다.

다중스레드의 문제점

  • thread-safe 보장 필요

다중프로세스의 문제점

  • 오버헤드가 다중스레드에 비해 큼
  • 호출대상과 반환값은 피클가능한 객체만 가능
    • 이는 multiprocessing 모듈의 Queue를 내부적으로 사용하기 때문
  • OS가 subprocess 생성 시 부모 process를 fork하는게 기본인 OS에서는 난수생성시 같은 값을 그대로 사용할 수 있음
    • random seed를 초기화하거나 파이썬의 경우 built-in random을 사용하면 알아서 random seed 초기화함

다중프로세스vs다중스레드vsAsyncio

  • 다중프로세스가 유용할 때
    • CPU-bound
  • 다중스레드가 유용할 때
    • I/O-bound
  • Asyncio가 유용할 때
    • I/O-bound
    • thread-safe를 비교적 덜 생각해서, 유지보수가 잘 되어야하는 부분
      • 다중스레드의 경우 lock을 걸어야하는 경우가 많아지면 유지보수가 힘들어짐

concurrent.futures

  • 동시 처리를 수행하기 위한 표준 라이브러리
  • 예전에는 threading, multiprocessing 라이브러리를 활용했지만, 지금은 concurrent.futures로 둘 다 구현 가능하고 권장함

concurrent.futures.Future와 concurrent.futures.Executor

concurrent.futures.Executor는 추상 클래스

  • 구현한 서브 클래스로는 ThreadPoolExecutor, ProcessPoolExecutor가 있다.
  • ThreadPoolExecutor나 ProcessPoolExecutor나 API 사용법이 유사하여 서로간 변경이 간단하다.
  • Executor 클래스에 비동기로 수행하고 싶은 callable 객체를 전달하면(submit메서드), 처리 실행을 스케줄링한 future(=Future 객체)를 반환한다.
    • 처리 실행을 스케줄링했다란, 여러 스레드에 실행을 위임하는 것
    • 첫 callable은 반드시 submit과 동시에 실행되지만, 그 이후 callable은 여유 worker가 있으면 submit과 동시에 실행되지만, 여유 worker가 없으면 pending으로 시작됨
    • future의 메서드
      • return값 확인은 result
      • 상태 확인용 메서드(done, running, cancelled)
  • max_workers의 기본값은 코어수 * 5
  • concurrent.futures.Future는
    • 테스트를 위해 직접 만들 수는 있지만, executor.submit을 통해 만들어 사용하기를 권장한다.(공식문서)
    • submit과 동시에 실행할 수 있으면 하고 안되면 pending으로, 상태값을 갖는 객체이다.

concurrent.futures.wait

  • futures, timeout, return_when을 받아 지정한 return_when 규칙과 timeout에 따라 완료된 futures와 완료되지 않은 futures를 나눠 반환한다.

concurrent.futures.as_completed

  • futures, timeout을 받아 완료된 순으로 반환하는 iterators over futures를 받는다.
  • as_completed를 호출하기 전에 완료된 futures가 argument에 있었다면 그것을 먼저 반환함
  • 메인 스레드에서 blocking된 상태로 완료되는 future 순으로 받는 형태이다.

asyncio

  • async/await 문법을 사용하여 동시성 코드를 작성하는 라이브러리
  • I/O-bound and 고수준의 정형화된 네트워크를 작성하는데 적합할 때가 많다.
  • 동시성은 task단위로 이루어진다. coro단위가 아니다.
    • 즉, await가 붙는 순간 await의 오른쪽을 현재 task에서 실행시키고 제어권을 other task로 이동시킨다.
    • 다수의 task로 코드를 작성하지 않는다면 asyncio는 의미가 없다.
  • thread-safe일 경우가 많다. 단일 스레드 기반이므로
  • asyncio로 작성되지 않은 타 라이브러리와 함께 사용할 경우에는 loop.run_in_executor를 통해 다중스레드 형태로 사용한다.

High-level APIs

Coroutines and Tasks

  • Coroutines
    • coroutine function
      • async def로 작성된 함수
    • coroutine object
      • coroutine function을 호출하여 얻은 객체
      • coro라고 부를 때가 많다.
      • coro자체가 function body를 실행하지는 않는다.
    • coro(coroutine function body)를 실행하는 방법
      • asyncio.run()에 넘긴다.
        • 대게는 asyncio.run(main()) 형태로 top-level entry point로서 사용
      • await coro
        • 이것은 coro가 완료될 때까지 current task에서는 기다리고 제어권은 other task가 가져간다.
      • asyncio.create_task(coro, *, name=None):
        • coro를 실행과 동시에 task로 변환하여 반환한다.(task_1)
          • coro를 실행하고 내부에 제어권을 계속 가져가다가 await를 만나면 그 때 task_1의 제어권을 other task에 넘긴다.
      • asyncio.gather(*coros_or_futures, loop=None, return_exceptions=False)
        • coro들은 task로 wrap된다. 꼭 입력받은 순으로 wrap되지는 않음
          • coro1의 body를 실행하다가 await를 만나면 coro1을 task1으로 바꾸고 이벤트 루프에 등록되고, 다른 argument에 대해서도 똑같이 한다. 하지만 순서는 보장안됨
        • one future를 반환한다. 따라서 awaitable
        • res = await asyncio.gather(coro1, coro2, ...) 의 형태로 사용
        • res는 완료순이 아니라 입력받은 순의 결과로 정렬된다.
      • asyncio.wait(fs, *, timeout=None, return_when=ALL_COMPLETED)
        • awaitable objects(=fs)를 동시적으로 실행한다.
        • timeout, return_when을 고려하여 wait
        • two sets of Future를 반환(done, not_done)
      • asyncio.as_completed(fs, *, loop=None, timeout=None)
        • awaitable objects(=fs)를 동시적으로 실행하고
        • iterator over coro
          • 각 coro는 fs에 있는 coro가 아니라, fs에 있는 것 중 return이 먼저 나오는 것을 받아오는 새로운 coro인 것이다.(wait for one)
  • Awaitables
    • 키워드 await가 붙을 수 있는 객체를 awaitable object라 한다.
    • await “something”은 current task에서 something이 완료될 때까지 기다린다는 것이다. 그리고 제어권은 other task로 간다.
      • other task가 없으면 current task에서 그냥 기다릴 뿐
      • 동시에 여러 coros을 실행케하려면 asyncio.gather나 asyncio.wait, asyncio.as_completed 를 실행한다.
      • 완전 동시는 아니지만 제어권을 금방 다시 가져와서 여러 coro를 순차적으로 실행시키는 것은 asyncio.create_task를 사용한다.
    • awaitable은
      • coro
      • task/future
      • __await__가 정의된 클래스의 객체
  • current task에서 주도적으로 other task로 동작시키는 방법(제어권을 넘기는 방법)
    • asyncio.sleep(0)을 사용한다.
      • asyncio.sleep은 coro를 반환한다.
      • await asyncio.sleep(0)을 하면 current task에서는 0초만큼 block하고 other task로 제어권을 넘김
  • event loop
    • 이벤트 루프는 다수의 tasks를 가질 수 있고 각 task는 각자의 call stack을 갖는다. 하지만 각 시점마다 한개의 task만 처리한다.
    • coroutine function 내부에서 현재 event loop를 얻으려면 asyncio.get_running_loop()를 호출
    • loop.run_in_executor를 통해 동기 함수도 다중스레드로 처리하여 동시성을 얻을 수 있다.
    • asyncio.{run, gather, wait} 등으로 coro를 실행시킨게 아니라, get_event_loop로 얻은 loop를 두고 loop.create_task했을 때는 coro가 즉시 동작하지 않는다.
    • get_event_loop는 하나의 스레드 사용을 위해 만들어진 메소드다. 다수 스레드 경우 스레드 내부에 new_event_loop 호출하고 set_event_loop해서 스레드에 새 이벤트 루프를 매핑하고 new event loop를 해당 스레드에서 사용하면 된다.
    • asyncio.create_task(coro)도 결국 loop.create_task(coro)을 쓰는 것이고, 후자의 경우 loop를 get_running_loop를 통해 얻은 loop여야 한다.
      • asyncio.create_task(coro)을 쓰는 게 현대식 방법

asyncio.Future

  • concurrent.futures.Future처럼, 언젠가 완료될 작업 클래스를 가리킨다.

asyncio.Task

  • asyncio.Future의 subclass
  • coroutin을 wraps했을 때의 future를 task라 한다.

asyncio.run

  • parameter로 받은 coro를 실행하고 result를 반환함
  • 호출하면 이벤트 루프를 생성하고 이 이벤트 루프가 coro의 실행을 제어한다.
  • 단일 이벤트 루프로 돌기 때문에, asyncio.run(main()) 형태로 한번의 호출만 존재한다.

asyncio.ensure_future

  • future(혹은 task)를 넣으면 그대로 반환
  • coro를 넣으면 task로 만들어 반환
  • 프레임워크 설계자를 위한 함수(최종 사용자는 create_task 사용하면 됨)
    • 무조건 결과가 future임을 만들기 위한 함수이다.
    • asyncio.gather(*aws, …)도 내부에 보면 awaitable 객체가 오면 다 future가 되게 만드는 ensure_future를 호출한다.

loop.run_in_executor

  • asyncio를 활용하다가 동기 I/O를 동시적으로 처리하려고 할 때 사용
  • parameter로 concurrent.futures.Executor 구현 클래스 객체를 넣든가, None(default executor를 사용하며, set_default_executor를 따로 하지 않았다면 ThreadPoolExecutor를 사용)
  • executor, func, *args를 parameter로 받는다.

async with

  • 기존 동기 context manager의 with 사용과 다른 점은 딱 하나다.
    • enter와 exit의 동작이 coro로 수행된다는 점이다.
      • 즉, enter와 exit가 I/O-bound여서 event loop가 그 시간에 다른 task를 했으면할 때 사용한다.
  • 즉, aenter와 aexit가 I/O-bound일 때, async로 둬서 다른 task가 cpu사용할 수 있게끔하기 위함이다.

async for

  • 기존 동기 이터레이터 만드는 것과 차이점은 딱 하나다.
    • anext가 coro로 동작한다는 것
    • 대표적인 예로 next 원소를 db에서 가져오는 경우, I/O-bound이므로 이 때 event loop가 타 task를 다룰 수 있게 하는 상황에 필요

예시

순차 처리와 멀티스레딩 기본 예제

"""
아래 max_workers를 바꿔가며 테스트한다.
max_workers=1이면 첫 download url은 submit과 동시에 running이지만
다음 url부터는 submit해도 pending으로 시작됨

max_workers=3으로 두면 sequential보다 다중스레드가 낫다는걸 확인가능
"""
from concurrent.futures import as_completed
from concurrent.futures import ThreadPoolExecutor
import time
from hashlib import md5
from pathlib import Path
from urllib import request

def elapsed_time(f):
  def wrapper(*args, **kwargs):
    st = time.time()
    v = f(*args, **kwargs)
    print(f"{f.__name__}: {time.time() - st}")
    return v
  return wrapper

urls = [
  '<https://twitter.com>',
  '<https://facebook.com>',
  '<https://instagram.com>'
]

def download(url):
  print(f"DOWNLOAD START, url = {url}")
  req = request.Request(url)

  # 파일 이름에 / 등이 포함되지 않도록 함
  name = md5(url.encode('utf-8')).hexdigest()
  file_path = './' + name
  with request.urlopen(req) as res:
    Path(file_path).write_bytes(res.read())
    return url, file_path

@elapsed_time
def get_sequential():
  for url in urls:
    print(download(url))

@elapsed_time
def get_multi_thread():
  with ThreadPoolExecutor(max_workers=1) as executor:
    futures = [executor.submit(download, url) for url in urls]
    print(futures)
    for future in as_completed(futures):
      print(future.result())

if __name__ == '__main__':
  get_sequential()
  get_multi_thread()

thread-unsafe 예시와 thread-safe 수정 예시

from concurrent.futures import ThreadPoolExecutor, wait

# thread-unsafe
class Counter:
  def __init__(self):
    self.count = 0
  def increment(self):
    self.count += 1

def count_up(counter):
  for _ in range(1_000_000):
    counter.increment()

if __name__ == "__main__":
  counter = Counter()
  thread = 2
  with ThreadPoolExecutor() as e:
    futures = [e.submit(count_up, counter) for _ in range(thread)]
    done, not_done = wait(futures)  # (*)

  print(f'{counter.count=:,}')  # 2,000,000이 표시되지 않음

"""(*)
wait(fs, timeout=None, return_when=ALL_COMPLETED)
futures를 받아 기다린다.
	- parameter timeout
		timeout(seconds)까지 완료된 것과 완료되지 않은 것을 
		tuple로 반환한다.
	- parameter return_when
		FIRST_COMPLETED, FIRST_EXCEPTION, ALL_COMPLETED 상수 설정
		모두 concurrent.futures에 존재하는 상수들
		default는 ALL_COMPLETED
"""
import threading
from concurrent.futures import ThreadPoolExecutor, wait

# thread-unsafe
class ThreadSafeCounter:
  lock = threading.Lock()  # 
  def __init__(self):
    self.count = 0
  def increment(self):
    with self.lock:
      self.count += 1

def count_up(counter):
  for _ in range(1_000_000):
    counter.increment()

if __name__ == "__main__":
  counter = ThreadSafeCounter()
  thread = 2
  with ThreadPoolExecutor() as e:
    futures = [e.submit(count_up, counter) for _ in range(thread)]
    done, not_done = wait(futures)  # (*)

  print(f'{counter.count=:,}')  # 2,000,000

피보나치수열 - Sequential, 다중스레드, 다중프로세스 비교

"""
피보나치 수열 - Sequential
"""

import sys
import os
import time

def elapsed_time(f):
  def wrapper(*args, **kwargs):
    st = time.time()
    v = f(*args, **kwargs)
    print(f"{f.__name__}: {time.time() - st}")
    return v
  return wrapper

def fibonacci(n):
  a, b = 0, 1
  for _ in range(n):
    a, b = b, b + a
  else:
    return a

@elapsed_time
def get_sequential(nums):
  for num in nums:
    _ = fibonacci(num)

def main():
  n = 1_000_000
  nums = [n] * os.cpu_count()
  get_sequential(nums)

if __name__ == '__main__':
  main()  # 168초(cpu 개수마다 다를 값)
"""
피보나치 수열 - multi-process
"""

import os
import sys
import time

from concurrent.futures import ProcessPoolExecutor, as_completed

def elapsed_time(f):
  def wrapper(*args, **kwargs):
    st = time.time()
    v = f(*args, **kwargs)
    print(f"{f.__name__}: {time.time() - st}")
    return v
  return wrapper

def fibonacci(n):
  a, b = 0, 1
  for _ in range(n):
    a, b = b, b + a
  else:
    return a

@elapsed_time
def get_multi_process(nums):
  with ProcessPoolExecutor() as e:
    futures = [e.submit(fibonacci, num) for num in nums]
    for future in as_completed(futures):
      _ = future.result()

def main():
  n = 1_000_000
  nums = [n] * os.cpu_count()
  get_multi_process(nums)

if __name__ == '__main__':
  main()  # 약 14초
"""
피보나치 수열 - multi-thread
"""

import os
import sys
import time

from concurrent.futures import ThreadPoolExecutor, as_completed

def elapsed_time(f):
  def wrapper(*args, **kwargs):
    st = time.time()
    v = f(*args, **kwargs)
    print(f"{f.__name__}: {time.time() - st}")
    return v
  return wrapper

def fibonacci(n):
  a, b = 0, 1
  for _ in range(n):
    a, b = b, b + a
  else:
    return a

@elapsed_time
def get_multi_thread(nums):
  with ThreadPoolExecutor() as e:
    futures = [e.submit(fibonacci, num) for num in nums]
    for future in as_completed(futures):
      _ = future.result()

def main():
  n = 1_000_000
  nums = [n] * os.cpu_count()
  get_multi_thread(nums)

if __name__ == '__main__':
  main()

unpickable callable을 다중프로세스에 사용시 에러와 해결

from concurrent.futures import ProcessPoolExecutor, wait

func = lambda: 1
# def func():
#   return 1

def main():
  with ProcessPoolExecutor() as e:
    future = e.submit(func)
    done, not_done = wait([future])
  print(future.result())  # (*) 

if __name__ == "__main__":
  main()

"""
(*) 여기서 error raised, multiprocessing.Queue에서 반환값을 가져올 때 
pickle dump를 사용하는데 lambda가 pickle가능하지 않아 에러 발생
"""
from concurrent.futures import ThreadPoolExecutor, wait

func = lambda: 1
# def func():
#   return 1

def main():
  with ThreadPoolExecutor() as e:
    future = e.submit(func)
    done, not_done = wait([future])
  print(future.result())  # (*) 

if __name__ == "__main__":
  main()

"""
ThreadPoolExecutor는 에러 발생하지 않음
"""
from concurrent.futures import ProcessPoolExecutor, wait

def func():
  return 1

def main():
  with ProcessPoolExecutor() as e:
    future = e.submit(func)
    done, not_done = wait([future])
  print(future.result())  # (*) 

if __name__ == "__main__":
  main()

"""
일반함수는 ProcessPoolExecutor여도 pickle가능하므로 error not raised
"""

다중프로세스에서 fork 방식의 난수 생성 문제와 해결

# np_random_multiprocess.py

from concurrent.futures import ProcessPoolExecutor, as_completed

import numpy as np

def use_numpy_random():
  return np.random.random()

def main():
  with ProcessPoolExecutor() as e:
    futures = [e.submit(use_numpy_random) for _ in range(3)]
    for future in as_completed(futures):
      print(future.result())

if __name__ == "__main__":
  main()

"""
해당코드를 WINDOWS, MAC에서는 문제 없음
다만 UNIX 환경에서 실행하면 같은 값이 중복해서 나온다.
이는 UNIX에서는 자식 프로세스 만드는 방식이 부모 프로세스를 복제하는 fork방식이 기본값
"""
# 해결방법 1 np.random.seed() 추가

from concurrent.futures import ProcessPoolExecutor, as_completed

import numpy as np

def use_numpy_random():
	np.random.seed()  # (*)
  return np.random.random()

def main():
  with ProcessPoolExecutor() as e:
    futures = [e.submit(use_numpy_random) for _ in range(3)]
    for future in as_completed(futures):
      print(future.result())

if __name__ == "__main__":
  main()

"""
(*) np.random.seed()를 통해 난수 생성기를 초기화해서 해결
"""
# 해결방법 2 np.random말고 빌트인 random 사용

from concurrent.futures import ProcessPoolExecutor, as_completed

import numpy as np

def use_random():  # (*)
  return random.random()

def main():
  with ProcessPoolExecutor() as e:
    futures = [e.submit(use_random) for _ in range(3)]
    for future in as_completed(futures):
      print(future.result())

if __name__ == "__main__":
  main()

"""
(*) built-in random의 경우 fork할 떄 자동으로 난수생성기를 초기화함
"""

asyncio의 helloworld

import asyncio

async def main():
    print('Hello ...')
    await asyncio.sleep(10)
    print('... World!')

# Python 3.7+
asyncio.run(main())  # Hello ...  출력 후 10초 후에 ... World!가 출력

asyncio.gather 예제

import asyncio
import random

async def call_web_api(url):
  # Web API 처리를 sleep으로 대체
  print(f'send a request: {url}')
  await asyncio.sleep(random.random())
  print(f'got a response: {url}')
  return url

async def async_download(url):
  # await를 사용해 코루틴을 호출
  response = await call_web_api(url)
  return response

async def main():
  task = asyncio.gather(
    async_download('<https://twitter.com/>'),
    async_download('<https://facebook.com/>'),
    async_download('<https://instagram.com/>'),
  )
  return await task

result = asyncio.run(main())

asyncio.create_task를 통해 코루틴 함수 내부도 동시적으로 실행하기

# create_task를 쓰지 않은 예
# 6초 걸려 끝남
import asyncio

async def coro(n):
  await asyncio.sleep(n)
  return n

async def main():
  print(await coro(3))
  print(await coro(2))
  print(await coro(1))

asyncio.run(main())
# create_task를 사용한 예
# 3초만에 끝남
import asyncio

async def coro(n):
  await asyncio.sleep(n)
  return n

async def main():
  task1 = asyncio.create_task(coro(3))
  task2 = asyncio.create_task(coro(2))
  task3 = asyncio.create_task(coro(1))  
  print(await task1)
  print(await task2)
  print(await task3)
  

asyncio.run(main())

"""
3
2
1
"""

loop.run_in_executor를 통해 동기 I/O를 동시적으로 처리하기

import asyncio
from concurrent.futures import as_completed
from concurrent.futures import ThreadPoolExecutor
import time
from hashlib import md5
from pathlib import Path
from urllib import request

urls = [
  '<https://twitter.com>',
  '<https://facebook.com>',
  '<https://instagram.com>'
]

def download(url):
  print(f"DOWNLOAD START, url = {url}")
  req = request.Request(url)

  # 파일 이름에 / 등이 포함되지 않도록 함
  name = md5(url.encode('utf-8')).hexdigest()
  file_path = './' + name
  with request.urlopen(req) as res:
    Path(file_path).write_bytes(res.read())
    return url, file_path

async def main():
  loop = asyncio.get_running_loop()
  # 동기 I/O를 이용하는 download를 동시적으로 처리
  futures = []
  for url in urls:
    future = loop.run_in_executor(None, download, url)
    futures.append(future)

  for result in await asyncio.gather(*futures):
    print(result)

asyncio.run(main())

ForLoop도 task마다 돌게 만든 예제

import asyncio

async def counter(name: str):
    for i in range(0, 100):
        print(f"{name}: {i}")
        await asyncio.sleep(0)

async def main():
    tasks = []
    for n in range(0, 4):
        tasks.append(asyncio.create_task(counter(f"task{n}")))

    while True:
        tasks = [t for t in tasks if not t.done()]
        if len(tasks) == 0:
            return

        await tasks[0]

asyncio.run(main())

"""
CounterStart of task0
task0: 0
CounterStart of task1
task1: 0
CounterStart of task2
task2: 0
CounterStart of task3
task3: 0
task0: 1
task1: 1
task2: 1
task3: 1
task0: 2
task1: 2
task2: 2
task3: 2
task0: 3
task1: 3
task2: 3
task3: 3
task0: 4
task1: 4
task2: 4
task3: 4
task0: 5
task1: 5
task2: 5
task3: 5
task0: 6
task1: 6
task2: 6
task3: 6
task0: 7
task1: 7
task2: 7
task3: 7
task0: 8
task1: 8
task2: 8
task3: 8
task0: 9
task1: 9
task2: 9
task3: 9
"""

async with 예제

import asyncio
import sys

async def log(msg, l=10, f='.'):
  for i in range(l*2+1):
    if i == l:
      for c in msg:
        sys.stdout.write(c)
        sys.stdout.flush()
        await asyncio.sleep(0.05)
    else:
      sys.stdout.write(f)
      sys.stdout.flush()
    await asyncio.sleep(0.2)
  sys.stdout.write('\\n')
  sys.stdout.flush()

class AsyncCM:
  def __init__(self, i):
    self.i = i
  async def __aenter__(self):
    await log('Entering Context')
    return self
  async def __aexit__(self, *args):
    await log('Exiting Context')
    return self

async def main1():
  '''Test Async Context Manager'''
  async with AsyncCM(10) as c:
    for i in range(c.i):
      print(i)
## 실행

# loop = asyncio.get_event_loop()
# loop.run_until_complete(main1())
async def main():
  task = asyncio.gather(main1(), main1())
  return await task

asyncio.run(main())

"""
....................EEnntteerriinngg  CCoonntteexxtt....................
0
1
2
3
4
5
6
7
8
9
.
0
1
2
3
4
5
6
7
8
9
...................EExxiittiinngg  CCoonntteexxtt....................
"""
"""
__aenter__와 __aexit__가 각각 다른 task로 동작함을 알 수 있다.
"""

async for 예제

# 블로킹 이터레이터
class A:
	def __iter__(self):
		self.x = 0
		return self
	def __next__(self):
		if self.x > 2:
			raise StopIteration
		else:
			self.x += 1
			return self.x

for i in A():
	print(i)  

"""
1
2
3
"""

# 비동기(논블로킹) 이터레이터
import asyncio
from aioredis import create_redis

async def main():
	redis = await create_redis(('localhost', 6379))
	keys = ["Americas", "Africa", "Europe", "Asia"]
	async for value in OneAtATime(redis, keys):  # (1)
		await do_something_with(value)  # (2)

class OneAtATime:
	def __init__(self, redis, keys):
		self.redis = redis
		self.keys = keys
	def __aiter__(self):
		self.ikeys = iter(self.keys)
		return self
	async def __anext__(self):
		try:
			k = next(self.ikeys)
		except StopIteration:
			raise StopAsyncIteration  # (3)
		value = await self.redis.get(k)  # (4)
		return value

asyncio.run(main())

"""
- def __aiter__를 구현해야한다.(not async def)
- __aiter__()는 async def __anext__()를 구현한 객체를 반환해야한다.
- __anext__()는 반복의 각 단계에 대한 값을 반환하고, 반복이 끝나면 StopAsyncIteraction을 발생시켜야 한다.

(1) async for를 사용한다. 중요한 점은 반복 중에 다음 데이터를 얻기 전까지 반복 자체를 일시 정지할 수 있다는 점이다.
(2) I/O 동작을 수행한다고 하자. 예를 들면 데이터를 변환하고 다른 데이터베이스에 전달하는 동작
(3) 일반적인 iteration이 끝나서 StopIteration을 발생시키고 그것을 StopAsyncIteration으로 변환시키는 방법
(4) redis에서 값을 가져올 때도 await를 줘서 이벤트 루프가 다른 작업을 할 수 있게 했다.
"""

# 비동기(논블로킹) 제너레이터로 바꾼 예제
import asyncio
from aioredis import create_redis

async def main():
	redis = await create_redis(('localhost', 6379))
	keys = ["Americas", "Africa", "Europe", "Asia"]
	async for value in one_at_a_time(redis, keys):
		await do_something_with(value)

async def one_at_a_time(redis, keys):  # (1)
	for k in keys:
		value = await redis.get(k)
		yield value  # (2)

asyncio.run(main())

"""
- 코루틴과 제너레이터는 완전 다른 개념이다.
- 비동기 제너레이터는 일반 제너레이터와 유사하게 작동한다.
- 반복 수행 시, for 대신 async for를 사용한다.
(1) 비동기 제너레이터는 async def로 정의한다.
(2) 비동기 제너레이터는 제너레이터처럼 yield를 쓴다.

"""

 

contextlib 사용한 async with 예제

# 먼저 일반적인 블로킹 방식
from contextlib import contextmanager

@contextmanager
def web_page(url):
	data = download_webpage(url)  # (1)
	yield data
	update_stats(url)  # (2)

with web_page('google.com') as data:
	process(data)

"""
contextmanager 데커레이터는 제너레이터함수를 콘텍스트 관리자로 변환한다.
yield한 것이 as data의 data로 들어간다.
with문이 끝나면 update_stats(url)이 실행된다.

(1) 위 블로킹 방식에서는 (1)을 수행하는 동안 프로그램이 중지 된다.
download_webpage(url)이 coro가 되게 수정을 하든가, 아래 executor 방식을 사용한다.
coro가 되게 수정하면 베스트겠지만, third-pary library인 경우 수정이 쉽지 않다.

(2) URL을 통해 전달받은 데이터를 처리할 때마다 다운로드 횟수와 같은 통계를 갱신하는 상황을 가정한 것이다. 만약 이 함수가 데이터베이스를 갱신하는 것과 같은 I/O 동작을 내부적으로 포함하고 있다면 마찬가지로 블로킹 호출이 되므로, coro가 되게 하든 executor를 활용한다.
"""

# 논블로킹(단, download_webpage, update_stats이 coroutine function이어야함)
from contextlib import asynccontextmanager

@asynccontextmanager
async def web_page(url):
	data = await download_webpage(url)
	yield data
	await update_stats(url)

async with web_page('google.com') as data:
	process(data)

"""
download_webpage와 update_stats를 coroutine function으로 수정했을 때의 예제이다.
@asynccontextmanager를 사용하려면 async def로 정의해야한다.
yield가 있으니 제너레이터 함수인데, async def까지 사용했으니, 이 함수를 호출하면 비동기 제너레이터 객체를 반환한다.
비동기 제너레이터 함수/객체임을 확인하는 방법은 
inspect 모듈의 isasyncgenfunction()/isasyncgen()가 있다.
asynccontextmanager를 활용하려면 async with로 시작해야한다.
"""

# executor를 활용한 논블로킹(download_webpage, update_stats를 coro로 바꾸기 힘들 때)
from contextlib import asynccontextmanager

@asynccontextmanager
async def web_page(url):
	loop = asyncio.get_event_loop()
	data = await loop.run_in_executor(None, download_webpage, url)
	yield data
	await loop.run_in_executor(None, update_stats, url)

async with web_page('google.com') as data:
	process(data)

"""
별도의 스레드에서 executor로 블로킹 호출함수를 넘겨 논블로킹을 구현한 방식

"""

728x90
728x90
  • import 타임이란 것을 체험?해보자.
# registration.py
# BEGIN REGISTRATION

registry = []  # <1>

def register(func):  # <2>
    print('running register(%s)' % func)  # <3>
    registry.append(func)  # <4>
    return func  # <5>

@register  # <6>
def f1():
    print('running f1()')

@register
def f2():
    print('running f2()')

def f3():  # <7>
    print('running f3()')

def main():  # <8>
    print('running main()')
    print('registry ->', registry)
    f1()
    f2()
    f3()

if __name__=='__main__':
    main()  # <9>

# END REGISTRATION

"""
$ python3 registration.py
running register(<function f1 at 0x100631bf8>)  # running main보다 먼저 실행
running register(<function f2 at 0x100631c80>)  # running main보다 먼저 실행
running main()
registry -> [<function f1 at 0x100631bf8>, <function f2 at 0x100631c80>]
running f1()
running f2()
running f3()
"""

 

# registration.py를 실행하지 않고 import하면
>>> import registration
running register(<function f1 at 0x10063b1e0>)  # decorator의 실행이 확인됨
running register(<function f2 at 0x10063b268>)

즉, decorator는 모듈이 import되자마자 실행되지만, decorated function은 명시적으로 호출될 때만 실행됨을 알 수 있다. 이 예제는 파이썬 개발자가 "import time"과 "runtime"이라고 부르는 것의 차이를 명확히 보여준다.

  • 참고
    • 위처럼 decorator와 decorated function이 한 모듈에 정의되어 있는 것은 일반적이지 않다. 대개 다른 모듈에 정의하여 사용함
    • 위처럼 decorator가 decorated function을 그대로 반환하는 것은 일반적이지 않다. 대개 내부함수를 정의해서 반환한다.
    • 그대로 반환하는 부분은 유용하지 않지만, registry.append 부분처럼 웹 프레임워크에서 "함수를 어떤 중앙의 레지스트리에 추가"하는 형태로 사용한다.
  • 이와 마찬가지로
    • class가 포함된 module을 import할 때 class variable이 실행됨
      • 이는 singleton을 아주 쉽게 구현할 때 사용
    • function을 정의할 때 default value를 mutable한 것으로 설정하면 안되는 이유가
      • function을 import할 때, default value 객체를 만든다.
      • 이후 function을 call을 할 때, import할 때 만들어진 default value를 계속 활용한다.
      • 근데, 이 default value가 mutable이었으면 기대한 바와 다르게 프로그래밍 될 수 있다.
        def test_function(a, b=[]):
            b.append(a)
            print(b)
        
        if __name__ == '__main__':
            test_function(3)  # [3], expected
            test_function(4)  # [3, 4], unexpected​
728x90
728x90

수명 주기 동안 결코 변하지 않는 해시값을 갖고 있고(__hash__() 메서드가 필요) 다른 객체와 비교할 수 있으면(__eq__() 메서드가 필요), 객체를 해시가능하다고 한다. 동일(==)하다고 판단되는 객체는 반드시 해시값이 동일해야 한다.

  • 모든 불변형은 해시가능하다.
    • 조금 틀린 말이다. tuple의 경우 불변형이지만, 해시불가능한 객체를 참조할 때는 튜플 그 자체도 해시불가능해진다.
  • 사용자 정의 자료형은 기본적으로 해시가능하다. 그 이유는 __hash__() 가 id() 를 이용하여 구하므로 모든 객체가 서로 다르기 때문
  • 파이썬 dictionary의 경우 hashtable, open addressing 방식으로 구현되어 있다. dict.get("key")로 조회를 하는 경우 다음 순서를 따른다.
    • "key"의 hash값으로 먼저 조회
    • hash값이 hashtable에 존재하더라도 그 hash값을 가지는 원소의 "key"를 비교한다. hash와 key값 비교(==)가 완료된 것의 value를 반환한다.
  • 따라서 아래의 예제가 이해된다.즉, key는 가변적이고 hash값은 dictionary 객체가 생성될 때의 값을 사용한다는 것을 알기.
  • class MyList(list):
        # 임의로 수명주기 동안 변하지 않는 hash가 아니라
        # 수명주기 동안에도 변하는 hash를 준 예제
        def __hash__(self):
            return sum(self)
    
    
    my_list = MyList([1, 2, 3])
    
    my_dict = {my_list: 'a'}
    
    print(my_dict.get(my_list))  # a
    
    my_list[2] = 4  # __hash__() becomes 7
    print(next(iter(my_dict)))  # [1, 2, 4], 즉 key가 변경
    
    print(my_dict.get(MyList([1, 2, 3])))  
    # None, hash값은 같지만 key비교가 안맞아서 조회 불가 
    
    print(my_dict.get(MyList([1, 2, 4])))  
    # None, dictionary 객체가 생성될 때의 hash값(6)과 안맞아서 조회 불가
    # 이 부분이 중요, 같은 key값을 넣었지만, dictionary(hashtable)이 보유한 hash는 6이라서 안된 점
    # 즉, line:14에서 key변경을 해도 dictionary는 보유한 hash를 업데이트 하지 않음(6으로 고정)
    
    my_list[0] = 0  # __hash_() is 6 again, but for different elements
    print(next(iter(my_dict)))  # [0, 2, 4], 즉 key가 변경
    
    print(my_dict.get(my_list))  
    # 'a', hash값비교(6==6), key값비교(MyList([0,2,4])==MyList([0,2,4]))돼서 조회가능
    • 따라서,
      • hash메소드는 객체 수명 주기 동안 불변한 값으로 정의하라는 지침이 있는 이유를 알 수 있다.
      • dictionary key는 왜 hashable만 받게 해둔 것인지를 알 수 있다.
728x90
728x90

class로부터 객체를 생성할 때

__new__가 실행되고 이후 __init__이 실행된다.

__new__를 생략했을 시, 상위 class의 __new__를 실행하여 객체를 생성한다.

상위 클래스를 생략했다면 object class의 __new__를 실행하여 객체를 생성한다.

 

즉, object 클래스의 __new__를 오버라이딩하여 객체생성을 커스텀할 수가 있음

 

 

 

질문 1

객체 생성을 커스텀할 필요가 있나?

->싱글톤 패턴 참고

 

728x90
728x90

상속이 일어난 경우 

super를 사용할 일이 있다.

 

super(C, self).method(sth) 란

C의 상위에서부터(A->B->C 형태의 상속이라면)

B에서부터 method을 찾고 그 method의 인자로 sth을 넣어서

self, super(C, self).method(sth) line을 포함하고 있는 class의 instance에

적용하란 의미이다.

 

질문1

B에 method가 없고 A에 있다면?

->A의 method가 실행됨

B에 method가 있다면?

->B의 method가 실행됨

 

질문2

B에 method가 존재하는 것을 개발자가 미리 알고 있어서 

super(C, self).method(sth) 대신에

B.method(sth)을 쓰면 안되나?

->코드 유지보수면에 있어서 super(C, self).method(sth)가 유리(B의 class명을 바꾼다거나 등을 생각)

 

질문3

super().method(sth)의 의미는 무엇인가?

->super()는 super(현재 class, self)와 같음

즉, C라는 class 내부에 super().method(sth)했다면 super(C, self)와 같음

 

 

정리하면, super()는 현재 class 내의 method와 이름이 같은 것인데 걔가 아닌 상위 클래스의 method을 써야할 때 사용한다. 

특히 __init__ method에서 상위 class내의 __init__을 그대로 사용하고, 추가 작업을 할 때 보통 사용

즉, super().__init__(*arg)같은 형태로 많이 사용함

 

참고자료:

Python | super() in single inheritance - GeeksforGeeks

 

Python | super() in single inheritance - GeeksforGeeks

A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.

www.geeksforgeeks.org

 

 

728x90

+ Recent posts