【问题标题】:Context variables in PythonPython中的上下文变量
【发布时间】:2018-06-14 10:10:00
【问题描述】:

假设我的 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 的Context Variables 可以用于这个吗?怎么样?
  • 这就是这些上下文变量的用途吗?
  • 如何使用 Python v3.6 或更早版本执行此操作?

编辑

出于多种原因(架构遗留、库等)我不能/不会更改像 f2 这样的中间函数的签名,所以我不能只通过 user_id作为参数,不要将所有这些函数放在同一个类中。

【问题讨论】:

  • 您有什么理由不将user_id 作为参数传递给f2f3
  • 您似乎被context这个词的广泛含义所误导;因为我在这里无法识别任何 异步 我不认为这是该概念的预期用途。有点难以决定,但我强烈建议将 user_id 作为参数传递给 f2()f3() 或创建一个类,使用 f1、f2 和 f3 作为方法。您的使用看起来与全局变量非常相似。
  • 在你的情况下,使用全局变量有什么区别?

标签: python python-3.x python-3.7


【解决方案1】:

您可以在 Python 3.7 中使用 contextvars 来满足您的要求。这通常很容易:

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

ContextVar 上的 set 方法返回一个 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(或查找一个已由您的框架定义的)并在其上设置 getset 值,如上所示,您应该一切顺利。

很多contextvars 的使用可能会在后台进行,作为各种库的实现细节,这些库希望具有“全局”状态,不会泄漏线程之间或单独异步任务之间的更改单线程。在这种情况下,上面的示例可能更有意义:f1f3 是同一个库的一部分,f2 是用户提供的回调,传递到其他地方的库中。

【讨论】:

  • 这是否意味着如果我的应用程序上运行了多个线程,如果 thread1 设置了上下文变量user_id,它将只为这个线程设置,而不是为其他线程设置?跨度>
  • 是的,这就是使用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 的回答所示。

作为一般规则:依赖全局变量或某些“魔术”上下文,这些上下文可以(或不)由 you-dont-who-nor-where-nor-when 设置,即使不是不可能,也很难编写代码原因,并且可能以最不可预测的方式中断(谷歌搜索“全球邪恶”将产生大量关于该主题的文献)。

【讨论】:

  • 正如我现在在问题中添加的那样,我不可能将这些函数放在一个类中,但我接受你关于这种类似全局变量的危险的建议。
【解决方案3】:

您可以在函数调用中使用 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 等同于您在上下文变量中查看的内容,但仅限于调用堆栈。它是在每个函数中传递(通过类似指针)的相同内存元素,并且不会复制内存中的变量。

在我看来,但我想看看大家的看法,上下文变量是一种授权全局变量并对其进行控制的优雅方式。

【讨论】:

  • 这是一个很好的解决方案,但您应该解释决定在所有函数之间传递 kwargs 的理由(而不是使用全局上下文)。
  • 既然 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() 方法将返回任何隐式或显式默认值。
  • @shmee 谢谢你的评论,我编辑了它,我会在我自己的代码中考虑到它!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-12-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多