How session is passed to templates and between fun

2019-07-30 22:35发布

问题:

During investigation of another conundrum that I had with Flask sessions, I have tried to better my understanding of how Flask sessions work, in general.


According to Flask documentation on sessions session object itself is a proxy.

My understanding (that is, in all probability, wrong in some way, and this is what this question is about =) of what it means is:

  1. the proxy session object is accessed from the app and is used to modify read data as needed

  2. normally, proxy-session will transfer it's changes to proxied-session right away (except for the change of mutables in proxy-session)

  3. in case if proxied-session is busy (in case of a multi-thread app), proxy-session will wait until proxied-session available, and then transfer it's changes to proxied-session

  4. templates receive the 'original' session (i.e. proxied-session), so there is no ability/need to access session._get_current_object() from the templates

  5. as dictionaries (which session is) are mutable, I'd assume it's id should remain unchanged for the length of session (though content could be modified)

  6. the actual session (proxied object, that is available via session._get_current_object()) should never change its ID

Now, when I have tried to check my assumptions - the behaviour I've encountered confused me a bit.

Consider the following code:

my_app.py

from flask import (
Flask,
render_template,
session,
)

app = Flask(__name__)
app.secret_key = 'some random secret key'

@app.route('/create/')
def create():
    session['example'] = ['one', 'two']
    print_ids()
    return str(session['example'])

@app.route('/modify/')
def modify():
    session['example'].append('three')
    print_ids()
    return render_template('my_template.html', id=id)

@app.route('/display/')
def display():
    print_ids()
    return str(session['example'])

def print_ids():
    import inspect
    calling_function = inspect.stack()[1][3]
    print('')
    print(calling_function + ": session ID is: {}".format(id(session)))
    print(calling_function + ": session['example'] ID is {}".format(id(session['example'])))
    print('________________________________')
    print(calling_function + ": session._get_current_object() ID is: {}".format(id(session._get_current_object())))
    print(calling_function + ": session._get_current_object()['example'] ID is: {}".format(id(session._get_current_object()['example'])))

my_template.html

