精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

局部變量是怎么實現靜態查找的,它和 local 名字空間又有什么聯系呢?

開發 前端
我們說過,當調用 locals 的時候,會對名字空間進行更新,然后返回更新之后的名字空間。由于函數內部存在 y = ... 這樣的賦值語句,所以符號表中就存在 "y" 這個符號,于是會進行更新。但更新的時候,發現 y 還沒有被賦值,于是又將字典中的鍵值對 "y": 2 給刪掉了。


楔子


前面我們剖析了字節碼的執行流程,本來應該接著介紹一些常見指令的,但因為有幾個指令涉及到了局部變量,所以我們單獨拿出來說。與此同時,我們還要再度考察一下 local 名字空間,它的背后還隱藏了很多內容。

我們知道函數的參數和函數內部定義的變量都屬于局部變量,均是通過靜態方式訪問的。

x = 123

def foo1():
    global x
    a = 1
    b = 2

# co_nlocals 會返回局部變量的個數
# a 和 b 是局部變量,x 是全局變量,因此是 2
print(foo1.__code__.co_nlocals)  # 2


def foo2(a, b):
    pass

print(foo2.__code__.co_nlocals)  # 2


def foo3(a, b):
    a = 1
    b = 2
    c = 3

print(foo3.__code__.co_nlocals)  # 3

無論是參數還是內部新創建的變量,本質上都是局部變量。

按照之前的理解,當訪問一個全局變量時,會去訪問 global 名字空間(也叫全局名字空間)。

圖片

那么問題來了,當操作函數的局部變量時,是不是也等價于操作其內部的 local 名字空間(局部名字空間)呢?我們往下看。


圖片


如何訪問(創建)一個局部變量


之前我們說過 Python 變量的訪問是有規則的,會按照本地、閉包、全局、內置的順序去查找,也就是 LEGB 規則,所以在查找變量時,local 名字空間應該是第一選擇。

但不幸的是,虛擬機在為調用的函數創建棧幀對象時,這個至關重要的 local 名字空間并沒有被創建。因為棧幀的 f_locals 字段和 f_globals 字段分別指向了局部名字空間和全局名字空間,而創建棧幀時 f_locals 被初始化成了 NULL,所以并沒有創建局部名字空間。

我們通過源碼來進行驗證,不過要先補充一個知識點,就是當調用一個 Python 函數時,底層會調用哪些 C 函數呢?

圖片

我們看一下源碼:

// Objects/call.c
/*
 * Python 函數也是一個對象,當調用 Python 函數時
 * 底層會將 Python 函數對象作為參數,調用 _PyFunction_Vectorcall
 * 關于函數,我們后續會剖析
 */
PyObject *
_PyFunction_Vectorcall(PyObject *func, PyObject* const* stack,
                       size_t nargsf, PyObject *kwnames)
{
    // ...
    
    // 參數 func 指向函數對象,它內部的 func_code 指向 PyCodeObject 對象
    // 如果 co_flags & CO_OPTIMIZED 為真,表示 PyCodeObject 是被優化過的
    // 那么對應的函數在調用時,會靜態查找本地局部變量
    if (((PyCodeObject *)f->func_code)->co_flags & CO_OPTIMIZED) {
        // 在這種情況下,會給 _PyEval_Vector 的第三個參數傳遞 NULL
        return _PyEval_Vector(tstate, f, NULL, stack, nargs, kwnames);
    }
    // ...
}


// Python/ceval.c
/*
 * 創建棧幀,調用 _PyEval_EvalFrame,最終執行幀評估函數
 * 注意該函數的第三個參數,顯然它表示局部名字空間
 * 而 _PyFunction_Vectorcall 在調用時傳遞的是 NULL
 */
PyObject *
_PyEval_Vector(PyThreadState *tstate, PyFunctionObject *func,
               PyObject *locals,
               PyObject* const* args, size_t argcount,
               PyObject *kwnames)
{
    // ...
    
    // 執行幀評估函數之前,要先創建棧幀
    // 這個過程由 _PyEvalFramePushAndInit 負責
    _PyInterpreterFrame *frame = _PyEvalFramePushAndInit(
        tstate, func, locals, args, argcount, kwnames);
    // ...
    return _PyEval_EvalFrame(tstate, frame, 0);
}

/*
 * 在當前棧幀之上創建新的棧幀,并推入虛擬機為其準備的 C Stack 中
 */
static _PyInterpreterFrame *
_PyEvalFramePushAndInit(PyThreadState *tstate, PyFunctionObject *func,
                        PyObject *locals, PyObject* const* args,
                        size_t argcount, PyObject *kwnames)
{
    // ...

    // 棧幀創建之后,調用 _PyFrame_Initialize,進行初始化
    _PyFrame_Initialize(frame, func, locals, code, 0);
    // ...
}

// Include/internal/pycore_frame.h
/*
 * 對 frame 進行初始化
 */
staticinlinevoid
_PyFrame_Initialize(
    _PyInterpreterFrame *frame, PyFunctionObject *func,
    PyObject *locals, PyCodeObject *code, int null_locals_from)
{
    frame->f_funcobj = (PyObject *)func;
    frame->f_code = (PyCodeObject *)Py_NewRef(code);
    frame->f_builtins = func->func_builtins;
    frame->f_globals = func->func_globals;
    // 將 f_locals 字段初始化為參數 locals
    // 而參數 locals 是從 _PyFunction_Vectorcall 一層層傳過來的
    // 由于 _PyFunction_Vectorcall 傳的是 NULL
    // 所以棧幀的 f_locals 字段最終會被初始化為 NULL
    frame->f_locals = locals;
    // ...
}

所以我們驗證了在調用函數時,棧幀的局部名字空間確實被初始化為 NULL,當然也明白了 C 函數的調用鏈路。

我們用 Python 代碼演示一下:

import inspect

# 模塊的棧幀
frame = inspect.currentframe()
# 對于模塊而言,局部名字空間和全局名字空間是同一個字典
print(frame.f_locals is frame.f_globals)  # True
# 當然啦,局部名字空間和全局名字空間也可以通過內置函數獲取
print(
    frame.f_locals is locals() is frame.f_globals is globals()
)  # True


# 但對于函數而言就不一樣了
def foo():
    name = "古明地覺"
    return inspect.currentframe()

