Context variables in Python

2020-03-01 17:19发布

问题:

Suppose that I have a function in my Python application that define some kind of context - a user_id for example. This function call other functions that do not take this context as a function argument. For example:

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

My questions are:

  • Can the Context Variables of Python 3.7 be used for this? How?
  • Is this what these Context Variables are intended for?
  • How to do this with Python v3.6 or earlier?

EDIT

For multiple reasons (architectural legacy, libraries, etc) I can't/won't change the signature of intermediary functions like f2, so I can't just pass user_id as arguments, neither place all those functions inside the same class.

回答1:

You can use contextvars in Python 3.7 for what you're asking about. It's usually really easy:

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

The set method on the ContextVar returns a Token instance, which you can use to reset the variable to the value it had before the set operation took place. So if you wanted f1 to restore things the way they were (not really useful for a user_id context variable, but more relevant for something like setting the precision in the decimal module), you can do:

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)

There's more to the contextvars module than this, but you almost certainly don't need to deal with the other stuff. You probably only need to be creating your own contexts and running code in other contexts if you're writing your own asynchronous framework from scratch.

If you're just using an existing framework (or writing a library that you want to play nice with asynchronous code), you don't need to deal with that stuff. Just create a global ContextVar (or look up one already defined by your framework) and get and set values on it as shown above, and you should be good to go.

A lot of contextvars use is probably going to be in the background, as an implementation detail of various libraries that want to have a "global" state that doesn't leak changes between threads or between separate asynchronous tasks within a single thread. The example above might make more sense in this kind of situation: f1 and f3 are part of the same library, and f2 is a user-supplied callback passed into the library somewhere else.



回答2:

Essentially what you're looking for is a way to share a state between a set of function. The canonical way to do so in an object oriented language is to use a class:

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()

With this solution, your class instances (here f) are "the context" in which the functions are executed - each instance having it's own dedicated context.

The functional programing equivalent would be to use closures, I'm not going to give an example here since while Python supports closures, it's still first and mainly an object language so the OO solution is the most obvious.

And finally, the clean procedural solution is to pass this context (which can be expressed as a dict or any similar datatype) all along the call chain, as shown in DFE's answer.

As a general rule : relying on global variables or some "magic" context that could - or not - be set by you-dont-who-nor-where-nor-when makes for code that is hard if not impossible to reason about, and that can break in the most unpredictable ways (googling for "globals evil" will yield an awful lot of litterature on the topic).



回答3:

You can use kwargs in your function calls in order to pass

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

the kwargs dict is the equivalent to what you are looking at in context variables, but limited at a call stack. It is the same memory element passed (through pointer-like) in each function and not duplicates variables in memory.

In my opinion, but I would like to see what you all think, context variables is an elegant way to authorize globals variables and to control it.