Python作用域與名字空間源碼學(xué)習(xí)筆記
作用域與名字空間
問題:
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)
以這個程序為例,代碼中出現(xiàn)的每個變量的作用域分別是什么?程序中總共涉及多少個名字空間?Python又以怎樣的順序去查找一個變量呢?
1. 名字綁定
1.1 賦值
在Python中,變量只是一個與實際對象綁定起來的名字,變量定義本質(zhì)上就是建立名字與對象的約束關(guān)系。因此,賦值語句本質(zhì)上就是建立這樣的約束關(guān)系,將右邊的對象與左邊的名字綁定起來:
a = 1
賦值語句是最基本的將名字與對象綁定的方式,除此之外還有很多其他方式都起到了這樣的作用。
1.2 模塊導(dǎo)入
當(dāng)我們導(dǎo)入一個模塊時,也會在當(dāng)前上下文創(chuàng)建一個名字,并與被導(dǎo)入對象綁定。
# 在當(dāng)前上下文創(chuàng)建一個名字test,與被導(dǎo)入的module對象綁定 import test
1.3 函數(shù)、類定義
# 函數(shù)名circle_area與function對象綁定
def circle_area(r):
return PI * r ** 2
# 類名Person與類型對象綁定
class Person(object):
def __init__(self):
pass
1.4 as關(guān)鍵字
# 將名字t與module對象綁定 import test as t
2. 作用域
問題:當(dāng)我們引入一個名字之后,它的可見范圍有多大呢?
a = 1
def func1():
print(a) # 1
def func2():
a = 2
print(a) # 2
print(a) # 1
在不同的代碼區(qū)域引入的名字,其影響范圍是不一樣的。第1行定義的a可以影響到func1,而func2中定義的a則不能。此外,一個名字可能會在多個代碼區(qū)域中定義,但最終在某個代碼區(qū)域中只能使用其中一個。
2.1 靜態(tài)作用域
一個名字能夠施加影響的程序正文區(qū)域,便是該名字的作用域。在Python中,一個名字在程序中某個區(qū)域能否起作用,是由名字引入的位置決定的,而不是運(yùn)行時動態(tài)決定的。因此,Python具有靜態(tài)作用域,也稱為詞法作用域。那么,作用域具體是如何劃分的呢?
2.2 劃分作用域
- Python在編譯時,根據(jù)語法規(guī)則將代碼劃分為不同的代碼塊,每個代碼塊形成一個作用域。首先,整個.py文件構(gòu)成最頂層的作用域,這就是全局作用域,也成為模塊作用域;其次,當(dāng)代碼遇到函數(shù)定義,函數(shù)體成為當(dāng)前作用域的子作用域;再者,當(dāng)代碼遇到類定義,類定義體成為當(dāng)前作用域的子作用域。
- 一個名字在某個作用域引入后,它的影響范圍就被限制在該作用域內(nèi)。其中,全局作用域?qū)λ兄苯踊蜷g接內(nèi)嵌于其中的子作用域可見;函數(shù)作用域?qū)ζ渲苯幼幼饔糜蚩梢?,并且可以傳遞。
- 例子中的作用域的嵌套關(guān)系如下:

訪問關(guān)系如下:

2.3 閉包作用域
閉包的概念:在計算機(jī)科學(xué)中,閉包,又稱詞法閉包或函數(shù)閉包,是引用了自由變量的函數(shù)。這個被引用的自由變量將和這個函數(shù)一同存在,即使已經(jīng)離開了創(chuàng)造它的環(huán)境也不例外。所以,有另一種說法認(rèn)為閉包是由函數(shù)和與其相關(guān)的引用環(huán)境組合而成的實體。閉包在運(yùn)行時可以有多個實例,不同的引用環(huán)境和相同的函數(shù)組合可以產(chǎn)生不同的實例
代碼示例:
>>> 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函數(shù)對象是怎么拿到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
作用域分析:
全局作用域?qū)ζ渌袃?nèi)嵌其中的作用域均可見,所以在函數(shù)Speak()中可以訪問到language
類作用域和函數(shù)作用域不一樣,它對其子作用域是不可見的,所以在函數(shù)Gende()中g(shù)ender是不可見的
思考:
>>> male.gender 'male' >>> Male.gender 'male' >>> male.gender = 'male2' >>> male.gender 'male2' >>> Male.gender 'male'
2.5 復(fù)雜嵌套
2.5.1 函數(shù)嵌套類
在Python中,類可以動態(tài)創(chuàng)建,甚至在函數(shù)中返回。通過在函數(shù)中創(chuàng)建并返回類,可以按函數(shù)參數(shù)對類進(jìn)行動態(tài)定制
代碼示例:
>>> 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. 名字空間
作用域是語法層面的概念,是靜態(tài)的。當(dāng)程序開始執(zhí)行后,作用域中的名字綁定關(guān)系需要存儲起來,存儲的地方就是名字空間。由于名字綁定關(guān)系是由名字和對象組成的鍵值對,因此用dict是理想的存儲容器(之前在介紹dict的相關(guān)內(nèi)容時也有提到)
以計算圓面積的例子來認(rèn)識作用域背后的運(yùn)行時實體——名字空間。代碼示例如下:
>>> 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。在上述的例子中,根據(jù)我們之前對作用域的劃分,可以肯定全局名字空間中一定包含兩個名字:PI和closure_print。
如果其他模塊也需要使用PI或closure_print函數(shù),就需要通過import語句將模塊導(dǎo)入,導(dǎo)入后我們就可以獲得一個模塊對象:
# 假設(shè)我們在test.py中導(dǎo)入上述模塊testglobal.py >>> import testglobal >>> testglobal <module 'testglobal' from 'D:\\myspace\\code\\pythonCode\\mix\\namespace\\testglobal.py'> >>> type(testglobal) <class 'module'>
通過內(nèi)置函數(shù)dir()我們可以知道模塊對象下有哪些屬性可以訪問:
>>> dir(testglobal) ['PI', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'closure_print'] >>> testglobal.closure_print <function closure_print at 0x000002F33B14A050>
在Python中,一個對象可以訪問哪些屬性,成為對象的屬性空間。因此,模塊的屬性空間和全局名字空間本質(zhì)上就是同一個東西,都通過一個dict對象進(jìn)行存儲。那么如何找到這個dict對象呢——通過__dict__屬性:
>>> testglobal.__dict__
此外,我們也可以通過內(nèi)置函數(shù)globals()來獲取當(dāng)前模塊的全局名字空間:
>>> globals()
我們分別打印它們的id,本質(zhì)上就是同一個對象:
>>> id(testglobal.__dict__) 2219833831040 >>> id(globals()) 2219833831040
3.2 Locals
Python執(zhí)行一個作用域內(nèi)的代碼時,需要一個容器來訪問當(dāng)前作用域的名字,這就是局部名字空間Locals
當(dāng)Python執(zhí)行closure_print()函數(shù)時,將分配一個棧幀對象PyFrameObject來保存上下文信息以及執(zhí)行狀態(tài)。作為代碼執(zhí)行時必不可少的上下文信息,全局名字空間和局部名字空間也會在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將內(nèi)層代碼塊依賴的所有外層名字存儲在一個容器內(nèi),這就是閉包名字空間Enclosings
對于示例:
>>> pi = 3.14
>>> def closure_print(name: str):
def circle_area(r: int):
name = 1
print(name, pi * r * r)
return circle_area
當(dāng)Python執(zhí)行到print(name, pi * r * r)語句時,按照Locals、Enclosings、Globals這樣的順序查找語句中涉及的名字:名字name在Enclosings中找到,名字pi在Globals中找到,名字r在Locals中找到。那么還有一個名字print是如何找到的呢?
3.4 Builtin
Python在builtin模塊中提供了很多內(nèi)建函數(shù)和類型,構(gòu)成運(yùn)行時的另一個名字空間:內(nèi)建名字空間Builtin
全局名字空間中有一個名字指向內(nèi)建名字空間:
>>> import builtins >>> id(testglobal.__builtins__) 3065787874688 >>> id(builtins.__dict__) 3065787874688
4. 問題與總結(jié)
函數(shù)作用域?qū)?nèi)部所有的作用域均可見,包括內(nèi)部嵌套的類作用域和函數(shù)作用域(例如閉包);類作用域?qū)?nèi)部所有的作用域均不可見,包括內(nèi)部嵌套的類作用域和函數(shù)作用域。
“只要在當(dāng)前Locals命名空間中無同名變量且沒有g(shù)lobal,nonlocal等關(guān)鍵字的聲明的話,就一定創(chuàng)建一個該名字的新局部變量”,以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;
}
// 獲取對應(yīng)的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;
}
// 閉包相關(guān)
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作用域與名字空間源碼學(xué)習(xí)筆記的詳細(xì)內(nèi)容,更多關(guān)于Python作用域名字空間的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解python使用金山詞霸的翻譯功能(調(diào)試工具斷點的使用)
這篇文章主要介紹了詳解python使用金山詞霸的翻譯功能(調(diào)試工具斷點的使用),本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-01-01
基于Python+Matplotlib實現(xiàn)直方圖的繪制
Matplotlib是Python的繪圖庫,它能讓使用者很輕松地將數(shù)據(jù)圖形化,并且提供多樣化的輸出格式。本文將為大家介紹如何用matplotlib繪制直方圖,感興趣的朋友可以學(xué)習(xí)一下2022-04-04
tensorflow+k-means聚類簡單實現(xiàn)貓狗圖像分類的方法
這篇文章主要介紹了tensorflow+k-means聚類簡單實現(xiàn)貓狗圖像分類,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-04-04
使用PyInstaller將Python程序文件轉(zhuǎn)換為可執(zhí)行程序文件
與py2exe一樣,PyInstaller程序也可以將Python的.py程序文件轉(zhuǎn)換為.exe,并且還有Linux的版本,下面我們就來詳細(xì)看一下如何使用PyInstaller將Python程序文件轉(zhuǎn)換為可執(zhí)行程序文件2016-07-07
python wav模塊獲取采樣率 采樣點聲道量化位數(shù)(實例代碼)
這篇文章主要介紹了python wav模塊獲取采樣率 采樣點聲道量化位數(shù),本文通過實例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下2020-01-01