frame = foo()
# global 名字空間全局唯一
# 無論是獲取棧幀的 f_globals,還是調用 globals()
# 得到的都是同一份字典
print(frame.f_globals is globals())  # True
# 但每個函數都有自己獨立的局部名字空間
print(frame.f_locals)  # {'name': '古明地覺'}

# 咦,不是說局部名字空間被初始化為 NULL 嗎?
# 那么在 Python 里面獲取的話,結果應該是個 None 才對啊
# 關于這一點,我們稍后會解釋

總之對于函數而言,在創建棧幀時,它的 f_locals 被初始化為 NULL。那么問題來了,局部變量到底存儲在什么地方呢?當然,由于變量只是一個名字(符號),而局部變量的名字都存儲在符號表中,所以更嚴謹的說法是,局部變量的值存儲在什么地方?

在介紹虛擬機執行字節碼的時候我們說過,當函數被調用時,虛擬機會為其創建一個棧幀。棧幀是虛擬機的執行環境,包含了執行時所依賴的上下文,而棧幀內部有一個字段叫 f_localsplus,它是一個數組。

圖片圖片

這個數組雖然是一段連續內存,但在邏輯上被分成了 4 份,其中局部變量便存儲在 f_localsplus 的第一份空間中。現在我們明白了,局部變量是靜態存儲在數組中的。

我們舉個例子。

def foo(a, b):
    c = a + b
    print(c)

它的字節碼如下:

圖片

注意里面的 LOAD_FAST 和 STORE_FAST,這兩個指令對應的邏輯如下。

TARGET(LOAD_FAST) {
    PyObject *value;
    #line 192 "Python/bytecodes.c"
    // 通過宏 GETLOCAL 獲取局部變量的值
    value = GETLOCAL(oparg);
    assert(value != NULL);
    Py_INCREF(value);
    #line 90 "Python/generated_cases.c.h"
    // 將值壓入運行時棧,等價于 PUSH(value)
    STACK_GROW(1);
    stack_pointer[-1] = value;
    DISPATCH();
}

TARGET(STORE_FAST) {
    // 獲取棧頂元素
    PyObject *value = stack_pointer[-1];
    #line 209 "Python/bytecodes.c"
    // 通過宏 SETLOCAL 創建局部變量
    SETLOCAL(oparg, value);
    #line 124 "Python/generated_cases.c.h"
    // 將 stack_pointer 向棧底移動一個位置,即彈出棧頂元素
    // 如果和第一行組合起來的話,等價于 TOP()
    STACK_SHRINK(1);
    DISPATCH();
}

所以 LOAD_FAST 和 STORE_FAST 分別負責加載和創建局部變量,而核心就是里面的兩個宏:GETLOCAL、SETLOCAL。

// Python/ceval_macros.h

#define GETLOCAL(i)     (frame->localsplus[i])

#define SETLOCAL(i, value)      do { PyObject *tmp = GETLOCAL(i); \
                                     GETLOCAL(i) = value; \
                                     Py_XDECREF(tmp); } while (0)
/* 這里額外再補充一個關于 C 語言的知識點
 * 我們看到宏 SETLOCAL 展開之后的結果是 do {...} while (0) 
 * do while 循環會先執行 do 里面的循環體,然后再判斷條件是否滿足
 * 因此從效果上來說,執行 do {...} while (0) 和直接執行 ... 是等價的
 * 那么問題來了,既然效果等價,為啥還要再套一層 do while 呢
 * 其實原因很簡單,如果宏在展開之后會生成多條語句,那么這些語句要成為一個整體
 * 另外由于 C 程序的語句要以分號結尾,所以在調用宏時,我們也會習慣性地在結尾加上分號
 * 因此我們希望有這樣一種結構,能同時滿足以下要求:
 *   1)可以將多條語句包裹起來,作為一個整體;
 *   2)程序的語義不能發生改變;
 *   3)在語法上,要以分號結尾;
 * 顯然 do while 完美滿足以上三個要求,只需將 while 里的條件設置為 0 即可
 * 并且當編譯器看到 while (0) 時,也會進行優化,去掉不必要的循環控制結構
 * 因此以后看到 do {...} while (0) 時,不要覺得奇怪,這是宏的一個常用技巧
 */

我們看到操作局部變量,就是在基于索引操作數組 f_localsplus,顯然這個過程比操作字典要快。盡管字典是經過高度優化的,但顯然再怎么優化,也不可能快過數組的靜態操作。

所以此時我們對局部變量的藏身之處已經了然于心,它們就存放在棧幀的 f_localsplus 字段中,而之所以沒有使用 local 名字空間的原因也很簡單。因為函數內部的局部變量在編譯時就已經確定了,個數是不會變的,因此編譯時也能確定局部變量占用的內存大小,以及訪問局部變量的字節碼指令應該如何訪問內存。

def foo(a, b):
    c = a + b
    print(c)

print(
    foo.__code__.co_varnames
)  # ('a', 'b', 'c')

比如變量 c 位于符號表中索引為 2 的位置,這在編譯時就已確定。

  • 當創建變量 c 時,只需修改數組 f_localsplus 中索引為 2 的元素即可。
  • 當訪問變量 c 時,只需獲取數組 f_localsplus 中索引為 2 的元素即可。

這個過程是基于數組索引實現的靜態查找,所以操作局部變量和操作全局變量有著異曲同工之妙。操作全局變量本質上是基于 key 操作字典的 value,其中 key 是變量的名稱,value 是變量的值;而操作局部變量本質上是基于索引操作數組 f_localsplus 的元素,這個索引就是變量名在符號表中的索引,對應的數組元素就是變量的值。

所以我們說 Python 的變量其實就是個名字,或者說符號,到這里是不是更加深刻地感受到了呢?

但對于局部變量來說,如果想實現靜態查找,顯然要滿足一個前提:變量名在符號表中的索引和與之綁定的值在 f_localsplus 中的索引必須是一致的。毫無疑問,兩者肯定是一致的,并且索引是多少在編譯階段便已經確定,會作為指令參數保存在字節碼指令序列中。

好,到此可以得出結論,雖然虛擬機為函數實現了 local 名字空間(初始為 NULL),但在操作局部變量時卻沒有使用它,原因就是為了更高的效率。當然還有所謂的 LEGB,都說變量查找會遵循這個規則,但我們心里清楚,局部變量其實是靜態訪問的,不過完全可以按照 LEGB 的方式來理解。


圖片


解密 local 名字空間


先來看一下全局名字空間:

