Writing is hard work, and your “reading” is important.

preface

The Flask context mechanism will be asked in an interview for a Python Web job if you claim to have read the Flask source code. In this article, we will take a look at the Flask context mechanism.

Flask context

What is context?

Context in everyday life: If you take a paragraph from an article and read it, you may still be unable to understand it because it quotes ideas from other parts of the article. To understand the passage, you need to read and understand the ideas first. These scattered ideas are the context of the passage.

Context in a program: A function usually involves external variables (or methods). To use the function properly, you need to assign values to these external variables. The set of values of these external variables is called context.

Flask view functions need to know the url, parameters, database and other application information requested by the front-end to run properly. How to do this?

A crude approach is to pass this information layer by layer to the view function as a parameter, which is too inelegant. Flask has designed its own context mechanism for this, so that when you need to use request information, you can get all the information about the current request directly from Flask import Request and it is thread-safe in a multi-threaded environment, which is cool.

The general principle behind this effect is the same as in threading. Local, creating a global dictionary object with the thread ID as the key and the corresponding data as the value so that different threads can get their own data.

Flask context mechanism

The Flask context is defined on globals.py with the following code.

# flask/globals.py

def _lookup_req_object(name):
    top = _request_ctx_stack.top
    if top is None:
        raise RuntimeError(_request_ctx_err_msg)
    return getattr(top, name)


def _lookup_app_object(name):
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return getattr(top, name)


def _find_app(a):
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return top.app


# context locals
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)
# partial() builds a partial function
request = LocalProxy(partial(_lookup_req_object, "request"))
session = LocalProxy(partial(_lookup_req_object, "session"))
g = LocalProxy(partial(_lookup_app_object, "g"))
Copy the code

Flask looks like there are multiple contexts in Flask, but they all derive from _request_CTx_STACK and _app_CTx_stack. _request_CTx_stack is the request context, and _app_CTx_stack is the application context.

Common requests and sessions derive from the _request_CTx_stack, while current_APP and G derive from the _app_ctx_stack.

It can be found that the implementation of these contexts all use LocalStack and LocalProxy. The implementation of these two classes is in Werkzeug. Before implementing these two classes, you need to understand the Local class, and the code is as follows.

# werkzeug/local.py

class Local(object):
    __slots__ = ("__storage__"."__ident_func__")

    def __init__(self):
        # Call the __setattr__() method to set the value.
        object.__setattr__(self, "__storage__", {})
        Get the thread ID
        object.__setattr__(self, "__ident_func__", get_ident)

    Iterating over dictionary objects corresponding to different threads
    def __iter__(self):
        return iter(self.__storage__.items())

    # return LocalProxy object
    def __call__(self, proxy):
        """Create a proxy for a name."""
        return LocalProxy(self, proxy)
        
    # pop() clears data saved by the current thread
    def __release_local__(self):
        self.__storage__.pop(self.__ident_func__(), None)

    def __getattr__(self, name):
        try:
            Get the corresponding value in the current thread's data
            return self.__storage__[self.__ident_func__()][name]
        except KeyError:
            raise AttributeError(name)

    Set thread data
    def __setattr__(self, name, value):
        ident = self.__ident_func__()
        storage = self.__storage__
        try:
            storage[ident][name] = value
        except KeyError:
            storage[ident] = {name: value}

    # delete thread data
    def __delattr__(self, name):
        try:
            del self.__storage__[self.__ident_func__()][name]
        except KeyError:
            raise AttributeError(name)
Copy the code

The __init__() method creates __storage__ to store data and __ident_func__ to get the thread ID. Object.__setattr__() is used for assignment.

No, just a tick.

Because the Local class overrides the __setattr__() method itself, assigning directly to self.__storage__ = {} will call the overridden __setattr__ method, resulting in an error. Instead, use the __setattr__ parent class object’s __setattr__. Object.__setattr__ (‘name’, ‘two two ‘) is the same as object.name =’ two two ‘.

The Local class overrides __getattr__, __setattr__, and __delattr__ to customize the Local object property access, set, and delete operations. All of these methods use __ident_func__() to get the current thread ID and use it as a key to manipulate the data corresponding to the current thread. In this way, Local implements multithreaded data isolation.

