28

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.

Jundiaius
  • 6,214
  • 3
  • 30
  • 43
  • 3
    Do you have any reason not to pass `user_id` as an argument to `f2` and `f3`? – Cihan Jun 14 '18 at 10:24
  • 9
    You seem to be misled by the broad range of meanings of the word *context*; since I can't recognize anything *asynchronous* here I don't believe this is an intended use of the concept. Somewhat difficult to decide, but I strongly suggest to pass the user_id as parameter to `f2()`and `f3()` or to create a class, with f1, f2 and f3 as methods. Your use looks suspiciously similar to a globale variable. – guidot Jun 14 '18 at 10:25
  • In your case, what is the difference by using global variable? – James Lin May 30 '19 at 03:22

3 Answers3

24

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.

Blckknght
  • 100,903
  • 11
  • 120
  • 169
  • Does that mean that if I have more than one thread running on my application, if thread1 set the context variable `user_id` it will be only set for this thread, and not for the others? – Jundiaius Jun 14 '18 at 12:30
  • 2
    Yes, that's the whole purpose of using a `ContextVar`, rather than just a global variable. An in-between solution that's more backwards compatible, if you need to run on a pre-3.7 version is using [`threading.local`](https://docs.python.org/3.7/library/threading.html#thread-local-data) to get a namespace specific to your thread (but not to your async environment). – Blckknght Jun 14 '18 at 14:34
12

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

bruno desthuilliers
  • 75,974
  • 6
  • 88
  • 118
  • As I have added now to the question, I don't have the possibility to place those functions inside a class, but I take your advice concerning the dangers of this global-like variables. – Jundiaius Jun 14 '18 at 13:33
2

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.

DFE
  • 126
  • 9
  • 1
    That's a pretty good solution, but you should explain the rationale behind the decision to pass kwargs between all the functions (instead of using a global context). – Aran-Fey Jun 14 '18 at 10:21
  • 2
    Since OP wrote `get user_id if there is a variable user_id in the context, get 'None' otherwise` this should probably better be `user_id = kwargs.get('user_id')`. `user_id = kwargs['user_id']` will raise a KeyError if no such variable is set, using the [get()](https://docs.python.org/3/library/stdtypes.html#dict.get) method will return any implicit or explicit default. – shmee Jun 14 '18 at 10:38
  • @shmee Thank you for the comment, I edited it and I will take it in account in my own code! – DFE Jun 18 '18 at 09:16