x = 1

def foo():
    globals()["x"] = 2
    
foo()
print(x)  # 2

global 空間全局唯一,在 Python 層面上就是一個字典,在任何地方操作該字典,都相當于操作全局變量,即使是在函數內部。

因此在執行完 foo() 之后,全局變量 x 就被修改了。但 local 名字空間也是如此嗎?我們嘗試一下。

def foo():
    x = 1
    locals()["x"] = 2
    print(x)


foo()  # 1

我們按照相同的套路,卻并沒有成功,這是為什么?原因就是上面解釋的那樣,函數內部有哪些局部變量在編譯時就已經確定了,查詢的時候是從數組 f_localsplus 中靜態查找的,而不是從 local 名字空間中查找。

然后我們打印一下 local 名字空間,看看里面都有哪些內容。

def foo():
    name = "satori"
    print(locals())
    age = 17
    print(locals())
    gender = "female"
    print(locals())

foo()
"""
{'name': 'satori'}
{'name': 'satori', 'age': 17}
{'name': 'satori', 'age': 17, 'gender': 'female'}
"""

我們看到打印 locals() 居然也會顯示內部的局部變量,相信聰明如你已經猜到 locals() 是怎么回事了。因為局部變量不是從局部名字空間里面查找的,所以它初始為空,但當我們執行 locals() 的時候,會動態構建一個字典出來。

符號表里面存儲了局部變量的符號(或者說名字),f_localsplus 里面存儲了局部變量的值,當執行 locals() 的時候,會基于符號表和 f_localsplus 創建一個字典出來。

def foo():
    name = "satori"
    age = 17
    gender = "female"
    print(locals())

# 符號表:保存了函數中創建的局部變量的名字
print(foo.__code__.co_varnames)
"""
('name', 'age', 'gender')
"""
# 調用函數時會創建棧幀,局部變量的值都保存在 f_localsplus 里面
# 并且符號表中變量名的順序和 f_localsplus 中變量值的順序是一致的
f_localsplus = ["satori", 17, "female"]
# 這里就用一個列表來模擬了

我們來看一下變量的創建。

  • 由于符號 name 位于符號表中索引為 0 的位置,那么執行 name = "satori" 時,就會將 "satori" 放在 f_localsplus 中索引為 0 的位置。
  • 由于符號 age 位于符號表中索引為 1 的位置,那么執行 age = 17 時,就會將 17 放在 f_localsplus 中索引為 1 的位置。
  • 由于符號 gender 位于符號表中索引為 2 的位置,那么執行 gender = "female" 時,就會將 "female" 放在 f_localsplus 中索引為 2 的位置。

后續在訪問變量的時候,比如訪問變量 age,由于它位于符號表中索引為 1 的位置,那么就會通過 f_localsplus[1] 獲取它的值,這些符號對應的索引都是在編譯階段確定的。所以在運行時才能實現靜態查找,指令 LOAD_FAST 和 STORE_FAST 都是基于索引來靜態操作底層數組。

我們用一張圖來描述這個過程:

圖片

符號表負責存儲局部變量的名字,f_localsplus 負責存儲局部變量的值(里面的元素初始為 NULL),而在給局部變量賦值的時候,本質上就是將值寫在了 f_localsplus 中。并且變量名在符號表中的索引,和變量值在 f_localsplus 中的索引是一致的,因此操作局部變量本質上就是在操作 f_localsplus 數組。

至于 locals() 或者說局部名字空間,它是基于符號表和 f_localsplus 動態創建的。為了方便我們獲取已存在的局部變量,執行 locals() 會臨時創建一個字典。

所以我們通過 locals() 獲取局部名字空間之后,訪問里面的局部變量是可以的,只不過此時將靜態訪問變成了動態訪問。

def foo():
    name = "satori"
    # 會從 f_localsplus 中靜態查找
    print(name)
    # 先基于已有的變量和值創建一個字典
    # 然后通過字典實現變量的動態查找
    print(locals()["name"])

foo()
"""
satori
satori
"""

兩種方式都是可以的,但基于 locals() 來訪問,在效率上明顯會低一些。

另外基于 locals() 訪問一個變量是可以的,但無法創建一個變量。

def foo():
    name = "satori"
    locals()["age"] = 17
    try:
        print(age)
    except NameError as e:
        print(e)

foo()
"""
name 'age' is not defined
"""

局部變量是靜態存儲在數組里的,locals() 只是做了一個拷貝而已。往局部名字空間里面添加一個鍵值對,不等于創建一個局部變量,因為局部變量不是從它這里查找的,因此代碼中打印 age 報錯了。但如果外部還有一個全局變量 age 的話,那么會打印全局變量 age。

然后再補充一點,我們說全局名字空間在任何地方都是唯一的,而對于函數而言,它的局部名字空間在整個函數內部也是唯一的。不管調用 locals 多少次,拿到的都是同一個字典。

def foo():
    name = "satori"
    # 執行 locals() 的時候,內部只有一個鍵值對
    d = locals()
    print(d)  # {'name': 'satori'}
    # 再次獲取,此時有兩個鍵值對
    print(locals())  # {'name': 'satori', 'd': {...}}
    
    # 但兩者的 id 相同,因為一個函數只有一個局部名字空間
    # 不管調用多少次 locals(),拿到的都是同一個字典
    print(id(d) == id(locals()))  # True

foo()

所以 locals() 和 globals() 指向的名字空間都是唯一的,只不過 locals() 是在某個函數內部唯一,而 globals() 在所有地方都唯一。

因此局部名字空間初始為 NULL,但在第一次執行 locals() 時,會以符號表中的符號作為 key,f_localsplus 中的值作為 value,創建一個字典作為函數的局部名字空間。而后續再執行 locals() 的時候,由于名字空間已存在,就不會再次創建了,直接基于當前的局部變量對字典進行更新即可。

def foo():
    # 創建一個字典,由于當前還沒有定義局部變量,因此是空字典
    print(locals())
    """
    {}
    """
    # 往局部名字空間添加一個鍵值對
    locals()["a"] = "b"
    print(locals())
    """
    {'a': 'b'}
    """
    # 定義一個局部變量
    name = "satori"
    # 由于局部名字空間已存在,因此不會再次創建
    # 直接將局部變量的名字作為 key、值作為 value,拷貝到字典中
    print(locals())
    """
    {'a': 'b', 'name': 'satori'}
    """