<!doctype html>
<html>
    <head><title>Display session['example']</title></head>
    <body>
        <div>
            {% if session['example'] %}
                {{ session['example'] }}
                <br />
                session ID is: {{ id(session) }}
                <br />
                session['example'] ID is: {{ id(session['example']) }}
                <br />
            {% else %}
                session['example'] is not set =(
            {% endif %}
        </div>
    </body>
</html>

The idea is to print id's of proxy-session, session['example'] (which is a list), proxied-session (i.e. session._get_current_object()) and proxied-session['example'] (i.e. session._get_current_object()['example']) from every function, as well as id(session) and id(session['example']) on the rendered template, in order to track down what is used where.

Here are the results:

.../create/
    # id(session)                                 4338311808 
    # id(session._get_current_object())           4343709776
    # id(session['example'])                                 4343654376
    # id(session._get_current_object()['example'])           4343654376

.../modify/
    # id(session)                                  4338311808
    # id(session._get_current_object())            4344315984
    # id(session['example'])                                  4343652720      
    # id(session._get_current_object()['example'])            4343652720
rendered my_template.html
    # id(session)                                  4344315984
    # id(session['example'])                                  4343652720

.../display/
    # id(session)                                  4338311808         
    # id(session._get_current_object())            4344471632
    # id(session['example'])                                  4341829576
    # id(session._get_current_object()['example'])            4341829576

# one more time
.../display/
    # id(session)                                  4338311808         
    # id(session._get_current_object())            4344471632
    # id(session['example'])                                  4344378072
    # id(session._get_current_object()['example'])            4344378072

Things I'm striving to understand are:

  1. What are my misunderstandings/wrong assumptions, concerning Flask sessions concept, in general?
  2. Why ids of session['example'] and session._get_current_object()['example'] are changed on every hit of display (and every other method, but display in particular, since it does not modify anything, I would expect all ids to remain unchanged)?
  3. Why id of session._get_current_object() changes and id of session is not?
  4. Since ids of session['example'] and session._get_current_object()['example'] are identical in context of any function, I would assume that if one object is changed - then both are changed, as they are the same object.

    With that being said, and taking into account that session._get_current_object()['example'] is inside proxied (i.e. 'real') session I'd expect the following:

    .../create/ # return ['one', 'two']
    .../modify/ # will render page containing ['one', 'two', 'three']
    .../display/ # return ['one', 'two', 'three'] as proxy and proxied sessions should have been modified

But as I have previously discovered - it's not happening. So why ids are the same?

回答1:

Most of your confusion comes from misunderstandings about Flask proxy objects, such as session, g and request.

All that these objects do is make sure you get the correct data for the current thread; they proxy between a global object (accessible by all threads, easy to import and use in your Flask code), to an object stored in thread-local storage, which is an object that transparently differentiates attribute access by thread id. There is no need for locking or 'waiting' in this, proxied objects are never used by more than one thread. session.foo indirectly accesses and returns the exact same object as session._get_current_object().foo does (which is why their ids always match).

So when accessing the session object, proxying is transparent. This is not something you ever need worry about unless you want to share a proxied object with another thread.

The proxied object you access is created a-new for every request. That's because the contents of a session are dependent on data in each request. Flask's session machinery is pluggable, but the default implementation stores all data in a cryptographically signed cookie, which needs to be decoded into Python data if you want to be able to interact with it. Each of your /create/, /modify/ and /display/ URLs are handled as separate requests, so they all load session data from your request into new Python objects; their ids will usually differ.

After a request is done, the session object is gone again. You can't have this any other way, because a new request coming in on the same thread needs to present the session data from that new request to your Flask code, not the data from the old request.

All this means that the output of id() is meaningless here. id() is a number that is unique for all currently active objects in the current Python process. This means that the ids of objects that are removed from memory can be reused, just because you have seen the same id() value at two points in time doesn't mean you have the same object. And just because you have the same data (value equality) doesn't mean that you have the same object in memory, even if their id is the same. The old object could have been deleted and a new object could simply have been re-created with the same value.

Under the hood, Flask calls the open_session() method on the object assigned to Flask().session_interface at the start of each request. At the end, the save_session() method is called to save the session again, and the session object is discarded. The default implementation is the SecureSessionInterface object, which looks for a specific cookie on the request, and if present and with a valid signature, decodes the data as tagged JSON (a compact JSON serialisation), and returns a SecureCookieSession instance with that data. This is the object that session proxies for, and is returned by session._get_current_object(). When saving, the data is serialised to tagged JSON again, signed, and added to the response as an outgoing Set-Cookie header.

Saving only happens when the session object has been 'changed', (session.modified is set to True). Note that the default implementation only sets modified to True when directly manipulating the session mapping (setting, updating or deleting keys in the mapping itself), not when changing mutable objects stored in the session; session['foo'] = 'bar' is detectable, but if you stored a list or dictionary in the session, then mutating those with expressions like session['spam'][0] = 'ham' will not be detected. Either re-set the mutable object (session[key] = session[key]) or set the modified flag to True manually.



回答2:

Dedication =): this answer only came to be thanks to users: brunns and shmee and their answers: 1(brunns), 2(shmee) to my other questions.


Here are answers to the list of (my own) questions:

  1. The main blunder is that: yes — session is a proxy, yes — object proxied by session is returned by session._get_current_object(), BUT: object proxied by session is different for every request.

  2. It is because object proxied by session (and thus everything it contains) is different for each request. For details: see answer to point 3, below.

  3. Well:

    • this answer pointed out to me that, as session is an object imported from the flask module and as it is imported only once — its id() does not ever change

    • there is one underlying object (which is returned by session._get_current_object()) per request, and as suggested by an answer to another question, as well as in Flask documentation — object proxied by session belongs to RequestContext and thus is different for each new request. Hence, different values for different requests (the only obscurity here is that sometimes session._get_current_object() remains unchanged between between consecutive requests, and as pointed out in the same answer (bold is mine), it is:

      probably, due to the new session object being created in the same memory address that the old one from the previous request occupied.

  4. Here the expectations are wrong — not the results. The reason session['example'] is not modified from one request to another is clearly stated in the documentation on modified attribute of a session:

    Be advised that modifications on mutable structures are not picked up automatically, in that situation you have to explicitly set the attribute to True yourself.

    As session['example'] is a list and lists are mutable — in order changes to be picked up, we need to change the code for modify function, as follows:

    @app.route('/modify/')
    def modify():
        session['example'].append('three')
        session.modified = True
        print_ids()
        return render_template('my_template.html', id=id)
    

    After this change:

    .../create/ # returns ['one', 'two']
    .../modify/ # renders page containing ['one', 'two', 'three']
    .../display/ # returns ['one', 'two', 'three']