It’s worth mentioning that __ident_func__() can also get the id of a coroutine, but you need to install the Greenlet library, and this article focuses on threads.

The LocalStack class is a stack structure based on the Local class implementation. The code is as follows.

# werkzeug/local.py
 
Build a stack
class LocalStack(object):

    def __init__(self):
        Instantiate Local
        self._local = Local()
        
    Clear data saved by the current thread
    def __release_local__(self):
        self._local.__release_local__()

    @property
    def __ident_func__(self): Get the current thread ID
        return self._local.__ident_func__

    @__ident_func__.setter
    def __ident_func__(self, value):
        object.__setattr__(self._local, "__ident_func__", value)

    def __call__(self):
        def _lookup(a):
            rv = self.top
            if rv is None:
                raise RuntimeError("object unbound")
            return rv

        return LocalProxy(_lookup)

    def push(self, obj):
        Build a stack using a list
        rv = getattr(self._local, "stack".None)
        if rv is None:
            self._local.stack = rv = []
        rv.append(obj)
        return rv

    def pop(self):
        stack = getattr(self._local, "stack".None)
        if stack is None:
            return None
        elif len(stack) == 1:
            # release_local() still calls __release_local__().
            release_local(self._local)
            return stack[- 1]
        else:
            # the stack
            return stack.pop()

    @property
    def top(self):
        try:
            Get top of stack data
            return self._local.stack[- 1]
        except (AttributeError, IndexError):
            return None
Copy the code

LocalStack = LocalStack = LocalStack = LocalStack = LocalStack = LocalStack = LocalStack = LocalStack = LocalStack The _request_CTx_STACK and _app_CTx_STACK contexts are the stack installed by a thread, and all information about the thread is stored in the corresponding stack until it is needed.

The LocalProxy class is a proxy object of the Local class. Its purpose is to forward operations to the Local object.

# werkzeug/local.py

@implements_bool
class LocalProxy(object):
    __slots__ = ("__local"."__dict__"."__name__"."__wrapped__")

    def __init__(self, local, name=None):
        object.__setattr__(self, "_LocalProxy__local", local)
        object.__setattr__(self, "__name__", name)
        if callable(local) and not hasattr(local, "__release_local__"):
            object.__setattr__(self, "__wrapped__", local)
            
    def _get_current_object(self):
        if not hasattr(self.__local, "__release_local__") :return self.__local() Get the local object
        try:
            return getattr(self.__local, self.__name__)
        except AttributeError:
            raise RuntimeError("no object bound to %s" % self.__name__)
    
    
    #... Omit some code
    
    def __getattr__(self, name):
        if name == "__members__":
            return dir(self._get_current_object())
        Select name from local
        return getattr(self._get_current_object(), name)

    def __setitem__(self, key, value):
        # assign to the local object
        self._get_current_object()[key] = value

    def __delitem__(self, key):
        del self._get_current_object()[key]
    
    __setattr__ = lambda x, n, v: setattr(x._get_current_object(), n, v)
       
    #... Omit some code
Copy the code

The LocalProxy class assigns the local instance to _LocalProxy__local in the __init__() method and operates on it in subsequent methods as __local, which is also a tick.

Because the LocalProxy class overrides the __setattr__ method, it cannot copy it directly, instead assigning it to object.__setattr__(). According to the Python documentation (see the source URL in the reference section), any private variable __xxx that formally begins with a double underscore is replaced by _classname__xxx on the text, and __setattr__ operates directly on the text, so assign _LocalProxy__local.

The logic behind the LocalProxy class is actually a layer of proxies that hand over the real processing to the Local object.

At the end

Considering the number of words, the content of the context is divided into two parts. The next part will put forward several questions and give corresponding answers and opinions.

1. Why do Werkzeug need to create a local class to store data? 2. Why not just use Local? And you want to encapsulate it as a stack operation using the LocalStack class? 3. Why not just use Local? Instead of doing it through the LocalProxy class, right?

Anyway, WHEN I look at the source code, there are such doubts, which is the essence of design, is where we have to learn, to keep in suspense, see you next.

If this article inspires you, click “Watching” to support 22.

Reference:

Flask What is the meaning of a single and a double underscore before an object name? Python documentation: Private Variables