foo()

注意:雖然局部名字空間里面存在 "a" 這個 key,但 a 這個局部變量是不存在的。


圖片


local 名字空間的創建過程


目前我們已經知道 local 名字空間是怎么創建的了,也熟悉了它的特性,下面通過源碼來看一下它的構建過程。

// Python/bltinmodule.c
static PyObject *
builtin_locals_impl(PyObject *module)
{
    // Python 內置函數的源碼實現位于 bltinmodule.c 中
    // 這里又調用了 _PyEval_GetFrameLocals
    return _PyEval_GetFrameLocals();
}

// Python/ceval.c
PyObject *
_PyEval_GetFrameLocals(void)
{
    PyThreadState *tstate = _PyThreadState_GET();
     _PyInterpreterFrame *current_frame = _PyThreadState_GetFrame(tstate);
    if (current_frame == NULL) {
        _PyErr_SetString(tstate, PyExc_SystemError, "frame does not exist");
        return NULL;
    }
    // 調用了 _PyFrame_GetLocals
    return _PyFrame_GetLocals(current_frame, 1);
}

所以核心邏輯位于 _PyFrame_GetLocals 函數中,來看一下它的邏輯。

// Object/frameobject.c
PyObject *
_PyFrame_GetLocals(_PyInterpreterFrame *frame, int include_hidden)
{
    // 獲取局部名字空間
    PyObject *locals = frame->f_locals;
    // 如果為 NULL,那么創建一個新字典,作為名字空間
    // 所以局部名字空間只會創建一次,后續不會再創建
    if (locals == NULL) {
        locals = frame->f_locals = PyDict_New();
        if (locals == NULL) {
            return NULL;
        }
    }
    PyObject *hidden = NULL;

    // 在 Include/internal/pycore_code.h 里面有 4 個宏
    /* #define CO_FAST_HIDDEN  0x10
     * #define CO_FAST_LOCAL   0x20
     * #define CO_FAST_CELL    0x40
     * #define CO_FAST_FREE    0x80
     */
    // 它們分別對應隱藏變量、局部變量、cell 變量、free 變量
    // 所謂隱藏變量,指的就是解析式里的臨時變量,比如列表解析式
    // 解析式具有獨立的作用域,里面的臨時變量不會污染外部的作用域
    // 所以一般我們也不會關注這些隱藏變量,locals() 也不會返回它
    // 但如果你真的關注,那么可以將 include_hidden 指定為真
    // 那么調用 locals() 時,這些隱藏變量也會一塊兒返回
    if (include_hidden) {
        // 單獨創建一個字典,負責保存隱藏變量
        hidden = PyDict_New();
        if (hidden == NULL) {
            return NULL;
        }
    }
    // 初始化 free 變量,這個和閉包有關
    // 關于閉包,等剖析完函數之后會說,這里暫時先不關注
    frame_init_get_vars(frame);

    PyCodeObject *co = frame->f_code;
    // co_nlocalsplus 等于局部變量、cell 變量、free 變量的個數之和
    // 這些變量都要拷貝到 local 名字空間中
    for (int i = 0; i < co->co_nlocalsplus; i++) {
        PyObject *value;
        // 獲取 f_localsplus[i],在函數內部會對 value 進行修改
        if (!frame_get_var(frame, co, i, &value)) {
            continue;
        }
        // f_localsplus[i] 對應局部名字空間的 value
        // 那么 co_localsplusnames[i] 顯然對應局部名字空間的 key
        // 估計有人已經忘記 co_localsplusnames 字段的含義了,我們再解釋一下
        /* co_localsplusnames:包含所有局部變量、cell 變量、free 變量的名稱
         * co_nlocalsplus:co_localsplusnames 的長度,或者說這些變量的個數之和

         * co_varnames:包含所有局部變量的名稱,co_nlocals:局部變量的個數
         * co_cellvars:包含所有 cell 變量的名稱,co_ncellvars:cell 變量的個數
         * co_freevars:包含所有 free 變量的名稱,co_nfreevars:free 變量的個數
         
         * 因此不難得出它們之間的關系:
         * co_localsplusnames = co_varnames + co_cellvars + co_freevars
         * co_nlocalsplus = co_nlocals + co_ncellvars + co_nfreevars
         */
        // 所以 co_localsplusnames 也是符號表,并且是 co_varnames 的超集
        PyObject *name = PyTuple_GET_ITEM(co->co_localsplusnames, i);
        // 到此局部名字空間的 key 和 value 便有了,但還要做一個判斷
        _PyLocals_Kind kind = _PyLocals_GetKind(co->co_localspluskinds, i);
        // 如果變量的類型是隱藏變量,那么添加到 hidden 中
        // 所以 co_localsplusnames 其實還包含了隱藏變量的名稱
        // 但我們基本不會遇到這種情況,因此關于隱藏變量直接忽略掉即可
        if (kind & CO_FAST_HIDDEN) {
            if (include_hidden && value != NULL) {
                if (PyObject_SetItem(hidden, name, value) != 0) {
                    goto error;
                }
            }
            continue;
        }
        // 如果不是隱藏變量,那么拷貝到 locals 中,但這里有一個判斷很重要
        // 當 value 為 NULL 時,如果 key 已存在,那么會將它刪掉
        // 關于這里的玄機,稍后會解釋
        if (value == NULL) {
            if (PyObject_DelItem(locals, name) != 0) {
                if (PyErr_ExceptionMatches(PyExc_KeyError)) {
                    PyErr_Clear();
                }
                else {
                    goto error;
                }
            }
        }
        // 到這里說明 value 指向了一塊合法的內存
        // 也就是變量名和變量值已經完成了綁定,那么將它們添加到 locals 中
        else {
            if (PyObject_SetItem(locals, name, value) != 0) {
                goto error;
            }
        }
        // 繼續遍歷下一個符號
    }
    // 隱藏變量保存在 hidden 中,它不會污染 f_locals
    if (include_hidden && PyDict_Size(hidden)) {
        // 創建一個新字典
        PyObject *innerlocals = PyDict_New();
        if (innerlocals == NULL) {
            goto error;
        }
        // 合并 locals
        if (PyDict_Merge(innerlocals, locals, 1) != 0) {
            Py_DECREF(innerlocals);
            goto error;
        }
        // 合并 hidden
        if (PyDict_Merge(innerlocals, hidden, 1) != 0) {
            Py_DECREF(innerlocals);
            goto error;
        }
        // 重新賦值給 locals,所以返回的結果會包含 hidden 里的鍵值對
        // 但 f_locals 里面是沒有隱藏變量的
        locals = innerlocals;
    }
    else {
        Py_INCREF(locals);
    }
    Py_CLEAR(hidden);
    // 返回 locals
    return locals;

  error:
    Py_XDECREF(hidden);
    return NULL;
}

