Вопрос:

Как мне написать непротиворечивые контекстные менеджеры с состоянием?

python

160 просмотра

1 ответ

33674 Репутация автора

РЕДАКТИРОВАТЬ: Как указал Тьерри Lathuille , PEP567 , где он ContextVarбыл представлен, не был предназначен для работы с генераторами (в отличие от изъятого PEP550 ). Тем не менее, главный вопрос остается. Как мне написать контекстные менеджеры с состоянием, которые правильно работают с несколькими потоками, генераторами и asyncioзадачами?


У меня есть библиотека с некоторыми функциями, которые могут работать в разных «режимах», поэтому их поведение может быть изменено локальным контекстом. Я смотрю на contextvarsмодуль, чтобы реализовать его надежно, чтобы я мог использовать его из разных потоков, асинхронных контекстов и т. Д. Однако у меня возникли проблемы с получением простого примера, работающего правильно. Рассмотрим эту минимальную настройку:

from contextlib import contextmanager
from contextvars import ContextVar

MODE = ContextVar('mode', default=0)

@contextmanager
def use_mode(mode):
    t = MODE.set(mode)
    try:
        yield
    finally:
        MODE.reset(t)

def print_mode():
   print(f'Mode {MODE.get()}')

Вот небольшой тест с функцией генератора:

def first():
    print('Start first')
    print_mode()
    with use_mode(1):
        print('In first: with use_mode(1)')
        print('In first: start second')
        it = second()
        next(it)
        print('In first: back from second')
        print_mode()
        print('In first: continue second')
        next(it, None)
        print('In first: finish')

def second():
    print('Start second')
    print_mode()
    with use_mode(2):
        print('In second: with use_mode(2)')
        print('In second: yield')
        yield
        print('In second: continue')
        print_mode()
        print('In second: finish')

first()

Я получаю следующий вывод:

Start first
Mode 0
In first: with use_mode(1)
In first: start second
Start second
Mode 1
In second: with use_mode(2)
In second: yield
In first: back from second
Mode 2
In first: continue second
In second: continue
Mode 2
In second: finish
In first: finish

В разделе:

In first: back from second
Mode 2
In first: continue second

Это должно быть Mode 1вместо Mode 2, потому что это было напечатано там first, где должен быть контекст применения, насколько я понимаю use_mode(1). Тем не менее, кажется , что use_mode(2)из secondуложена над ним , пока генератор не закончит. Генераторы не поддерживаются contextvars? Если да, есть ли способ надежной поддержки контекстных менеджеров с сохранением состояния? Надежно, я имею в виду, это должно вести себя последовательно, использую ли я:

  • Несколько потоков.
  • Генераторы.
  • asyncio
Автор: jdehesa Источник Размещён: 04.12.2018 11:13

Ответы (1)


0 плюса

60376 Репутация автора

да. сложно.

На самом деле у вас есть «взаимосвязанный контекст» - без возврата __exit__части для secondфункции он не восстановит контекст с помощью ContextVars, независимо от того, что

Итак, я придумал кое-что здесь - и лучшее, что я мог придумать, - это декоратор, чтобы явно объявить, какие вызываемые объекты будут иметь свой собственный контекст - я создал класс ContextLocal, который работает как пространство имен, как jsut thread.local- и атрибуты в этом пространстве имен должен вести себя правильно, как вы ожидаете.

Сейчас я заканчиваю код - поэтому я еще не проверял его на асинхронность или многопоточность, но он должен работать - если вы можете помочь мне написать правильную тестовую батарею, решение, приведенное ниже, может стать пакетом Python.

(Мне пришлось прибегнуть к внедрению объекта в словарь локальных кадров фреймов генератора и сопрограмм, чтобы очистить реестр контекста после завершения генератора или сопрограммы - существует PEP 558, формализующий поведение locals () для Python 3.8 и далее, и я не помню сейчас, разрешена ли эта инъекция - она ​​работает до 3,8 бета 3, хотя, поэтому я думаю, что это использование допустимо)

