PEP 567 – 컨텍스트 변수
- Author:
- Yury Selivanov <yury at edgedb.com>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 12-Dec-2017
- Python-Version:
- 3.7
- Post-History:
- 12-Dec-2017, 28-Dec-2017, 16-Jan-2018
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 컨텍스트 변수를 지원하기 위한 새로운 contextvars 모듈과 새로운 CPython C API 집합을 제안합니다. 이 개념은 스레드 로컬 저장소(TLS)와 유사하지만, TLS와 달리 비동기 작업별로 값을 올바르게 추적할 수도 있습니다. 예를 들어 asyncio.Task가 있습니다.
이 제안은 PEP 550의 단순화된 버전입니다. 핵심적인 차이점은 이 PEP가 제너레이터가 아니라 비동기 작업의 경우만 해결하는 데 중점을 둔다는 점입니다. 내장 타입이나 인터프리터를 수정하는 제안은 없습니다.
이 제안은 Python 컨텍스트 관리자와 엄밀히 관련된 것은 아닙니다. 다만 컨텍스트 관리자가 상태를 저장하는 데 사용할 수 있는 메커니즘을 제공하기는 합니다.
API 설계 및 구현 개정
Python 3.7.1에서 모든 컨텍스트 변수 C API의 시그니처는 PyContext *, PyContextVar *, PyContextToken * 대신 PyObject * 포인터를 사용하도록 변경되었습니다. 예를 들면 다음과 같습니다.:
// in 3.7.0:
PyContext *PyContext_New(void);
// in 3.7.1+:
PyObject *PyContext_New(void);
자세한 내용은 [6]에서 확인하십시오. 이 PEP의 C API 절은 변경 사항을 반영하도록 업데이트되었습니다.
근거
스레드 로컬 변수는 동일한 OS 스레드에서 동시에 실행되는 비동기 작업에는 충분하지 않습니다. threading.local()을 사용하여 컨텍스트 값을 저장하고 복원하는 컨텍스트 관리자는 async/await 코드에서 사용될 때 컨텍스트 값이 예기치 않게 다른 코드로 새어 나가게 합니다.
비동기 코드에서 제대로 작동하는 컨텍스트 로컬 저장소가 바람직한 몇 가지 예는 다음과 같습니다.
decimal컨텍스트 및numpy.errstate와 같은 컨텍스트 관리자입니다.- 웹 애플리케이션의 보안 토큰과 요청 데이터 같은 요청 관련 데이터,
gettext의 언어 컨텍스트 등입니다. - 대규모 코드베이스에서의 프로파일링, 추적 및 로깅입니다.
소개
이 PEP는 컨텍스트 변수를 관리하기 위한 새로운 메커니즘을 제안합니다. 이 메커니즘과 관련된 핵심 클래스는 contextvars.Context와 contextvars.ContextVar입니다. 이 PEP는 비동기 작업에서 이 메커니즘을 사용하기 위한 몇 가지 정책도 제안합니다.
컨텍스트 변수에 액세스하기 위해 제안된 메커니즘은 ContextVar 클래스를 사용합니다. 새로운 메커니즘을 사용하려는 모듈(예: decimal)은 다음을 수행해야 합니다.
- 키로 사용할
ContextVar를 보유하는 모듈 전역 변수를 선언하십시오. - 키 변수의
get()메서드를 통해 현재 값에 액세스하십시오. - 키 변수의
set()메서드를 통해 현재 값을 수정하십시오.
“현재 값”이라는 개념은 특별히 고려할 가치가 있습니다. 존재하며 동시에 실행되는 서로 다른 비동기 작업은 동일한 키에 대해 서로 다른 값을 가질 수 있습니다. 이 개념은 스레드 로컬 저장소에서 잘 알려져 있지만, 이 경우 값의 국소성이 반드시 스레드에 결부되는 것은 아닙니다. 대신 스레드 로컬 저장소에 저장되는 “현재 Context”라는 개념이 있습니다. 현재 컨텍스트의 조작은 작업 프레임워크(예: asyncio)의 책임입니다.
Context는 ContextVar 객체에서 해당 객체의 값으로의 매핑입니다. Context 자체는 abc.Mapping 인터페이스를 노출하지만(abc.MutableMapping은 아닙니다!), 직접 수정할 수는 없습니다. Context 객체에서 컨텍스트 변수에 새로운 값을 설정하려면 사용자는 다음을 수행해야 합니다.
Context.run()메서드를 사용하여Context객체를 “현재” 상태로 만드십시오.ContextVar.set()을 사용하여 컨텍스트 변수에 새로운 값을 설정하십시오.
ContextVar.get() 메서드는 self를 키로 사용하여 현재 Context 객체에서 변수를 찾습니다.
현재 Context 객체에 대한 직접 참조를 얻을 수는 없지만, contextvars.copy_context() 함수를 사용하여 얕은 복사본을 얻을 수는 있습니다. 이를 통해 Context.run()의 caller가 자신의 Context 객체를 유일하게 소유하도록 보장합니다.
사양
다음 API를 포함하는 새로운 표준 라이브러리 모듈 contextvars가 추가됩니다.
copy_context() -> Context함수는 현재 OS 스레드의 현재Context객체 복사본을 가져오는 데 사용됩니다.- 컨텍스트 변수를 선언하고 액세스하기 위한
ContextVar클래스입니다. Context클래스는 컨텍스트 상태를 캡슐화합니다. 모든 OS 스레드는 자신의 현재Context인스턴스에 대한 참조를 저장합니다. 해당 참조를 직접 제어할 수는 없습니다. 대신Context.run(callable, *args, **kwargs)메서드를 사용하여 다른 컨텍스트에서 Python 코드를 실행합니다.
contextvars.ContextVar
ContextVar 클래스는 다음 생성자 시그니처를 가집니다: ContextVar(name, *, default=_NO_DEFAULT). name 매개변수는 인트로스펙션 및 디버깅 목적으로 사용되며, 읽기 전용 ContextVar.name 속성으로 노출됩니다. default 매개변수는 선택 사항입니다. 예:
# Declare a context variable 'var' with the default value 42.
var = ContextVar('var', default=42)
(_NO_DEFAULT는 기본값이 제공되었는지 감지하는 데 사용되는 내부 센티널 객체입니다.)
ContextVar.get(default=_NO_DEFAULT)는 현재 Context에 대한 컨텍스트 변수의 값을 반환합니다.:
# Get the value of `var`.
var.get()
현재 컨텍스트에 변수의 값이 없으면 ContextVar.get()은 다음을 수행합니다.
- 제공된 경우
get()메서드의 default인자의 값을 반환합니다. - 제공된 경우 컨텍스트 변수의 기본값을 반환합니다.
LookupError를 발생시킵니다.
ContextVar.set(value) -> Token은 현재 Context에서 컨텍스트 변수에 새로운 값을 설정하는 데 사용됩니다.:
# Set the variable 'var' to 1 in the current context.
var.set(1)
ContextVar.reset(token)은 현재 컨텍스트의 변수를 token을 생성한 set() 연산 이전의 값으로 재설정하는 데 사용합니다(설정되지 않았으면 변수를 제거합니다).:
# Assume: var.get(None) is None
# Set 'var' to 1:
token = var.set(1)
try:
# var.get() == 1
finally:
var.reset(token)
# After reset: var.get(None) is None,
# i.e. 'var' was removed from the current context.
ContextVar.reset() 메서드는 다음과 같은 경우 예외를 발생시킵니다:
- 다른 변수가 생성한 토큰 객체와 함께 호출된 경우
ValueError를 발생시킵니다; - 현재
Context객체가 토큰 객체가 생성된 컨텍스트와 일치하지 않는 경우ValueError를 발생시킵니다; - 토큰 객체가 이미 변수를 재설정하는 데 한 번 사용된 경우
RuntimeError를 발생시킵니다.
contextvars.Token
contextvars.Token은 ContextVar를 이전 값으로 복원하거나, 변수가 이전에 설정되지 않았다면 컨텍스트에서 제거하는 데 사용해야 하는 불투명 객체입니다. 이 객체는 ContextVar.set()을 호출하는 경우에만 생성할 수 있습니다.
디버깅 및 인트로스펙션 목적으로 다음을 제공합니다:
- 토큰을 생성한 변수를 가리키는 읽기 전용 속성
Token.var를 제공합니다; set()호출 이전에 변수가 가지고 있던 값으로 설정되는 읽기 전용 속성Token.old_value를 제공하며, 변수가 이전에 설정되지 않았다면Token.MISSING으로 설정됩니다.
contextvars.Context
Context객체는 컨텍스트 변수와 값의 매핑입니다.
Context()는 빈 컨텍스트를 생성합니다. 현재 OS 스레드의 현재 Context 사본을 가져오려면 contextvars.copy_context() 메서드를 사용하십시오.:
ctx = contextvars.copy_context()
일부 Context에서 Python 코드를 실행하려면 Context.run() 메서드를 사용하십시오.:
ctx.run(function)
function이 유발하는 모든 컨텍스트 변수의 변경 사항은 ctx 컨텍스트에 포함됩니다.:
var = ContextVar('var')
var.set('spam')
def main():
# 'var' was set to 'spam' before
# calling 'copy_context()' and 'ctx.run(main)', so:
# var.get() == ctx[var] == 'spam'
var.set('ham')
# Now, after setting 'var' to 'ham':
# var.get() == ctx[var] == 'ham'
ctx = copy_context()
# Any changes that the 'main' function makes to 'var'
# will be contained in 'ctx'.
ctx.run(main)
# The 'main()' function was run in the 'ctx' context,
# so changes to 'var' are contained in it:
# ctx[var] == 'ham'
# However, outside of 'ctx', 'var' is still set to 'spam':
# var.get() == 'spam'
Context.run()은 둘 이상의 OS 스레드에서 동일한 컨텍스트 객체에 대해 호출되거나 재귀적으로 호출되면 RuntimeError를 발생시킵니다.
Context.copy()는 컨텍스트 객체의 얕은 사본을 반환합니다.
Context객체는 collections.abc.Mapping ABC를 구현합니다. 이를 사용하여 컨텍스트를 인트로스펙션할 수 있습니다.:
ctx = contextvars.copy_context()
# Print all context variables and their values in 'ctx':
print(ctx.items())
# Print the value of 'some_variable' in context 'ctx':
print(ctx[some_variable])
Context.__getitem__ 및 Context.get을 포함한 모든 매핑 메서드는 컨텍스트 변수의 기본값(즉, ContextVar.default)을 무시한다는 점에 유의하십시오. 이는 기본값으로 생성되었지만 context에 설정되지 않은 변수 var에 대해 다음을 의미합니다:
context[var]는KeyError를 발생시킵니다,var in context는False를 반환합니다,- 해당 변수는
context.items()등에 포함되지 않습니다.
asyncio
asyncio는 함수의 비동기 실행을 예약하기 위해 Loop.call_soon(), Loop.call_later(), Loop.call_at()을 사용합니다. asyncio.Task는 래핑된 코루틴을 실행하기 위해 call_soon()을 사용합니다.
현재 컨텍스트를 기본값으로 하는 새로운 선택적 context 키워드 전용 인자를 허용하도록 Loop.call_{at,later,soon} 및 Future.add_done_callback()을 수정합니다.:
def call_soon(self, callback, *args, context=None):
if context is None:
context = contextvars.copy_context()
# ... some time later
context.run(callback, *args)
asyncio의 태스크는 생성된 시점부터 상속하는 자체 컨텍스트를 유지해야 합니다. asyncio.Task는 다음과 같이 수정됩니다.:
class Task:
def __init__(self, coro):
...
# Get the current context snapshot.
self._context = contextvars.copy_context()
self._loop.call_soon(self._step, context=self._context)
def _step(self, exc=None):
...
# Every advance of the wrapped coroutine is done in
# the task's context.
self._loop.call_soon(self._step, context=self._context)
...
구현
이 절에서는 의사 코드로 고수준 구현 세부 사항을 설명합니다. 이 절을 짧고 명확하게 유지하기 위해 일부 최적화는 생략합니다.
Context매핑은 불변 딕셔너리를 사용하여 구현합니다. 이를 통해 copy_context()함수를 O(1)로 구현할 수 있습니다. 참조 구현에서는 Hash Array Mapped Tries (HAMT)를 사용하여 불변 딕셔너리를 구현합니다. HAMT 성능 분석은 PEP 550을 참조하십시오 [1].
이 절에서는 카피 온 라이트 접근 방식과 내장 dict 타입을 사용하여 불변 딕셔너리를 구현합니다.:
class _ContextData:
def __init__(self):
self._mapping = dict()
def __getitem__(self, key):
return self._mapping[key]
def __contains__(self, key):
return key in self._mapping
def __len__(self):
return len(self._mapping)
def __iter__(self):
return iter(self._mapping)
def set(self, key, value):
copy = _ContextData()
copy._mapping = self._mapping.copy()
copy._mapping[key] = value
return copy
def delete(self, key):
copy = _ContextData()
copy._mapping = self._mapping.copy()
del copy._mapping[key]
return copy
모든 OS 스레드는 현재 Context객체에 대한 참조를 가집니다.:
class PyThreadState:
context: Context
contextvars.Context는 _ContextData를 감싸는 래퍼입니다.:
class Context(collections.abc.Mapping):
_data: _ContextData
_prev_context: Optional[Context]
def __init__(self):
self._data = _ContextData()
self._prev_context = None
def run(self, callable, *args, **kwargs):
if self._prev_context is not None:
raise RuntimeError(
f'cannot enter context: {self} is already entered')
ts: PyThreadState = PyThreadState_Get()
self._prev_context = ts.context
try:
ts.context = self
return callable(*args, **kwargs)
finally:
ts.context = self._prev_context
self._prev_context = None
def copy(self):
new = Context()
new._data = self._data
return new
# Implement abstract Mapping.__getitem__
def __getitem__(self, var):
return self._data[var]
# Implement abstract Mapping.__contains__
def __contains__(self, var):
return var in self._data
# Implement abstract Mapping.__len__
def __len__(self):
return len(self._data)
# Implement abstract Mapping.__iter__
def __iter__(self):
return iter(self._data)
# The rest of the Mapping methods are implemented
# by collections.abc.Mapping.
contextvars.copy_context()는 다음과 같이 구현합니다.:
def copy_context():
ts: PyThreadState = PyThreadState_Get()
return ts.context.copy()
contextvars.ContextVar는 PyThreadState.context와 직접 상호 작용합니다.:
class ContextVar:
def __init__(self, name, *, default=_NO_DEFAULT):
self._name = name
self._default = default
@property
def name(self):
return self._name
def get(self, default=_NO_DEFAULT):
ts: PyThreadState = PyThreadState_Get()
try:
return ts.context[self]
except KeyError:
pass
if default is not _NO_DEFAULT:
return default
if self._default is not _NO_DEFAULT:
return self._default
raise LookupError
def set(self, value):
ts: PyThreadState = PyThreadState_Get()
data: _ContextData = ts.context._data
try:
old_value = data[self]
except KeyError:
old_value = Token.MISSING
updated_data = data.set(self, value)
ts.context._data = updated_data
return Token(ts.context, self, old_value)
def reset(self, token):
if token._used:
raise RuntimeError("Token has already been used once")
if token._var is not self:
raise ValueError(
"Token was created by a different ContextVar")
ts: PyThreadState = PyThreadState_Get()
if token._context is not ts.context:
raise ValueError(
"Token was created in a different Context")
if token._old_value is Token.MISSING:
ts.context._data = ts.context._data.delete(token._var)
else:
ts.context._data = ts.context._data.set(token._var,
token._old_value)
token._used = True
참고로 참조 구현에서 ContextVar.get()은 가장 최근 값에 대한 내부 캐시를 가지므로 해시 조회를 건너뛸 수 있습니다. 이는 decimal모듈이 PyThreadState_GetDict()에서 해당 컨텍스트를 가져오기 위해 구현하는 최적화와 유사합니다. 캐시 구현을 매우 자세히 설명하는 PEP 550을 참조하십시오.
Token클래스는 다음과 같이 구현합니다.:
class Token:
MISSING = object()
def __init__(self, context, var, old_value):
self._context = context
self._var = var
self._old_value = old_value
self._used = False
@property
def var(self):
return self._var
@property
def old_value(self):
return self._old_value
새로운 API 요약
Python API
ContextVar,Context,Token클래스를 제공하는 새로운contextvars모듈과copy_context()함수입니다.asyncio.Loop.call_at(),asyncio.Loop.call_later(),asyncio.Loop.call_soon(),asyncio.Future.add_done_callback()은 호출된 컨텍스트에서 콜백 함수를 실행합니다. 사용자 지정 컨텍스트를 지정하기 위해 새로운 context 키워드 전용 매개변수를 사용할 수 있습니다.asyncio.Task는 자체 컨텍스트를 유지하도록 내부적으로 수정합니다.
C API
PyObject * PyContextVar_New(char *name, PyObject *default):ContextVar객체를 생성합니다. default 인자는NULL일 수 있으며, 이는 변수에 기본값이 없음을 의미합니다.int PyContextVar_Get(PyObject *, PyObject *default_value, PyObject **value): 조회 중 오류가 발생하면-1을 반환하고, 그렇지 않으면0을 반환합니다. 컨텍스트 변수의 값을 찾으면 해당 값을value포인터에 설정합니다. 그렇지 않으면value가NULL이 아닐 때default_value로 설정합니다.default_value가NULL이면value를 변수의 기본값으로 설정하며, 이 기본값 역시NULL일 수 있습니다.value는 항상 새로운 참조입니다.PyObject * PyContextVar_Set(PyObject *, PyObject *): 현재 컨텍스트에서 변수의 값을 설정합니다.PyContextVar_Reset(PyObject *, PyObject *): 컨텍스트 변수의 값을 재설정합니다.PyObject * PyContext_New(): 새로운 빈 컨텍스트를 생성합니다.PyObject * PyContext_Copy(PyObject *): 전달된 컨텍스트 객체의 얕은 복사본을 반환합니다.PyObject * PyContext_CopyCurrent(): 현재 컨텍스트의 복사본을 가져옵니다.int PyContext_Enter(PyObject *)및int PyContext_Exit(PyObject *)를 사용하면 현재 OS 스레드의 컨텍스트를 설정하고 복원할 수 있습니다. 이전 컨텍스트를 항상 복원해야 합니다.:PyObject *old_ctx = PyContext_Copy(); if (old_ctx == NULL) goto error; if (PyContext_Enter(new_ctx)) goto error; // run some code if (PyContext_Exit(old_ctx)) goto error;
거부된 아이디어
threading.local() 인터페이스 복제
Token을 ContextVar.unset()으로 대체
Token API를 사용하면 ContextVar.unset() 메서드 없이도 처리할 수 있으며, 이 메서드는 PEP 550의 연결된 컨텍스트 설계와 호환되지 않습니다. 제너레이터와 비동기 제너레이터에서 컨텍스트 변수를 지원해야 한다는 수요가 있을 경우 PEP 550과의 향후 호환성이 필요합니다.
Token API는 사용성도 더 뛰어납니다. 사용자가 값의 부재를 별도로 처리할 필요가 없습니다. 다음을 비교하십시오.:
token = cv.set(new_value)
try:
# cv.get() is new_value
finally:
cv.reset(token)
다음과 비교하십시오.:
_deleted = object()
old = cv.get(default=_deleted)
try:
cv.set(blah)
# code
finally:
if old is _deleted:
cv.unset()
else:
cv.set(old)
ContextVar.reset() 대신 Token.reset() 사용
Nathaniel Smith는 ContextVar.reset() 메서드를 Token 클래스에 직접 구현하자고 제안했으므로, 다음 대신:
token = var.set(value)
# ...
var.reset(token)
다음과 같이 작성합니다.:
token = var.set(value)
# ...
token.reset()
Token.reset()을 사용하면 사용자가 다른 변수가 생성한 토큰 객체로 변수를 재설정하려고 시도할 수 없게 됩니다.
이 제안은 ContextVar.reset()이 코드에서 어떤 변수가 재설정되는지를 사람에게 더 명확하게 보여준다는 이유로 거부되었습니다.
Context 객체를 피클링 가능하게 만들기
Antoine Pitrou가 제안한 이 방법을 사용하면 Context 객체를 프로세스 간에 투명하게 사용할 수 있으므로, Offloading execution to other threads 예제가 ProcessPoolExecutor에서도 작동할 수 있습니다.
다음과 같은 이유로 이를 활성화하는 데에는 문제가 있습니다.
ContextVar객체에는__module__및__qualname__속성이 없으므로Context객체를 간단하게 피클링할 수 없습니다. 컨텍스트 변수가 정의된 모듈을 자동으로 감지하도록 API를 수정하거나,ContextVar생성자에 새로운 키워드 전용 “module” 매개변수를 추가하면 이 문제를 해결할 수 있습니다.- 모든 컨텍스트 변수가 피클링 가능한 객체를 참조하는 것은 아닙니다.
ContextVar를 피클링 가능하게 만드는 것은 명시적으로 선택할 수 있어야 합니다.
Python 3.7 릴리스 일정의 시간 제약을 고려하여 이 제안은 Python 3.8로 연기하기로 결정되었습니다.
Context를 MutableMapping으로 만들기
Context 클래스가 abc.MutableMapping 인터페이스를 구현하게 되면 Context[var] = value 및 del Context[var] 연산을 사용하여 변수를 설정하고 설정 해제할 수 있게 됩니다.
다음과 같은 이유로 이 제안은 Python 3.8 이상으로 연기되었습니다.
- Python 3.8에서 제너레이터가 컨텍스트 변수를 지원하도록 결정되면(PEP 550 및 PEP 568 참조), 각 제너레이터가 자체 매핑을 갖게 되므로
Context는 컨텍스트 변수 매핑의 체인 맵으로 변환될 것입니다. 그러면Context.__delitem__과 같은 변경 연산은 체인의 최상위 매핑에만 적용되므로 혼란을 일으킬 수 있습니다. - 컨텍스트를 변경하는 방법을 하나로 통일하면(
ContextVar.set()및ContextVar.reset()메서드) API가 더 간단해집니다.예를 들어 아래 코드 조각이 예상대로 작동하지 않는 이유는 명확하지 않습니다.:
var = ContextVar('var') ctx = copy_context() ctx[var] = 'value' print(ctx[var]) # Prints 'value' print(var.get()) # Raises a LookupError
반면 다음 코드는 작동합니다.:
ctx = copy_context() def func(): ctx[var] = 'value' # Contrary to the previous example, this would work # because 'func()' is running within 'ctx'. print(ctx[var]) print(var.get()) ctx.run(func)
Context가 가변적이라면, 컨텍스트 변수가 해당 컨텍스트 내에서 실행되는 코드와 별도로(또는 동시에) 변경될 수 있음을 의미하게 됩니다. 이는 실행 중인 파이썬 프레임 객체에 대한 참조를 얻어 다른 OS 스레드에서 그f_locals를 수정하는 것과 유사합니다. 컨텍스트 변수에 값을 할당하는 방법을 하나로 통일하면 컨텍스트가 개념적으로 더 단순해지고 예측 가능해지는 동시에, 향후 성능 최적화의 여지도 남겨둘 수 있습니다.
ContextVar의 초기값 갖기
Nathaniel Smith는 ContextVar 생성자에 필수 키워드 전용 인자인 initial_value를 두자고 제안했습니다.
이 제안에 반대하는 주된 논거는, 일부 타입의 경우 None을 제외하면 딱히 합리적인 “초기값”이 존재하지 않는다는 점입니다. 예를 들어 현재 HTTP 요청 객체를 컨텍스트 변수에 저장하는 웹 프레임워크를 생각해 봅시다. 현재의 시맨틱스에서는 기본값 없이 컨텍스트 변수를 생성할 수 있습니다:
# Framework:
current_request: ContextVar[Request] = \
ContextVar('current_request')
# Later, while handling an HTTP request:
request: Request = current_request.get()
# Work with the 'request' object:
return request.method
위 예제에서 request가 None인지 확인할 필요가 없다는 점에 유의하십시오. 프레임워크가 항상 current_request 변수를 설정할 것이라고 단순히 기대되며, 그렇지 않다면 버그입니다(이 경우 current_request.get()은 LookupError를 일으킵니다).
그러나 만약 필수 초기값이 있었다면, None 값에 대해 명시적으로 방어해야 했을 것입니다:
# Framework:
current_request: ContextVar[Optional[Request]] = \
ContextVar('current_request', initial_value=None)
# Later, while handling an HTTP request:
request: Optional[Request] = current_request.get()
# Check if the current request object was set:
if request is None:
raise RuntimeError
# Work with the 'request' object:
return request.method
게다가 컨텍스트 변수를 일반 파이썬 변수 및 threading.local() 객체와 느슨하게 비교할 수 있습니다. 둘 다 조회에 실패하면 오류를 일으킵니다(각각 NameError와 AttributeError).
하위 호환성
이 제안은 100% 하위 호환성을 유지합니다.
컨텍스트 관련 값을 저장하기 위해 threading.local()을 사용하는 라이브러리는 현재 동기 코드에서만 올바르게 동작합니다. 이러한 라이브러리를 제안된 API를 사용하도록 전환하면 동기 코드에 대한 동작은 변경되지 않은 채로 유지되지만, 비동기 코드에 대한 지원이 자동으로 활성화됩니다.
예제
threading.local()을 사용하는 코드 변환하기
threading.local()을 사용하는 전형적인 코드 조각은 보통 다음과 같습니다.:
class PrecisionStorage(threading.local):
# Subclass threading.local to specify a default value.
value = 0.0
precision = PrecisionStorage()
# To set a new precision:
precision.value = 0.5
# To read the current precision:
print(precision.value)
이러한 코드는 contextvars 모듈을 사용하도록 변환할 수 있습니다.:
precision = contextvars.ContextVar('precision', default=0.0)
# To set a new precision:
precision.set(0.5)
# To read the current precision:
print(precision.get())
실행을 다른 스레드로 오프로딩하기
현재 스레드 컨텍스트의 사본을 사용하여 별도의 OS 스레드에서 코드를 실행하는 것이 가능합니다.:
executor = ThreadPoolExecutor()
current_context = contextvars.copy_context()
executor.submit(current_context.run, some_function)
참조 구현
승인
PEP 567은 2018년 1월 22일 월요일에 귀도에 의해 승인되었습니다 [5]. 참조 구현은 같은 날 병합되었습니다.
참고 문헌
감사의 글
이 PEP에 대한 피드백, 아이디어, 편집, 비평, 코드 리뷰, 논의를 해주신 Guido van Rossum, Nathaniel Smith, Victor Stinner, Elvis Pranskevichus, Alyssa Coghlan, Antoine Pitrou, INADA Naoki, Paul Moore, Eric Snow, Greg Ewing, 그리고 그 밖의 많은 분께 감사드립니다.
Copyright
This document has been placed in the public domain.