所以邏輯非常簡單,如果不考慮隱藏變量(也不需要考慮),那么整個過程就是我們剛才說的:遍歷符號表和 f_localsplus,將變量名和變量值組成鍵值對拷貝到字典中。

但里面有一處細節非常關鍵。

圖片

當變量值為 NULL 時,說明在獲取名字空間時,該變量還沒有被賦值。要是此時變量已經在局部名字空間中,那么會將它從名字空間中刪掉。這一處非常關鍵,在介紹 exec 的時候你就會明白。


圖片


local 名字空間與 exec 函數


我們再來搭配 exec 關鍵字,結果會更加明顯。首先 exec 函數可以將一段字符串當成代碼來執行,并將執行結果體現在當前的名字空間中。

def foo():
    print(locals())  # {}
    exec("x = 1")
    print(locals())  # {'x': 1}
    try:
        print(x)
    except NameError as e:
        print(e)  # name 'x' is not defined
        
foo()

盡管 locals() 變了,但是依舊訪問不到 x,因為虛擬機并不知道 exec("x = 1") 是創建一個局部變量,它只知道這是一個函數調用。

事實上 exec 會作為一個獨立的編譯單元來執行,并且有自己的作用域。

所以 exec("x = 1") 執行完之后,效果就是改變了局部名字空間,里面多了一個 "x": 1 鍵值對。但關鍵的是,局部變量 x 不是從局部名字空間中查找的,exec 終究還是錯付了人。

由于函數 foo 對應的 PyCodeObject 對象的符號表中并沒有 x 這個符號,所以報錯了。

補充:exec 默認影響的是 local 名字空間,如果在執行時發現 local 名字空間為 NULL,那么會自動創建一個。所以調用 exec 也可以創建名字空間(當它為 NULL 時)。

exec("x = 1")
print(x)  # 1

如果放在模塊里面是可以的,因為模塊的 local 名字空間和 global 名字空間指向同一個字典,所以 global 名字空間會多一個 key 為 "x" 的鍵值對。而全局變量是從 global 名字空間中查找的,所以這里沒有問題。

def foo():
    # 此時 exec 影響的是全局名字空間
    exec("x = 123", globals())
    # 這里不會報錯, 但此時的 x 不是局部變量, 而是全局變量
    print(x)

foo()
print(x)
"""
123
123
"""

可以給 exec 指定要影響的名字空間,代碼中 exec 影響的是全局名字空間,打印的 x 也是全局變量。

以上幾個例子都比較簡單,接下來我們開始上強度了。

def foo():
    exec("x = 1")
    print(locals()["x"])

foo()
"""
1
"""

def bar():
    exec("x = 1")
    print(locals()["x"])
    x = 123

bar()
"""
Traceback (most recent call last):
  File .....
    bar()
  File .....
    print(locals()["x"])
KeyError: 'x'
"""

這是什么情況?函數 bar 只是多了一行賦值語句,為啥就報錯了呢?其實背后的原因我們之前分析過。

1)函數的局部變量在編譯的時候已經確定,并存儲在對應的 PyCodeObject 對象的符號表中,這是由語法規則所決定的;

2)函數內的局部變量在其整個作用域范圍內都是可見的;

對于 foo 函數來說,exec 執行完之后相當于往 local 名字空間中添加一個鍵值對,這沒有問題。對于 bar 函數而言也是如此,在執行完 exec("x = 1") 之后,local 名字空間也會存在 "x": 1 這個鍵值對,但問題是下面執行 locals() 的時候又把字典更新了。

因為局部變量可以在函數的任意位置創建,或者修改,所以每一次執行 locals() 的時候,都會遍歷符號表和 f_localsplus,然后組成鍵值對拷貝到名字空間中。

在 bar 函數里面有一行 x  = 123,所以知道函數里面存在局部變量 x,符號表里面也會有 "x" 這個符號,這是在編譯時就確定的。但我們是在 x = 123 之前調用的 locals,所以此時符號 x 在 f_localsplus 中對應的值還是一個 NULL,沒有指向一個合法的 PyObject。換句話說就是,知道里面存在局部變量 x,但此時尚未賦值。

然后在更新名字空間的時候,如果發現值是個 NULL,那么就把名字空間中該變量對應的鍵值對給刪掉。

圖片圖片

所以 bar 函數執行 locals()["x"] 的時候,會先獲取名字空間,原本里面是有 "x": 1 這個鍵值對的。但因為賦值語句 x = 123 的存在,導致符號表里面存在 "x" 這個符號,可執行 locals() 的時候又尚未完成賦值,因此值為 NULL,于是又把這個鍵值對給刪掉了。所以執行 locals()["x"] 的時候,出現了 KeyError。

因為局部名字空間體現的是局部變量的值,而調用 locals 的時候,局部變量 x 還沒有被創建。所以 locals() 里面不應該存在 key 為 "x" 的鍵值對,于是會將它刪除。

我們將名字空間打印一下:

def foo():
    # 創建局部名字空間,并寫入鍵值對 "x": 1
    # 此時名字空間為 {"x": 1}
    exec("x = 1")
    # 獲取名字空間,會進行更新
    # 但當前不存在局部變量,所以名字空間仍是 {"x": 1}
    print(locals())

def bar():
    # 創建局部名字空間,并寫入鍵值對 "x": 1
    # 此時名字空間為 {"x": 1}
    exec("x = 1")
    # 獲取名字空間,會進行更新
    # 由于里面存在局部變量 x,但尚未賦值
    # 于是將字典中 key 為 "x" 的鍵值對給刪掉
    # 所以名字空間變成了 {}
    print(locals())
    x = 123


foo()  # {'x': 1}
bar()  # {}

上面代碼中,局部變量的創建發生在 exec 之后,如果發生在 exec 之前也是類似的結果。

def foo():
    exec("x = 2")
    print(locals())

foo()  # {'x': 2}


def bar():
    x = 1
    exec("x = 2")
    print(locals())

bar()  # {'x': 1}