В любом случае, вот код (названный "context_wrapper.py"):

"""
Super context wrapper -

meant to be simpler to use and work in more scenarios than
Python's contextvars.

Usage:
Create one or more project-wide instances of "ContextLocal"
Decorate your functions, co-routines, worker-methods and generators
that should hold their own states with that instance's `context` method -

and use the instance as namespace for private variables that will be local
and non-local until entering another callable decorated
with `intance.context` - that will create a new, separated scope
visible inside  the decorated callable.


"""

import sys
from functools import wraps

__author__ = "João S. O. Bueno"
__license__ = "LGPL v. 3.0+"

class ContextError(AttributeError):
    pass


class ContextSentinel:
    def __init__(self, registry, key):
        self.registry = registry
        self.key = key

    def __del__(self):
        del self.registry[self.key]


_sentinel = object()


class ContextLocal:

    def __init__(self):
        super().__setattr__("_registry", {})

    def _introspect_registry(self, name=None):

        f = sys._getframe(2)
        while f:
            h = hash(f)
            if h in self._registry and (name is None or name in self._registry[h]):
                return self._registry[h]
            f = f.f_back
        if name:
            raise ContextError(f"{name !r} not defined in any previous context")
        raise ContextError("No previous context set")


    def __getattr__(self, name):
        namespace = self._introspect_registry(name)
        return namespace[name]


    def __setattr__(self, name, value):
        namespace = self._introspect_registry()
        namespace[name] = value


    def __delattr__(self, name):
        namespace = self._introspect_registry(name)
        del namespace[name]

    def context(self, callable_):
        @wraps(callable_)
        def wrapper(*args, **kw):
            f = sys._getframe()
            self._registry[hash(f)] = {}
            result = _sentinel
            try:
                result = callable_(*args, **kw)
            finally:
                del self._registry[hash(f)]
                # Setup context for generator or coroutine if one was returned:
                if result is not _sentinel:
                    frame = getattr(result, "gi_frame", getattr(result, "cr_frame", None))
                    if frame:
                        self._registry[hash(frame)] = {}
                        frame.f_locals["$context_sentinel"] = ContextSentinel(self._registry, hash(frame))

            return result
        return wrapper

Вот модифицированная версия вашего примера для использования с ним:

from contextlib import contextmanager

from context_wrapper import ContextLocal

ctx = ContextLocal()


@contextmanager
def use_mode(mode):
    ctx.MODE = mode
    print("entering use_mode")
    print_mode()
    try:
        yield
    finally:

        pass

def print_mode():
   print(f'Mode {ctx.MODE}')


@ctx.context
def first():
    ctx.MODE = 0
    print('Start first')
    print_mode()
    with use_mode(1):
        print('In first: with use_mode(1)')
        print('In first: start second')
        it = second()
        next(it)
        print('In first: back from second')
        print_mode()
        print('In first: continue second')
        next(it, None)
        print('In first: finish')
        print_mode()
    print("at end")
    print_mode()

@ctx.context
def second():
    print('Start second')
    print_mode()
    with use_mode(2):
        print('In second: with use_mode(2)')
        print('In second: yield')
        yield
        print('In second: continue')
        print_mode()
        print('In second: finish')

first()

Вот результат выполнения этого:

Start first
Mode 0
entering use_mode
Mode 1
In first: with use_mode(1)
In first: start second
Start second
Mode 1
entering use_mode
Mode 2
In second: with use_mode(2)
In second: yield
In first: back from second
Mode 1
In first: continue second
In second: continue
Mode 2
In second: finish
In first: finish
Mode 1
at end
Mode 1

(он будет медленнее, чем нативные контекстные переменные, на порядок, поскольку они встроены в нативный код Python во время выполнения - но, кажется, проще использовать их на столько же)

Автор: jsbueno Размещён: 11.08.2019 07:35
Вопросы из категории :
32x32