Контекстные переменные в Python

1

Предположим, что у меня есть функция в моем приложении Python, которая определяет какой-то контекст - например user_id. Эта функция вызывает другие функции, которые не воспринимают этот контекст как аргумент функции. Например:

def f1(user, operation):
    user_id = user.id
    # somehow define user_id as a global/context variable for any function call inside this scope
    f2(operation)

def f2(operation):
    # do something, not important, and then call another function
    f3(operation)

def f3(operation):
    # get user_id if there is a variable user_id in the context, get 'None' otherwise
    user_id = getcontext("user_id")
    # do something with user_id and operation

Мои вопросы:

  • Могут ли для этого использоваться переменные контекста Python 3.7? Как?
  • Это то, для чего предназначены эти переменные контекста?
  • Как это сделать с Python v3.6 или ранее?

РЕДАКТИРОВАТЬ

По нескольким причинам (архитектурное наследие, библиотеки и т.д.) Я не могу/не буду изменять подпись промежуточных функций, таких как f2, поэтому я не могу просто передать user_id качестве аргументов, и не user_id все эти функции внутри одного класса.

Теги:
python-3.x
python-3.7

3 ответа

3
Лучший ответ

Вы можете использовать contextvars в Python 3.7 для того, о чем вы спрашиваете. Это обычно очень легко:

import contextvars

user_id = contextvars.ContextVar("user_id")

def f1(user, operation):
    user_id.set(user.id)
    f2()

def f2():
    f3()

def f3():
    print(user_id.get(default=None))  # gets the user_id value, or None if no value is set

Метод set в ContextVar возвращает экземпляр Token, который вы можете использовать для сброса переменной до значения, которое она имела до выполнения set операции. Поэтому, если вы хотите, чтобы f1 восстанавливал вещи так, как они были (не очень полезен для переменной user_id, но более уместен для чего-то вроде установки точности в decimal модуле), вы можете сделать:

token = some_context_var.set(value)
try:
    do_stuff()    # can use value from some_context_var with some_context_var.get()
finally:
    some_context_var.reset(token)

Там больше для модуля contextvars чем это, но вам почти наверняка не нужно иметь дело с другим материалом. Вероятно, вам нужно создавать собственные контексты и запускать код в других контекстах, если вы создаете собственную асинхронную структуру с нуля.

Если вы просто используете существующую структуру (или записываете библиотеку, которую хотите играть с асинхронным кодом), вам не нужно разбираться в этом. Просто создайте глобальный ContextVar (или найдите один из них, уже определенный вашей каркасом), и get и set значения на нем, как показано выше, и вам должно быть хорошо идти.

Множество contextvars использования, вероятно, будет в фоновом режиме, в качестве детали реализации различных библиотек, которые хотят иметь "глобальное" состояние, которое не течет между потоками или отдельными асинхронными задачами в рамках одного потока. Вышеприведенный пример может иметь больше смысла в такой ситуации: f1 и f3 являются частью одной и той же библиотеки, а f2 - передаваемый пользователем обратный вызов, переданный в библиотеку где-то в другом месте.

  • 0
    Означает ли это, что если в моем приложении запущено более одного потока, если thread1 установит контекстную переменную user_id он будет установлен только для этого потока, а не для других?
  • 2
    Да, в этом и заключается цель использования ContextVar , а не просто глобальной переменной. Промежуточное решение, более совместимое с предыдущими версиями, если вам нужно работать с версией до 3.7, использует threading.local для получения пространства имен, специфичного для вашего потока (но не для вашей асинхронной среды).
2

По сути, вы ищете способ совместного использования состояния между набором функций. Канонический способ сделать это на объектно-ориентированном языке - использовать класс:

class Foo(object):
    def __init__(self, operation, user=None):
        self._operation = operation
        self._user_id = user.id if user else None

    def f1(self):
        print("in f1 : {}".format(self._user_id))
        self.f2()

    def f2(self):
        print("in f2 : {}".format(self._user_id))
        self.f3()

    def f3(self):
        print("in f3 : {}".format(self._user_id))


 f = Foo(operation, user)
 f.f1()

С помощью этого решения ваши экземпляры классов (здесь f) являются "контекстом", в котором выполняются функции - каждый экземпляр имеет свой собственный выделенный контекст.

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

И, наконец, чисто процедурное решение состоит в том, чтобы передать этот контекст (который может быть выражен как dict или любой подобный тип данных) по всей цепочке вызовов, как показано в ответе DFE.

Как правило: полагаясь на глобальные переменные или какой-то "волшебный" контекст, который может или не был установлен вами - не-who-nor-where-nor-when делает для кода, который трудно, если не невозможно, и это может прорваться самыми непредсказуемыми способами (поиск по "глобалам зла" приведет к ужасному количеству литтера на эту тему).

  • 0
    Как я уже добавил к этому вопросу, у меня нет возможности разместить эти функции внутри класса, но я прислушиваюсь к вашим советам относительно опасностей этих глобальных переменных.
1

Вы можете использовать kwargs в своих вызовах функций, чтобы пройти

def f1(user, operation):
    user_id = user.id
    # somehow define user_id as a global/context variable for any function call inside this scope
    f2(operation, user_id=user_id)

def f2(operation, **kwargs):
    # do something, not important, and then call another function
    f3(operation, **kwargs)

def f3(operation, **kwargs):
    # get user_id if there is a variable user_id in the context, get 'None' otherwise
    user_id = kwargs.get("user_id")
    # do something with user_id and operation

kwargs dict эквивалентен тому, что вы смотрите в переменных контекста, но ограниченным в стеке вызовов. Это тот же самый элемент памяти, который передается (посредством указателя) в каждой функции, а не дублирует переменные в памяти.

На мой взгляд, но я хотел бы видеть, что вы все думаете, переменные контекста - это элегантный способ авторизации глобальных переменных и управления им.

  • 1
    Это довольно хорошее решение, но вы должны объяснить обоснование решения о передаче kwargs между всеми функциями (вместо использования глобального контекста).
  • 2
    Так как OP написал get user_id if there is a variable user_id in the context, get 'None' otherwise случае, вероятно, лучше будет user_id = kwargs.get('user_id') . user_id = kwargs['user_id'] вызовет KeyError, если такая переменная не установлена, использование метода get () вернет любое неявное или явное значение по умолчанию.
Показать ещё 1 комментарий

Ещё вопросы

Сообщество Overcoder
Наверх
Меню