在 exec("x = 2") 執行之后,名字空間也變成了 {"x": 2}。但每次調用 locals,都會對字典進行更新,所以在 bar 函數里面獲取名字空間的時候,又把 "x" 對應的 value 給更新回來了。

當然這是在變量沖突的情況下,會保存真實存在的局部變量的值。如果不沖突,比如 bar 函數里面是 exec("y = 2"),那么 locals() 里面就會存在兩個鍵值對,但只有 x 才是真正的局部變量,而 y 則不是。

將 exec("x = 2") 換成 locals()["x"] = 2 也是一樣的效果,它們都是往局部名字空間中添加一個鍵值對,但不會創建一個局部變量。

薛定諤的貓

當 Python 中混進一只薛定諤的貓……,這是《Python 貓》在 19 年更新的一篇文章,里面探討的內容和我們本文的主題是重疊的。貓哥在文章中舉了幾個疑惑重重的例子,看看用上面學到的知識能不能合理地解釋。
# 例 0
def foo():
    exec('y = 1 + 1')
    z = locals()['y']
    print(z)

foo()
# 輸出:2


# 例 1
def foo():
    exec('y = 1 + 1')
    y = locals()['y']
    print(y)

foo()
# 報錯:KeyError: 'y'

以上是貓哥文章中舉的示例,首先例 0 很簡單,因為 exec 影響了所在的局部名字空間,里面存在 "y": 2 這個鍵值對,所以 locals()["y"] 會返回 2。

但例 1 則不同,因為 Python 在語法解析的時候發現了 y  = ... 這樣的賦值語句,那么它在編譯的時候就知道函數里面存在 y 這個局部變量,并寫入符號表中。既然符號表中存在,那么調用 locals 的時候就會寫入到名字空間中。但問題是變量 y 的值是多少呢?由于對 y 賦值是發生在調用 locals 之后,所以在調用 locals 的時候,y 的值還是一個 NULL,也就是變量還沒有賦值。所以會將名字空間中的 "y": 2 這個鍵值對給刪掉,于是報出 KeyError 錯誤。

再來看看貓哥文章的例 2:

# 例 2
def foo():
    y = 1 + 1
    y = locals()['y']
    print(y)

foo()
# 2

locals() 是對真實存在的局部變量的一個拷貝,在調用 locals 之前 y 就已經創建好了。符號表里面有 "y",數組 f_localsplus 里面有數值 2,所以調用 locals() 的時候,會得到 {"y": 2},因此函數執行正常。

貓哥文章的例 3:

# 例3
def foo():
    exec('y = 1 + 1')
    boc = locals()
    y = boc['y']
    print(y)

foo()
# KeyError: 'y'

這個例3 和例 1 是一樣的,只不過用變量 boc 將局部名字空間保存起來了。執行 exec 的時候,會創建局部名字空間,寫入鍵值對 "y": 2。

但調用 locals 的時候,發現函數內部存在局部變量 y 并且還尚未賦值,于是又會將 "y": 2 這個鍵值對給刪掉,因此 boc 變成了一個空字典。于是執行 y = boc["y"] 的時候會出現 KeyError。

貓哥文章的例 4:

# 例4
def foo():
    boc = locals()
    exec('y = 1 + 1')
    y = boc['y']
    print(y)

foo()
# 2

顯然在調用 locals 的時候,會返回一個空字典,因為此時的局部變量都還沒有賦值。但需要注意的是:boc 已經指向了局部名字空間(字典),而局部名字空間在一個函數里面也是唯一的。

然后執行 exec("y = 1 + 1"),會往局部名字空間中寫入一個鍵值對,而變量 boc 指向的字典也會發生改變,因為是同一個字典,所以程序正常執行。

貓哥文章的例 5:

# 例5
def foo():
    boc = locals()
    exec('y = 1 + 1')
    print(locals())
    y = boc['y']
    print(y)

foo()

# {'boc': {...}} 
# KeyError: 'y'

首先在執行 boc = locals() 之后,boc 會指向一個空字典,然后 exec 函數執行之后會往字典里面寫入一個鍵值對 "y": 2。如果在 exec 執行之后,直接執行 y = boc["y"],那么代碼是沒有問題的,但問題是在中間插入了一個 print(locals())。

我們說過,當調用 locals 的時候,會對名字空間進行更新,然后返回更新之后的名字空間。由于函數內部存在 y = ... 這樣的賦值語句,所以符號表中就存在 "y" 這個符號,于是會進行更新。但更新的時候,發現 y 還沒有被賦值,于是又將字典中的鍵值對 "y": 2 給刪掉了。

由于局部名字空間只有一份,所以 boc 指向的字典也會發生改變,換句話說在 print(locals()) 之后,boc 就指向了一個空字典,因此出現 KeyError。

小結

以上我們就探討了局部變量的存儲原理以及它和 local 名字空間的關系。

  • 局部變量在編譯時就已經確定,所以會采用數組靜態存儲,并且在整個作用域內都是可見的。
  • f_localsplus 的內存被分成了四份,局部變量的值便存儲在第一份空間中。
  • 局部名字空間是對真實存在的局部變量的拷貝,調用 locals() 時,會遍歷得到每一個符號和與之綁定的值,然后拷貝到局部名字空間。
  • 如果遍歷時發現變量值為 NULL,這就說明獲取名字空間時,該變量尚未賦值,那么要將它從名字空間中刪掉。
責任編輯:武曉燕 來源: 古明地覺的編程教室
相關推薦

2024-07-09 08:35:09

2024-05-22 08:02:30

2024-09-20 14:46:49

Python函數編譯

2018-05-14 09:15:24

Python變量函數

2009-09-17 13:05:38

Linq局部變量類型

2012-07-11 23:10:49

SQL Server數據庫

2015-09-18 13:08:36

更新RedstoneWindows 10

2020-11-11 21:26:48

函數變量

2009-08-26 16:37:07

C#迭代器局部變量

2010-03-15 09:32:56

Python函數

2024-05-08 08:38:02

Python變量對象

2024-05-29 08:49:22

Python全局變量局部變量

2023-03-26 00:04:14

2015-01-07 14:41:32

Android全局變量局部變量

2009-10-12 14:13:00

VB.NET使用局部變

2009-09-11 10:07:05

Linq隱式類型化局部

2009-12-15 10:48:54

Ruby局部變量

2020-10-26 07:07:50

線程安全框架

