首頁 > 軟體

Python作用域與名稱空間原始碼學習筆記

2022-05-17 16:01:00

作用域與名稱空間

問題:

PI = 3.14
def circle_area(r):
    return PI * r ** 2
class Person(object):
    def __init__(self, name):
        self.name = name
    def say(self):
        print('i am', self.name)

以這個程式為例,程式碼中出現的每個變數的作用域分別是什麼?程式中總共涉及多少個名稱空間?Python又以怎樣的順序去查詢一個變數呢?

1. 名字繫結

1.1 賦值

在Python中,變數只是一個與實際物件繫結起來的名字,變數定義本質上就是建立名字與物件的約束關係。因此,賦值語句本質上就是建立這樣的約束關係,將右邊的物件與左邊的名字繫結起來:

a = 1

賦值語句是最基本的將名字與物件繫結的方式,除此之外還有很多其他方式都起到了這樣的作用。

1.2 模組匯入

當我們匯入一個模組時,也會在當前上下文建立一個名字,並與被匯入物件繫結。

# 在當前上下文建立一個名字test,與被匯入的module物件繫結
import test

1.3 函數、類定義

# 函數名circle_area與function物件繫結
def circle_area(r):
    return PI * r ** 2
# 類名Person與型別物件繫結
class Person(object):
    def __init__(self):
        pass

1.4 as關鍵字

# 將名字t與module物件繫結
import test as t

2. 作用域

問題:當我們引入一個名字之後,它的可見範圍有多大呢?

a = 1
def func1():
    print(a)  # 1
def func2():
    a = 2
    print(a)  # 2
print(a)  # 1

在不同的程式碼區域引入的名字,其影響範圍是不一樣的。第1行定義的a可以影響到func1,而func2中定義的a則不能。此外,一個名字可能會在多個程式碼區域中定義,但最終在某個程式碼區域中只能使用其中一個。

2.1 靜態作用域

一個名字能夠施加影響的程式正文區域,便是該名字的作用域。在Python中,一個名字在程式中某個區域能否起作用,是由名字引入的位置決定的,而不是執行時動態決定的。因此,Python具有靜態作用域,也稱為詞法作用域。那麼,作用域具體是如何劃分的呢?

2.2 劃分作用域

  • Python在編譯時,根據語法規則將程式碼劃分為不同的程式碼塊,每個程式碼塊形成一個作用域。首先,整個.py檔案構成最頂層的作用域,這就是全域性作用域,也成為模組作用域;其次,當程式碼遇到函數定義,函數體成為當前作用域的子作用域;再者,當程式碼遇到類定義,類定義體成為當前作用域的子作用域。
  • 一個名字在某個作用域引入後,它的影響範圍就被限制在該作用域內。其中,全域性作用域對所有直接或間接內嵌於其中的子作用域可見;函數作用域對其直接子作用域可見,並且可以傳遞。
  • 例子中的作用域的巢狀關係如下:

存取關係如下:

2.3 閉包作用域

閉包的概念:在電腦科學中,閉包,又稱詞法閉包或函數閉包,是參照了自由變數的函數。這個被參照的自由變數將和這個函數一同存在,即使已經離開了創造它的環境也不例外。所以,有另一種說法認為閉包是由函數和與其相關的參照環境組合而成的實體。閉包在執行時可以有多個範例,不同的參照環境和相同的函陣列合可以產生不同的範例

程式碼範例:

>>> pi = 3.14
>>> def closure_print(name: str):
    	def circle_area(r: int):
        	print(name, pi * r * r)
	    return circle_area
>>> circle_area1 = closure_print("circle1: ")
>>> circle_area2 = closure_print("circle2: ")
>>> circle_area1(1)
circle1:  3.14
>>> circle_area2(2)
circle2:  12.56

劃分作用域:

思考:circle_area1和circle_area2函數物件是怎麼拿到name的?

2.4 類作用域

程式碼範例:

>>> language = 'chinese'
>>> class Male:
        gender: str = 'male'
        def __init__(self, name: str):
            self.name = name
        def Speak(self):
            print('i speak', language)
        def Gender(self):
            print('i am', gender)
>>> male = Male('zhangsan')
>>> male.Gender()
Traceback (most recent call last):
  File "<pyshell#11>", line 1, in <module>
    male.Gender()
  File "<pyshell#9>", line 8, in Gender
    print('i am', gender)
NameError: name 'gender' is not defined
>>> male.Speak()
i speak chinese

作用域分析:

全域性作用域對其他所有內嵌其中的作用域均可見,所以在函數Speak()中可以存取到language

類作用域和函數作用域不一樣,它對其子作用域是不可見的,所以在函數Gende()中gender是不可見的

思考:

>>> male.gender
'male'
>>> Male.gender
'male'
>>> male.gender = 'male2'
>>> male.gender
'male2'
>>> Male.gender
'male'

2.5 複雜巢狀

2.5.1 函數巢狀類

