4. 실행 모델
4. 실행 모델 (Execution model)
이 장에서는 프로그램이 어떻게 구성되고, 이름이 어떻게 바인딩되고 해석되는지, 예외가 어떻게 처리되는지, 그리고 Python 런타임이 추상적으로 어떻게 동작하는지를 설명합니다.
본문
4.1. 프로그램의 구조 (Structure of a program)
Python 프로그램은 코드 블록(code block)으로 구성돼요. 블록은 하나의 단위로 실행되는 Python 프로그램 텍스트 조각이에요. 다음은 블록이에요: 모듈, 함수 본문, 클래스 정의. 대화형으로 입력하는 각 명령은 블록이에요. 스크립트 파일(인터프리터에 표준 입력으로 주어지거나 인터프리터에 명령줄 인자로 지정된 파일)은 코드 블록이에요. 스크립트 명령(-c 옵션으로 인터프리터 명령줄에 지정된 명령)은 코드 블록이에요. -m 인자를 사용해 명령줄에서 최상위 스크립트(모듈 __main__)로 실행되는 모듈도 코드 블록이에요. 내장 함수 eval()과 exec()에 전달되는 문자열 인자도 코드 블록이에요.
코드 블록은 *실행 프레임(execution frame)*에서 실행돼요. 프레임은 (디버깅에 사용되는) 몇 가지 관리 정보를 포함하고, 코드 블록의 실행이 완료된 후 실행이 어디서 어떻게 계속될지 결정해요.
4.2. 이름 짓기와 바인딩 (Naming and binding)
4.2.1. 이름의 바인딩 (Binding of names)
이름은 객체를 가리켜요. 이름은 이름 바인딩 연산에 의해 도입돼요.
다음 구조들은 이름을 바인딩해요:
- 함수의 형식 매개변수(formal parameter),
- 클래스 정의,
- 함수 정의,
- 할당 표현식(assignment expression),
- 할당에서 식별자로 나타나는 대상(target):
for루프 헤더,with문에서as뒤,except절,except*절, 또는 구조적 패턴 매칭의 as-패턴,- 구조적 패턴 매칭의 캡처 패턴(capture pattern),
import문.type문.- 타입 매개변수 목록.
from ... import * 형태의 import 문은 가져온 모듈에 정의된 모든 이름을 바인딩해요. 밑줄로 시작하는 이름은 제외하고요. 이 형태는 모듈 레벨에서만 사용할 수 있어요.
del 문에서 나타나는 대상도 이 목적상 바인딩된 것으로 간주돼요 (실제 의미는 이름을 바인딩 해제하는 것이지만요).
각 할당이나 import 문은 클래스나 함수 정의로 정의된 블록 안에서, 또는 모듈 레벨(최상위 코드 블록)에서 발생해요.
이름이 어떤 블록에 바인딩되면, nonlocal이나 global로 선언되지 않는 한 그 블록의 지역 변수예요. 이름이 모듈 레벨에서 바인딩되면 전역 변수예요. (모듈 코드 블록의 변수는 지역이자 전역이에요.) 변수가 코드 블록에서 사용되지만 거기서 정의되지 않으면, 그것은 자유 변수(free variable)예요.
프로그램 텍스트에서 이름이 나타날 때마다, 그 이름은 다음 이름 해석 규칙에 의해 확립된 그 이름의 바인딩을 가리켜요.
4.2.2. 이름의 해석 (Resolution of names)
*스코프(scope)*는 블록 안에서 이름의 가시성을 정의해요. 지역 변수가 블록에 정의되면, 그 스코프는 그 블록을 포함해요. 정의가 함수 블록에서 발생하면, 포함된 블록이 그 이름에 대한 다른 바인딩을 도입하지 않는 한, 스코프는 정의하는 블록 안에 포함된 모든 블록으로 확장돼요.
이름이 코드 블록에서 사용될 때, 가장 가까운 바깥 스코프를 사용해 해석돼요. 코드 블록에 보이는 이런 모든 스코프의 집합을 그 블록의 *환경(environment)*이라고 해요.
이름이 전혀 발견되지 않으면 NameError 예외가 발생해요. 현재 스코프가 함수 스코프이고, 그 이름이 이름이 사용되는 시점에 아직 값에 바인딩되지 않은 지역 변수를 가리키면 UnboundLocalError 예외가 발생해요. UnboundLocalError는 NameError의 하위 클래스예요.
이름 바인딩 연산이 코드 블록 어디에서든 발생하면, 블록 안에서 그 이름의 모든 사용은 현재 블록에 대한 참조로 취급돼요. 이는 이름이 바인딩되기 전에 블록 안에서 사용될 때 오류를 일으킬 수 있어요. 이 규칙은 미묘해요. Python에는 선언이 없고 이름 바인딩 연산이 코드 블록 어디에서든 발생할 수 있어요. 코드 블록의 지역 변수는 이름 바인딩 연산을 위해 블록의 전체 텍스트를 스캔함으로써 결정할 수 있어요. 예시는 UnboundLocalError에 대한 FAQ 항목을 참고하세요.
global 문이 블록 안에서 발생하면, 그 문에 지정된 이름들의 모든 사용은 최상위 네임스페이스에서 그 이름들의 바인딩을 가리켜요. 이름은 최상위 네임스페이스에서 전역 네임스페이스(즉, 코드 블록을 포함하는 모듈의 네임스페이스)와 builtins 네임스페이스(모듈 builtins의 네임스페이스)를 검색해 해석돼요. 전역 네임스페이스가 먼저 검색돼요. 이름이 거기 없으면 builtins 네임스페이스가 다음으로 검색돼요. 이름이 builtins 네임스페이스에도 없으면 전역 네임스페이스에 새 변수가 생성돼요. global 문은 나열된 이름의 모든 사용보다 앞서 있어야 해요.
global 문은 같은 블록의 이름 바인딩 연산과 동일한 스코프를 가져요. 자유 변수에 대한 가장 가까운 바깥 스코프가 global 문을 포함하면, 그 자유 변수는 전역으로 취급돼요.
nonlocal 문은 해당 이름들이 가장 가까운 바깥 함수 스코프에서 이전에 바인딩된 변수를 가리키게 해요. 주어진 이름이 어떤 바깥 함수 스코프에도 존재하지 않으면 컴파일 시점에 SyntaxError가 발생해요. 타입 매개변수는 nonlocal 문으로 다시 바인딩할 수 없어요.
모듈의 네임스페이스는 모듈이 처음 import될 때 자동으로 생성돼요. 스크립트의 메인 모듈은 항상 __main__이라고 불러요.
클래스 정의 블록과 exec() 및 eval()의 인자는 이름 해석 맥락에서 특별해요. 클래스 정의는 이름을 사용하고 정의할 수 있는 실행 가능한 문장이에요. 이러한 참조는 이름 해석의 일반 규칙을 따르지만, 바인딩되지 않은 지역 변수가 전역 네임스페이스에서 조회된다는 예외가 있어요. 클래스 정의의 네임스페이스는 그 클래스의 속성 사전이 돼요. 클래스 블록에 정의된 이름의 스코프는 클래스 블록으로 제한돼요. 메서드의 코드 블록으로는 확장되지 않아요. 여기에는 컴프리헨션과 제너레이터 표현식도 포함되지만, 자신의 바깥 클래스 스코프에 접근할 수 있는 어노테이션 스코프는 포함되지 않아요. 이는 다음이 실패한다는 뜻이에요:
class A:
a = 42
b = list(a + i for i in range(10))
그러나 다음은 성공해요:
class A:
type Alias = Nested
class Nested: pass
print(A.Alias.__value__) # <type 'A.Nested'>
4.2.3. 어노테이션 스코프 (Annotation scopes)
어노테이션, 타입 매개변수 목록, type 문은 어노테이션 스코프를 도입해요. 이 스코프는 함수 스코프와 대부분 비슷하게 동작하지만, 아래에서 논의할 몇 가지 예외가 있어요.
어노테이션 스코프는 다음 맥락에서 사용돼요:
- 함수 어노테이션.
- 변수 어노테이션.
- 제네릭 타입 별칭을 위한 타입 매개변수 목록.
- 제네릭 함수를 위한 타입 매개변수 목록. 제네릭 함수의 어노테이션은 어노테이션 스코프 안에서 실행되지만, 기본값과 데코레이터는 그렇지 않아요.
- 제네릭 클래스를 위한 타입 매개변수 목록. 제네릭 클래스의 기반 클래스와 키워드 인자는 어노테이션 스코프 안에서 실행되지만, 데코레이터는 그렇지 않아요.
- 타입 매개변수의 경계(bound), 제약(constraint), 기본값 (지연 평가됨).
- 타입 별칭의 값 (지연 평가됨).
어노테이션 스코프는 함수 스코프와 다음 방식으로 달라요:
- 어노테이션 스코프는 자신의 바깥 클래스 네임스페이스에 접근할 수 있어요. 어노테이션 스코프가 클래스 스코프 바로 안에 있거나, 클래스 스코프 바로 안에 있는 다른 어노테이션 스코프 안에 있으면, 어노테이션 스코프의 코드는 클래스 본문 안에서 직접 실행되는 것처럼 클래스 스코프에 정의된 이름을 사용할 수 있어요. 이는 클래스 안에서 정의된, 클래스 스코프에 정의된 이름에 접근할 수 없는 일반 함수와 대조돼요.
- 어노테이션 스코프의 표현식은
yield,yield from,await,:=표현식을 포함할 수 없어요. (이 표현식들은 어노테이션 스코프 안에 포함된 다른 스코프에서는 허용돼요.) - 어노테이션 스코프에 정의된 이름은 내부 스코프의
nonlocal문으로 다시 바인딩될 수 없어요. 어노테이션 스코프 안에 나타날 수 있는 다른 구문 요소가 새 이름을 도입할 수 없으므로, 여기에는 타입 매개변수만 포함돼요. - 어노테이션 스코프에는 내부 이름이 있지만, 그 이름은 스코프 안에 정의된 객체의 정규화된 이름(qualified name)에 반영되지 않아요. 대신 그러한 객체의
__qualname__은 그 객체가 바깥 스코프에 정의된 것처럼 돼요.
버전 3.12에서 추가: 어노테이션 스코프는 PEP 695의 일부로 Python 3.12에서 도입되었어요.
버전 3.13에서 변경: 타입 매개변수 기본값에도 PEP 696이 도입한 대로 어노테이션 스코프가 사용돼요.
버전 3.14에서 변경: PEP 649와 PEP 749가 지정한 대로 어노테이션에도 어노테이션 스코프가 이제 사용돼요.
4.2.4. 지연 평가 (Lazy evaluation)
대부분의 어노테이션 스코프는 *지연 평가(lazily evaluated)*돼요. 여기에는 어노테이션, type 문을 통해 생성된 타입 별칭의 값, 타입 매개변수 문법을 통해 생성된 타입 변수의 경계·제약·기본값이 포함돼요. 즉, 이것들은 타입 별칭이나 타입 변수가 생성될 때나 어노테이션을 담은 객체가 생성될 때 평가되지 않아요. 대신 필요할 때만 평가돼요. 예를 들어 타입 별칭의 __value__ 속성에 접근할 때요.
예시:
>>> type Alias = 1/0
>>> Alias.__value__
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
>>> def func[T: 1/0](): pass
>>> T = func.__type_params__[0]
>>> T.__bound__
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
여기서 예외는 타입 별칭의 __value__ 속성이나 타입 변수의 __bound__ 속성에 접근할 때만 발생해요.
이 동작은 주로 타입 별칭이나 타입 변수가 생성될 때 아직 정의되지 않은 타입을 참조하는 데 유용해요. 예를 들어 지연 평가는 상호 재귀적인 타입 별칭의 생성을 가능하게 해요:
from typing import Literal
type SimpleExpr = int | Parenthesized
type Parenthesized = tuple[Literal["("], Expr, Literal[")"]]
type Expr = SimpleExpr | tuple[SimpleExpr, Literal["+", "-"], Expr]
지연 평가된 값은 어노테이션 스코프에서 평가돼요. 즉, 지연 평가된 값 안에 나타나는 이름은 바로 바깥 스코프에서 사용된 것처럼 조회돼요.
버전 3.12에서 추가.
4.2.5. 내장 이름과 제한된 실행 (Builtins and restricted execution)
CPython 구현 세부 사항: 사용자는 __builtins__를 건드리면 안 돼요. 이것은 엄격히 구현 세부 사항이에요. builtins 네임스페이스의 값을 재정의하려는 사용자는 builtins 모듈을 import하고 그 속성을 적절히 수정해야 해요.
코드 블록의 실행과 연관된 builtins 네임스페이스는 실제로 그 전역 네임스페이스에서 이름 __builtins__를 조회함으로써 발견돼요. 이것은 사전이거나 모듈이어야 해요 (후자의 경우 모듈의 사전이 사용돼요). 기본적으로, __main__ 모듈에 있을 때 __builtins__는 내장 모듈 builtins이에요. 다른 모듈에 있을 때 __builtins__는 builtins 모듈 자체의 사전에 대한 별칭이에요.
4.2.6. 동적 기능과의 상호작용 (Interaction with dynamic features)
자유 변수의 이름 해석은 컴파일 시점이 아니라 런타임에 일어나요. 이는 다음 코드가 42를 출력한다는 뜻이에요:
i = 10
def f():
print(i)
i = 42
f()
eval()과 exec() 함수는 이름을 해석하기 위해 전체 환경에 접근하지 못해요. 이름은 호출자의 지역 및 전역 네임스페이스에서 해석될 수 있어요. 자유 변수는 가장 가까운 바깥 네임스페이스가 아니라 전역 네임스페이스에서 해석돼요. [1] exec()와 eval() 함수에는 전역 및 지역 네임스페이스를 재정의하는 선택적 인자가 있어요. 네임스페이스가 하나만 지정되면 둘 다에 사용돼요.
4.3. 예외 (Exceptions)
예외는 오류나 다른 예외적인 조건을 처리하기 위해 코드 블록의 정상적인 제어 흐름에서 벗어나는 수단이에요. 예외는 오류가 감지된 지점에서 *발생(raise)*돼요. 주변 코드 블록이나 오류가 발생한 코드 블록을 직접 또는 간접적으로 호출한 어떤 코드 블록에 의해 *처리(handle)*될 수 있어요.
Python 인터프리터는 런타임 오류(예: 0으로 나누기)를 감지하면 예외를 발생시켜요. Python 프로그램도 raise 문으로 명시적으로 예외를 발생시킬 수 있어요. 예외 처리기는 try … except 문으로 지정돼요. 그러한 문의 finally 절은 예외를 처리하지 않지만 앞선 코드에서 예외가 발생했든 아니든 실행되는 정리(cleanup) 코드를 지정하는 데 사용할 수 있어요.
Python은 오류 처리의 "종료(termination)" 모델을 사용해요: 예외 처리기는 무슨 일이 일어났는지 알아내고 바깥 레벨에서 실행을 계속할 수 있지만, 오류의 원인을 고치고 실패한 연산을 재시도할 수는 없어요 (잘못된 코드 조각을 위에서 다시 진입하는 것 외에는요).
예외가 전혀 처리되지 않으면, 인터프리터는 프로그램의 실행을 종료하거나 대화형 메인 루프로 돌아가요. 어느 경우든 스택 추적(traceback)을 출력해요. 예외가 SystemExit인 경우는 제외하고요.
예외는 클래스 인스턴스로 식별돼요. except 절은 그 인스턴스의 클래스에 따라 선택돼요. 그 인스턴스의 클래스나 그 클래스의 비가상 기반 클래스를 참조해야 해요. 인스턴스는 처리기가 받을 수 있고, 예외적인 조건에 대한 추가 정보를 전달할 수 있어요.
참고: 예외 메시지는 Python API의 일부가 아니에요. 그 내용은 경고 없이 Python 버전마다 바뀔 수 있으며, 여러 버전의 인터프리터에서 실행될 코드가 의존하면 안 돼요.
try 문의 설명은 The try statement 섹션, raise 문의 설명은 The raise statement 섹션도 참고하세요.
4.4. 런타임 구성 요소 (Runtime Components)
4.4.1. 일반 컴퓨팅 모델 (General Computing Model)
Python의 실행 모델은 진공에서 동작하지 않아요. 그것은 호스트 머신에서, 그리고 그 호스트의 런타임 환경(운영 체제(OS)가 있다면 그것을 포함)을 통해 실행돼요. 프로그램이 실행될 때, 호스트에서 실행되는 방식의 개념적 계층은 대략 다음과 같아요:
host machine (호스트 머신)
process (global resources — 전역 자원)
thread (runs machine code — 머신 코드 실행)
각 프로세스는 호스트에서 실행되는 프로그램을 나타내요. 각 프로세스 자체를 그 프로그램의 데이터 부분이라고 생각하세요. 프로세스의 스레드를 프로그램의 실행 부분이라고 생각하세요. 이 구분은 개념적 Python 런타임을 이해하는 데 중요해요.
데이터 부분으로서의 프로세스는 프로그램이 실행되는 실행 맥락이에요. 그것은 주로 호스트가 프로그램에 할당한 자원의 집합으로 구성돼요. 여기에는 메모리, 시그널, 파일 핸들, 소켓, 환경 변수가 포함돼요.
프로세스들은 서로 격리되고 독립적이에요. (호스트도 마찬가지예요.) 호스트는 프로세스 간의 조정 외에도 프로세스가 할당된 자원에 접근하는 것을 관리해요.
각 스레드는 프로그램의 머신 코드의 실제 실행을 나타내며, 프로그램의 프로세스에 할당된 자원에 상대적으로 실행돼요. 그 실행이 어떻게 그리고 언제 일어나는지는 전적으로 호스트의 몫이에요.
Python의 관점에서 프로그램은 항상 정확히 하나의 스레드로 시작해요. 그러나 프로그램은 여러 동시 스레드에서 실행되도록 성장할 수 있어요. 모든 호스트가 프로세스당 여러 스레드를 지원하는 것은 아니지만, 대부분은 지원해요. 프로세스와 달리 프로세스 안의 스레드는 서로 격리되고 독립적이지 않아요. 구체적으로, 프로세스 안의 모든 스레드는 프로세스의 모든 자원을 공유해요.
스레드의 근본적인 요점은 각각이 다른 것들과 동시에 독립적으로 실행된다는 것이에요. 그것은 개념적으로만 동시("concurrently")일 수도 있고 물리적으로("in parallel")일 수도 있어요. 어느 쪽이든 스레드는 동기화되지 않은 속도로 효과적으로 실행돼요.
참고: 그 비동기화된 속도는 주어진 스레드에서 실행되는 코드에 대해 프로세스의 어떤 메모리도 일관되게 유지된다는 보장이 없다는 뜻이에요. 따라서 다중 스레드 프로그램은 의도적으로 공유하는 자원에 대한 접근을 조정하도록 주의해야 해요. 마찬가지로 여러 스레드에서 다른 자원에 접근하지 않도록 절대적으로 신중해야 해요. 그렇지 않으면 동시에 실행되는 두 스레드가 어떤 공유 데이터의 사용에 우연히 간섭할 수 있어요. 이 모든 것은 Python 프로그램과 Python 런타임 모두에 해당돼요.
이 광범위하고 구조화되지 않은 요구의 비용은 스레드가 제공하는 종류의 원시적 동시성과의 트레이드오프예요. 요구되는 규율의 대안은 일반적으로 비결정적 버그와 데이터 손상을 다루는 것을 의미해요.
4.4.2. Python 런타임 모델 (Python Runtime Model)
같은 개념적 계층이 각 Python 프로그램에 적용되며, Python에 특정한 몇 가지 추가 데이터 계층이 있어요:
host machine (호스트 머신)
process (global resources — 전역 자원)
Python global runtime (state — Python 전역 런타임, 상태)
Python interpreter (state — Python 인터프리터, 상태)
thread (runs Python bytecode and "C-API" — Python 바이트코드와 "C-API" 실행)
Python thread state (Python 스레드 상태)
개념적 수준에서: Python 프로그램이 시작되면 정확히 그 다이어그램처럼 보이는데, 각각 하나씩 있어요. 런타임은 여러 인터프리터를 포함하도록 성장할 수 있고, 각 인터프리터는 여러 스레드 상태를 포함하도록 성장할 수 있어요.
참고: Python 구현이 반드시 런타임 계층을 뚜렷하게, 심지어 구체적으로 구현하지는 않아요. 유일한 예외는 threading 모듈을 통해서처럼 뚜렷한 계층이 직접 지정되거나 사용자에게 노출되는 곳이에요.
참고: 최초의 인터프리터는 일반적으로 "메인" 인터프리터라고 불러요. CPython 같은 일부 Python 구현은 메인 인터프리터에 특별한 역할을 할당해요.
마찬가지로 런타임이 초기화된 호스트 스레드를 "메인" 스레드라고 해요. 그것은 프로세스의 초기 스레드와 다를 수 있지만, 보통은 같아요. 어떤 경우에 "메인 스레드"는 더 구체적일 수 있고 초기 스레드 상태를 가리킬 수도 있어요. Python 런타임은 시그널 처리 같은 특정 책임을 메인 스레드에 할당할 수 있어요.
전체로서 Python 런타임은 전역 런타임 상태, 인터프리터, 스레드 상태로 구성돼요. 런타임은 그 모든 상태가 수명 동안 일관되게 유지되도록 보장해요. 특히 여러 호스트 스레드와 함께 사용될 때요.
개념적 수준에서 전역 런타임은 단지 인터프리터들의 집합이에요. 그 인터프리터들은 그 외에는 서로 격리되고 독립적이지만, 일부 데이터나 다른 자원을 공유할 수 있어요. 런타임은 이런 전역 자원을 안전하게 관리할 책임이 있어요. 이 자원들의 실제 성격과 관리는 구현별이에요. 궁극적으로 전역 런타임의 외부적 유용성은 인터프리터를 관리하는 것으로 제한돼요.
대조적으로 "인터프리터"는 개념적으로 우리가 보통 (완전한 기능을 갖춘) "Python 런타임"이라고 생각하는 것이에요. 호스트 스레드에서 실행되는 머신 코드가 Python 런타임과 상호작용할 때, 특정 인터프리터의 맥락에서 Python을 호출해요.
참고: 여기의 "인터프리터"라는 용어는 스레드에서 정기적으로 실행되며 컴파일된 Python 코드를 실행하는 "바이트코드 인터프리터"와 같지 않아요.
이상적인 세계에서 "Python 런타임"은 우리가 현재 "인터프리터"라고 부르는 것을 가리킬 거예요. 그러나 적어도 1997년(CPython:a027efa5b)에 도입된 이래로 "인터프리터"라고 불려 왔어요.
각 인터프리터는 Python 런타임이 작동하는 데 필요한 프로세스-전역이 아닌 모든 상태와 스레드-특정이 아닌 모든 상태를 완전히 캡슐화해요. 특히 인터프리터의 상태는 사용 사이에 지속돼요. sys.modules 같은 기본 데이터를 포함해요. 런타임은 같은 인터프리터를 사용하는 여러 스레드가 그 사이에서 안전하게 공유하도록 보장해요.
Python 구현은 같은 프로세스에서 동시에 여러 인터프리터를 사용하는 것을 지원할 수 있어요. 그것들은 서로 독립적이고 격리돼 있어요. 예를 들어 각 인터프리터는 자신만의 sys.modules를 가져요.
스레드별 런타임 상태를 위해, 각 인터프리터는 전역 런타임이 인터프리터 집합을 포함하는 것과 같은 방식으로 관리하는 스레드 상태 집합을 가져요. 필요한 만큼 많은 호스트 스레드에 대한 스레드 상태를 가질 수 있어요. 심지어 같은 호스트 스레드에 대한 여러 스레드 상태를 가질 수도 있는데, 그건 흔하지 않지만요.
개념적으로 각 스레드 상태는 인터프리터가 하나의 호스트 스레드에서 작동하는 데 필요한 모든 스레드별 런타임 데이터를 가져요. 스레드 상태는 현재 발생된 예외와 그 스레드의 Python 호출 스택을 포함해요. 다른 스레드별 자원을 포함할 수도 있어요.
참고: "Python 스레드"라는 용어가 가끔 스레드 상태를 가리킬 수 있지만, 보통은 threading 모듈을 사용해 생성된 스레드를 의미해요.
각 스레드 상태는 수명 동안 항상 정확히 하나의 인터프리터와 정확히 하나의 호스트 스레드에 묶여 있어요. 그것은 그 스레드와 그 인터프리터에서만 사용돼요.
여러 스레드 상태가 같은 호스트 스레드에 묶일 수 있어요. 서로 다른 인터프리터를 위해서든 심지어 같은 인터프리터를 위해서든요. 그러나 주어진 호스트 스레드에 대해, 그에 묶인 스레드 상태 중 오직 하나만 그 스레드가 한 번에 사용할 수 있어요.
스레드 상태는 서로 격리되고 독립적이며, 아마도 인터프리터를 공유하고 그 인터프리터에 속한 객체나 다른 자원을 공유하는 것을 제외하고는 어떤 데이터도 공유하지 않아요.
프로그램이 실행된 후에는 threading 모듈을 사용해(스레드를 지원하는 플랫폼과 Python 구현에서) 새 Python 스레드를 만들 수 있어요. os, subprocess, multiprocessing 모듈을 사용해 추가 프로세스를 만들 수 있어요. interpreters 모듈로 인터프리터를 만들고 사용할 수 있어요. 코루틴(async)은 각 인터프리터에서 asyncio를 사용해 실행할 수 있으며, 보통 단일 스레드(종종 메인 스레드)에서 실행돼요.
각주
[1] 이 제한은 이런 연산이 실행하는 코드가 모듈이 컴파일될 때 사용 가능하지 않기 때문에 발생해요.