2017-02-08 12:28:37

Android變量總結

2024-10-14 11:14:38

Python變量靜態
點贊
收藏

51CTO技術棧公眾號

搡老熟女老女人一区二区| 色姑娘综合av| 国产福利久久久| 久久99国产成人小视频| 一本一道久久a久久精品| 日韩在线第一区| 国产精品丝袜黑色高跟鞋| 国产精品videosex极品| 亚洲欧美国产精品久久久久久久 | 国产欧美韩国高清| 欧美成人精品欧美一级私黄| 欧美日韩一本| 91精品国产手机| 91视频最新入口| 黄色视屏免费在线观看| 91一区二区在线观看| 91精品久久久久久久久青青| 日韩精品久久久久久久| 91亚洲国产成人久久精品| 亚洲国产日韩精品在线| 亚洲美女爱爱视频| 欧美成人免费电影| 亚洲一区二区三区精品在线| 四虎影院一区二区三区 | 日韩av一卡二卡| 国产又黄又猛的视频| 是的av在线| 一区二区三区在线免费视频| 日韩一区二区三区高清| 天天综合网在线| 毛片av中文字幕一区二区| 不卡av电影院| 日本黄色小视频在线观看| 国产丝袜一区| 欧美一卡二卡三卡| 欧美成人三级在线播放| 亚洲精品中文字幕| 五月开心婷婷久久| 国产在线无码精品| 麻豆网站在线| 欧美极品少妇xxxxⅹ高跟鞋| 蜜桃视频在线观看91| 亚洲国产精品一| 国产一区二区三区在线观看免费视频| 国产精品扒开腿做爽爽爽视频| 日韩成人在线免费视频| 欧美日韩成人| 欧美猛少妇色xxxxx| 性生交大片免费全黄| 全球成人免费直播| 国产香蕉一区二区三区在线视频| 亚洲av无码一区二区三区网址 | 熟女高潮一区二区三区| 牛牛影视久久网| 亚洲第一级黄色片| 中文在线字幕观看| 97视频一区| 日韩无一区二区| 99re6在线观看| 亚洲欧美专区| 91精品国产欧美一区二区18| 一级淫片在线观看| 免费精品一区| 日韩精品一区国产麻豆| 第一页在线视频| 成人盗摄视频| 亚洲激情自拍图| 国产又黄又粗又猛又爽的视频| 欧美黑人巨大videos精品| 日韩国产高清污视频在线观看| 人妻丰满熟妇aⅴ无码| 亚洲区小说区| 尤物精品国产第一福利三区| 五月天婷婷丁香网| 亚洲情侣在线| 欧美激情啊啊啊| 亚洲一二三四五| 综合中文字幕| 亚洲精美色品网站| 9.1成人看片免费版| av影片在线一区| 久久精品视频在线| 国产一级生活片| 久久精品国语| 91精品久久久久久久久青青| 国产成人精品毛片| 99这里只有久久精品视频| 日本高清不卡一区二区三| 日本在线观看www| 一区二区三区欧美久久| 日本在线xxx| 日韩制服一区| 欧美成人女星排行榜| 欧美高清性xxxx| 日本不卡电影| 久久久久久久香蕉网| 国产一区免费看| 国产麻豆视频一区| 麻豆av福利av久久av| 蜜桃av在线免费观看| 亚洲福中文字幕伊人影院| 欧美成人精品欧美一级乱| 国产色99精品9i| 亚洲欧美综合v| 欧美精品成人久久| 免费在线视频一区| 国产一区二区在线观看免费播放| 九九在线视频| 亚洲综合免费观看高清完整版在线| 国产午夜伦鲁鲁| 精品一区二区三区四区五区 | 欧洲杯什么时候开赛| 久久91精品国产91久久跳| www.国产毛片| 成人av在线资源网站| 国产精品99久久久久久大便| 一本大道色婷婷在线| 日韩午夜在线播放| www中文在线| 亚洲综合激情| 午夜精品久久久久久久99| 欧美区亚洲区| 国产精品电影一区| 黑人乱码一区二区三区av| 欧美极品xxx| 欧美深夜福利视频| 精品一区二区三区中文字幕视频 | 精品香蕉视频| 久久久噜噜噜久久中文字免| 国产精品自产拍| 国产喂奶挤奶一区二区三区| 免费看黄在线看| 精品一区二区三区视频在线播放| 国产亚洲精品久久久久动| 日韩伦理在线视频| 国产成人av资源| 中文字幕乱码免费| 国产亚洲人成a在线v网站| 亚洲男人天堂视频| 综合激情网五月| 97久久久精品综合88久久| 国产爆乳无码一区二区麻豆| 91久久青草| 色婷婷综合成人| 中文区中文字幕免费看| 久久噜噜亚洲综合| 国产成人久久婷婷精品流白浆| 国产96在线亚洲| 欧美激情视频在线免费观看 欧美视频免费一 | 刘亦菲久久免费一区二区| 亚洲欧美乱综合| 欧美一级免费在线| 亚洲国产一区二区三区在线播放 | 一卡二卡在线视频| 1000部国产精品成人观看| av中文字幕网址| 在线电影一区二区| 91视频婷婷| 欧美1234区| 亚洲国产一区二区三区四区| 中日韩精品视频在线观看| 91污在线观看| 91在线视频观看免费| 大片网站久久| 亚洲a成v人在线观看| 国产三级国产精品国产专区50| 国产精品qvod| 欧美性受xxxx黑人猛交| 香港三日本三级少妇66| 一本色道久久综合亚洲aⅴ蜜桃| 99久久国产精| 日韩国产欧美一区二区三区| 色涩成人影视在线播放| 91精品一区| 欧美精品videosex极品1| 天天操天天插天天射| 色视频成人在线观看免| 国产第一页精品| 日韩av字幕| 国产一区二区三区视频免费| 中文永久免费观看| 中文字幕永久在线不卡| 亚洲av无一区二区三区久久| 136国产福利精品导航网址| 国产黄色高清视频| 亚洲色图视频网| 日本女人性视频| 一本一本久久| 亚洲精品国产精品国自产观看| 亚洲日本中文| 午夜精品免费视频| 国产69精品久久app免费版| 欧美日韩二区三区| 日韩欧美亚洲国产| 欧美韩国日本综合| 日韩精品――色哟哟| 美女诱惑一区| a级网站在线观看| 国偷自产视频一区二区久| 国产成人精品电影久久久| 麻豆网站在线看| 精品丝袜一区二区三区| 国产又黄又粗又猛又爽| 精品日本美女福利在线观看| 免费黄色在线网址| 成人黄页毛片网站| 91国内在线播放| 亚洲免费网站| 日本免费a视频| 国产91一区| 国产嫩草一区二区三区在线观看 | 午夜精品三级视频福利| 素人av在线| 国产视频精品一区二区三区| 国产精品自拍电影| 在线欧美日韩精品| 日韩精品一区二区三| 最新日韩av在线| 色欲AV无码精品一区二区久久| 国产成人精品网址| 日本美女视频一区| 视频一区二区三区中文字幕| 欧美图片激情小说| 91精品国产调教在线观看| 欧美一级日本a级v片| 丁香5月婷婷久久| 亚洲综合av影视| 欧美视频免费看| 国产精品国内视频| 亚洲成人短视频| 欧美野外猛男的大粗鳮| 91av久久| 欧美激情亚洲视频| 性直播体位视频在线观看| 日韩在线视频观看| 97最新国自产拍视频在线完整在线看| 日韩精品极品毛片系列视频| 超碰福利在线观看| 91精品国产一区二区| 97在线视频人妻无码| 欧美日韩国产一二三| 国产情侣小视频| 色综合久久综合中文综合网| 日韩 欧美 中文| 欧美日韩性视频在线| 日本熟妇乱子伦xxxx| 五月天国产精品| 国产成人精品一区二三区| 午夜亚洲福利老司机| 日本一级淫片免费放| 亚洲国产日日夜夜| 日韩黄色三级视频| 五月婷婷激情综合网| 中日韩黄色大片| 欧美性生交大片免网| 亚洲欧美偷拍一区| 在线看一区二区| 欧美男人天堂网| 欧美日韩免费一区二区三区视频| 亚洲熟女乱色一区二区三区久久久| 欧美视频一区二区三区四区 | 久久蜜桃精品| 老熟妇仑乱视频一区二区| 日韩av网站在线观看| 欧美wwwwwww| 国产一区二区三区四| 亚洲妇女无套内射精| 99久久精品情趣| xxxx日本黄色| 亚洲欧美综合网| 精品无码m3u8在线观看| 精品动漫一区二区| 亚洲中文无码av在线| 欧美福利视频一区| 亚洲国产成人精品一区二区三区| 亚洲国产成人一区| 久草视频视频在线播放| 日韩一区二区福利| 美女精品导航| 日本久久久久久久久久久| 嫩草伊人久久精品少妇av杨幂| 91色p视频在线| 国产精品色在线网站| 欧美日韩在线播放一区二区| 日韩精品2区| 久久成人福利视频| 水蜜桃久久夜色精品一区的特点| 天堂中文av在线| 成人av高清在线| 51妺嘿嘿午夜福利| 亚洲乱码中文字幕综合| 国产成人在线观看网站| 欧美日韩一区高清| 刘亦菲毛片一区二区三区| 国产一区二区动漫| 欧美人与牲禽动交com| 欧美最顶级丰满的aⅴ艳星| 婷婷久久综合九色综合99蜜桃| 国产精品免费在线播放| 久久福利影院| 日本少妇高潮喷水视频| 激情综合亚洲精品| 美国黄色一级毛片| 亚洲欧美另类久久久精品 | 欧美性xxxxxx少妇| www.中文字幕| 尤物yw午夜国产精品视频明星| 大桥未久在线播放| 成人国产在线视频| 久久99国内| 精品视频在线观看一区| 精品一区二区av| 影音先锋制服丝袜| 天天综合网 天天综合色| 国产免费福利视频| 中文字幕在线日韩| 午夜影院在线观看国产主播| 亚洲www视频| 成人国产精品一级毛片视频| 精品无码国模私拍视频| 国产激情一区二区三区| 国产亚洲精品久久久久久豆腐| 一本久道久久综合中文字幕| 欧美 日韩 综合| 欧美精品在线极品| 成人动漫视频在线观看| 午夜精品美女久久久久av福利| 国产精品日韩精品欧美精品| 国产无套精品一区二区三区| 国产精品久久久久久久久动漫| 中文字幕手机在线视频| 亚洲精品国产电影| 136福利第一导航国产在线| 91青青草免费在线看| 亚洲啊v在线观看| 国产日韩欧美久久| 中文字幕av资源一区| 91porny九色| 亚洲性日韩精品一区二区| 亚洲天堂导航| 欧美激情专区| 久久国产66| 日韩av在线看免费观看| 欧美日韩性视频| 六十路在线观看| 国产成人精品av在线| 欧美一区二区三区高清视频| 97公开免费视频| 国产亚洲欧洲一区高清在线观看| 亚洲AV无码成人精品区东京热| 亚洲毛片在线免费观看| 黑人精品一区| 日本一区二区在线视频| 日本在线不卡视频| 亚洲天堂精品一区| 欧美日韩一级片在线观看| 日本中文字幕视频在线| 成人网欧美在线视频| 欧美fxxxxxx另类| 亚洲av无码一区东京热久久| 亚洲成人av一区| 欧洲视频在线免费观看| 国产精品成人播放| 成人精品电影| 视频区 图片区 小说区| 亚洲国产视频一区二区| 五月天婷婷在线观看| 国产成人鲁鲁免费视频a| 91亚洲人成网污www| 欧美一级大片免费看| 疯狂做受xxxx高潮欧美日本| 极品白浆推特女神在线观看| 国产欧美精品在线| 欧美欧美全黄| aaaaaav| 欧美午夜影院一区| 最爽无遮挡行房视频在线| 久久久久久久久久久久久久一区 | 97人人爽人人| 亚州成人在线电影| 成人亚洲综合天堂| 亚洲综合日韩中文字幕v在线| 99这里有精品| 青青青视频在线播放| 日韩免费一区二区| 自由日本语热亚洲人| 青青草原网站在线观看| 26uuu亚洲综合色欧美| 丰满熟女人妻一区二区三| 欧美成人免费在线观看| 最新亚洲精品| 人妻精品久久久久中文字幕69| 婷婷久久综合九色国产成人 | 激情六月丁香婷婷| 亚洲天堂免费看| 日本a一级在线免费播放| 国产区精品视频| 亚洲一级在线| 欧美成欧美va| 最好看的2019年中文视频|