在Python中,類可以動態建立,甚至在函數中返回。通過在函數中建立並返回類,可以按函數引數對類進行動態客製化

程式碼範例:

>>> language = 'chinese'
>>> def MakeMale(sSortName: str):
        class Male:
            sortName = sSortName
            def __init__(self, name: str):
                self.name = name
            def Speak(self):
                print('i speak', language)
			def Sort(self):
				print(sSortName)
        return Male
>>> ChineseMale: type = MakeMale('Chinese Men')
>>> chineseMale = ChineseMale('zhangsan')
>>> chineseMale.Speak()
i speak chinese
>>> chineseMale.sortName
Chinese Men
>>> chineseMale.Sort()
Chinese Men

2.5.2 類巢狀類

程式碼範例:

>>> class OutClass:
        inName = 'in'
        class InClass:
            name = inName
Traceback (most recent call last):
  File "<pyshell#26>", line 1, in <module>
    class OutClass:
  File "<pyshell#26>", line 3, in OutClass
    class InClass:
  File "<pyshell#26>", line 4, in InClass
    name = inName
NameError: name 'inName' is not defined

3. 名稱空間

作用域是語法層面的概念,是靜態的。當程式開始執行後,作用域中的名字繫結關係需要儲存起來,儲存的地方就是名稱空間。由於名字繫結關係是由名字和物件組成的鍵值對,因此用dict是理想的儲存容器(之前在介紹dict的相關內容時也有提到)

以計算圓面積的例子來認識作用域背後的執行時實體——名稱空間。程式碼範例如下:

>>> PI = 3.14
>>> def closure_print(name: str):
    	def circle_area(r: int):
        	print(name, PI * r * r)
	    return circle_area

3.1 Globals

在Python中,每個模組都有一個dict物件,用於儲存全域性作用域中的名字,這就是全域性名稱空間Globals。在上述的例子中,根據我們之前對作用域的劃分,可以肯定全域性名稱空間中一定包含兩個名字:PI和closure_print。

如果其他模組也需要使用PI或closure_print函數,就需要通過import語句將模組匯入,匯入後我們就可以獲得一個模組物件:

# 假設我們在test.py中匯入上述模組testglobal.py
>>> import testglobal
>>> testglobal
<module 'testglobal' from 'D:\myspace\code\pythonCode\mix\namespace\testglobal.py'>
>>> type(testglobal)
<class 'module'>

通過內建函數dir()我們可以知道模組物件下有哪些屬性可以存取:

>>> dir(testglobal)
['PI', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'closure_print']
>>> testglobal.closure_print
<function closure_print at 0x000002F33B14A050>

在Python中,一個物件可以存取哪些屬性,成為物件的屬性空間。因此,模組的屬性空間和全域性名稱空間本質上就是同一個東西,都通過一個dict物件進行儲存。那麼如何找到這個dict物件呢——通過__dict__屬性:

>>> testglobal.__dict__

此外,我們也可以通過內建函數globals()來獲取當前模組的全域性名稱空間:

>>> globals()

我們分別列印它們的id,本質上就是同一個物件:

>>> id(testglobal.__dict__)
2219833831040
>>> id(globals())
2219833831040

3.2 Locals

Python執行一個作用域內的程式碼時,需要一個容器來存取當前作用域的名字,這就是區域性名稱空間Locals

當Python執行closure_print()函數時,將分配一個棧幀物件PyFrameObject來儲存上下文資訊以及執行狀態。作為程式碼執行時必不可少的上下文資訊,全域性名稱空間和區域性名稱空間也會在PyFrameObject上記錄:

struct _frame {
    PyObject_VAR_HEAD
    struct _frame *f_back;      /* previous frame, or NULL */
    PyCodeObject *f_code;       /* code segment */
    PyObject *f_builtins;       /* builtin symbol table (PyDictObject) */
    PyObject *f_globals;        /* global symbol table (PyDictObject) */
    PyObject *f_locals;         /* local symbol table (any mapping) */
    PyObject **f_valuestack;    /* points after the last local */
    PyObject *f_trace;          /* Trace function */
    int f_stackdepth;           /* Depth of value stack */
    char f_trace_lines;         /* Emit per-line trace events? */
    char f_trace_opcodes;       /* Emit per-opcode trace events? */
    /* Borrowed reference to a generator, or NULL */
    PyObject *f_gen;
    int f_lasti;                /* Last instruction if called */
    int f_lineno;               /* Current line number. Only valid if non-zero */
    int f_iblock;               /* index in f_blockstack */
    PyFrameState f_state;       /* What state the frame is in */
    PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
    PyObject *f_localsplus[1];  /* locals+stack, dynamically sized */
};

3.3 Enclosings

在作用域存在巢狀的情況下,Python將內層程式碼塊依賴的所有外層名字儲存在一個容器內,這就是閉包名稱空間Enclosings

對於範例:

>>> pi = 3.14
>>> def closure_print(name: str):
    	def circle_area(r: int):
            name = 1
        	print(name, pi * r * r)
	    return circle_area

當Python執行到print(name, pi * r * r)語句時,按照Locals、Enclosings、Globals這樣的順序查詢語句中涉及的名字:名字name在Enclosings中找到,名字pi在Globals中找到,名字r在Locals中找到。那麼還有一個名字print是如何找到的呢?

3.4 Builtin

Python在builtin模組中提供了很多內建函數和型別,構成執行時的另一個名稱空間:內建名稱空間Builtin

全域性名稱空間中有一個名字指向內建名稱空間:

>>> import builtins
>>> id(testglobal.__builtins__)
3065787874688
>>> id(builtins.__dict__)
3065787874688

4. 問題與總結

函數作用域對內部所有的作用域均可見,包括內部巢狀的類作用域和函數作用域(例如閉包);類作用域對內部所有的作用域均不可見,包括內部巢狀的類作用域和函數作用域。

“只要在當前Locals名稱空間中無同名變數且沒有global,nonlocal等關鍵字的宣告的話,就一定建立一個該名字的新區域性變數”,以nonlocal的使用為例:

範例1:

>>> def closure_print(name: str):
        def circle_area(r: int):
            print(locals())
            print(name, PI * r * r)
        return circle_area
>>> c = closure_print('circle1')
>>> c(1)
{'r': 1, 'name': 'circle1'}
circle1 3.14

範例2:

>>> PI = 3.14
>>> def closure_print(name: str):
        def circle_area(r: int):
            print(locals())
            name += '1'
            print(name, PI * r * r)
        return circle_area
>>> c = closure_print('circle1')
>>> c(1)
{'r': 1}
Traceback (most recent call last):
  File "<pyshell#4>", line 1, in <module>
    c(1)
  File "<pyshell#2>", line 4, in circle_area
    name += '1'
UnboundLocalError: local variable 'name' referenced before assignment

範例3:

>>> PI = 3.14
>>> def closure_print(name: str):
        def circle_area(r: int):
            print(locals())
            name = 'circle2'
            print(locals())
            print(name, PI * r * r)
        return circle_area
>>> c = closure_print('circle1')
>>> c(1)
{'r': 1}
{'r': 1, 'name': 'circle2'}
circle2 3.14

範例4:

>>> PI = 3.14
>>> def closure_print(name: str):
        def circle_area(r: int):
            print(locals())
            nonlocal name
            name += '1'
            print(locals())
            print(name, PI * r * r)
        return circle_area
>>> c = closure_print('circle1')
>>> c(1)
{'r': 1, 'name': 'circle1'}
{'r': 1, 'name': 'circle11'}
circle11 3.14

locals()輸出的到底是什麼?C原始碼如下:

int
PyFrame_FastToLocalsWithError(PyFrameObject *f)
{
    /* Merge fast locals into f->f_locals */
    PyObject *locals, *map;
    PyObject **fast;
    PyCodeObject *co;
    Py_ssize_t j;
    Py_ssize_t ncells, nfreevars;
    if (f == NULL) {
        PyErr_BadInternalCall();
        return -1;
    }
    // 初始賦值locals為f->f_locals
    locals = f->f_locals;
    if (locals == NULL) {
        locals = f->f_locals = PyDict_New();
        if (locals == NULL)
            return -1;
    }
    // 獲取對應的PyCodeObject
    co = f->f_code;
    // 獲取co_varnames欄位
    map = co->co_varnames;
    if (!PyTuple_Check(map)) {
        PyErr_Format(PyExc_SystemError,
                     "co_varnames must be a tuple, not %s",
                     Py_TYPE(map)->tp_name);
        return -1;
    }
    fast = f->f_localsplus;
    j = PyTuple_GET_SIZE(map);
    if (j > co->co_nlocals)
        j = co->co_nlocals;
    if (co->co_nlocals) {
        // 將co_varnames加入到locals中
        if (map_to_dict(map, j, locals, fast, 0) < 0)
            return -1;
    }
    // 閉包相關
    ncells = PyTuple_GET_SIZE(co->co_cellvars);
    nfreevars = PyTuple_GET_SIZE(co->co_freevars);
    if (ncells || nfreevars) {
        // 將co_cellvars加入到locals
        if (map_to_dict(co->co_cellvars, ncells,
                        locals, fast + co->co_nlocals, 1))
            return -1;
        /* If the namespace is unoptimized, then one of the
           following cases applies:
           1. It does not contain free variables, because it
              uses import * or is a top-level namespace.
           2. It is a class namespace.
           We don't want to accidentally copy free variables
           into the locals dict used by the class.
        */
        if (co->co_flags & CO_OPTIMIZED) {
            // 將co_freevars加入到locals
            if (map_to_dict(co->co_freevars, nfreevars,
                            locals, fast + co->co_nlocals + ncells, 1) < 0)
                return -1;
        }
    }
    return 0;
}

以上就是Python作用域與名稱空間原始碼學習筆記的詳細內容,更多關於Python作用域名稱空間的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com