引言
當你在程式中使用一個變量名時,Python在一個稱為命名空間(namespace)地方建立、改變、查找。命名空間是變量名存在的地方。Python會根據變量名第一次指派的位置決定将變量名放到不同的命名空間。換句話說,在源代碼中給變量名指派的位置決定了這個名字會存在于哪個命名空間和這個名字的作用域。例如,在函數内部指派的變量名會被放到函數的命名空間,也就是說這個變量隻在函數内有效。
進階
命名空間是可以嵌套的,函數定義了一個局部(
local
)作用域而子產品定義了一個全局(
global
)作用域,并有以下特性:
-
封閉子產品是全局作用域
每個子產品都是全局作用域——變量在頂層子產品檔案中建立并存在以一個命名空間中。當子產品被引入之後,全局變量變成了子產品對象的屬性,但在子產品内部仍舊隻是一個變量。
-
全局作用域的範圍隻是每個單獨檔案
Python中全局作用域是隻和每個子產品相聯系的,而子產品為單獨檔案,如果想使用某個檔案内的變量,必須先引入子產品
- 變量名預設是局部作用域除非聲明
或global
所有在函數内部指派的變量預設為局部變量,如果需要在函數内部建立全局變量,需要特殊聲明。nonlocal
-
每次函數調用都會建立一個新的局部作用域
每次調用函數就會建立一個新的局部作用域——一個函數内部定義的變量存在的命名空間。可以認為每個
或def
定義一個新的局部作用域,但是局部作用域是和函數調用相對應的。因為函數允許循環調用自己——遞歸函數。lambda
有一點要銘記,在互動式指令行中輸入的代碼也是存在于一個子產品中的。
也要注意,在函數内部所有類型的指派都會定義一個一個變量為局部變量。這包括
=
、
import
中的子產品名、
def
中的函數名、函數參數名等等,如果你在
def
内指派了一個變量,他都會預設為是局部變量。相反的,就地改變一個對象并不會定義一個名字為局域變量,隻有實際上的指派語句是。
LEGB規則
Python中處理變量名的解決方案稱之為LEGB規則,不過這個規則隻适用于變量名。
- 當你在函數内部使用一個無限制的變量時,Python會在4個作用域中搜尋——局部作用域(Local),然後是局部作用域的任何封閉(Enclosing)
或def
,然後是全局作用域(Global),然後是内置(Built-in)作用域。lambda
- 當在函數内部指派一個名字的時候,Python總是隻在局部作用域中建立或者改變,除非在函數内聲明
或者global
nonlocal
- 當在任何函數外指派一個變量的時候,局部作用域就是全局作用域——子產品的命名空間
圖示:
其他Python作用域
确切的來說,Python中還有其他三種作用域——一些推導式(comprehension)中的循環變量、一些
try
中的異常引用變量(exception reference)、
class
語句中的局部作用域。前兩個屬于特殊情況,而第三個遵循LEGB規則。
推導式變量(comprehension)
推導式中的變量X用來指向目前的疊代元素,如[ x for x in I]。因為他們可能會與其它變量名沖突而影響生成器的内在狀态,在3.X中,這樣的變量是表達式的局部變量,在所有的推導式格式中都是這樣:生成器、清單、集合和字典。在2.X中它們對于生成器表達式、集合和字典來說是局部的,但不适用于清單生成式。作為對比,for循環從來不局部化它們的變量。
異常變量
在try子產品中變量X用來指向抛出的錯誤例如 except E as X。因為在3.X它們可能推遲垃圾回收機制的記憶體回收,這樣的變量屬于except塊的局域變量,當塊退出後它們就被删除。在2.x中在try之後一直存在
内置(built-in)作用域
内置作用域就是一個内置子產品稱作
builtins
,但是當查詢這個子產品的時候必須引入,因為
builtins
這個名字并沒有内置到這個子產品中,
builtins
隻是一個标準庫檔案,可以用
dir
函數檢視:
>>> import builtins
>>> dir(builtins)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException',
'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning',
...many more names omitted...
'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed',
'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum',
'super', 'tuple', 'type', 'vars', 'zip']
很明顯能夠看出,前面的是内置異常名,後面的是内置函數名。因為Python會根據LEGB規則自動搜尋這個子產品,是以你可以自由的使用這些函數而不需要引入任何子產品。例如有兩種方式調用内部函數,效果是一樣的:
>>> zip
<class 'zip'>
>>> import builtins
>>> builtins.zip
<class 'zip'>
>>> zip is builtins.zip
True
并且銘記,盡管Python中有保留字,但是當你重定義一個内置函數名的時候Python是不警告你的,也不會抛出錯誤。是以盡量不要重定義名内置函數名,或者至少不要重定義你需要用到的函數。
global語句
Python中的
global
語句和
nonlocal
語句更像是聲明語句,但他們不是類型或大小的聲明,而是命名空間的聲明:
>>> x =
>>> y =
>>> def func():
... global x
... x =
... y =
...
>>> print(x, y)
程式設計:最小化全局變量
函數應該依賴于參數和傳回值,而不是全局變量。預設情況下函數内的指派是局部變量,如果你想改變函數外的變量你必須寫額外的代碼(如
global
),但你必須十分謹慎,避免日後潛在的麻煩和危險,程式會更難
debug
和了解,是以應該盡量避免使用global。
程式設計:最小化跨檔案改變
這是另一個作用域相關的設計問題——盡管我們可以在另一個檔案中直接改變變量,但是應該盡量避免這麼做:
# first.py
X = # This code doesn't know about second.py
# second.py
import first
print(first.X)
first.X =
第一個檔案定義了一個變量X,第二個檔案列印了X,并在之後通過指派語句改變了X。注意到我們必須在第二個人間中引入第一個檔案才能通路他的變量——正如我們之前了解到的,每一個子產品都有自己的命名空間,我們必須引入才能在另一個檔案中通路他的變量。這也是重點——通過分開變量在不同的人間中來避免命名沖突,同樣的方法也避免了不同函數内的變量沖突。
事實上,一旦被引入之後,子產品的全局作用域就變成了子產品對象的屬性命名空間——引入者能夠自動通路檔案的所有全局變量,因為在引入之後檔案的全局作用域轉變成了對象的屬性空間。
引入第一個子產品之後,第二個檔案列印變量并賦了一個新值,引用并列印變量這沒什麼問題——這正是在更大的系統中子產品是如何聯系到一起的。問題的關鍵在于重新指派,這有隐含性的危害——維護或重用第一個檔案的人可能并不知道某個檔案内在運作時會改變X的值。或許第二個檔案在完全不同的目錄,這很難注意到。
盡管這種跨檔案的變量改變是可能的,但這要比你想象的更微妙,這在兩個檔案間建立了太強的聯系——兩個檔案都依賴于變量X,這使其很困難去了解或重用其中單獨一個檔案。這樣的跨檔案依賴頑固的代碼和嚴重的bug。
跨檔案通信的最好方法是調用函數,傳參和傳回值,在下列這個例子中我們定義了一個函數去管理這種改變:
# first.py
X =
def setX(new):
global X
X = new
# second.py
import first
first.setX()
這需要更多的代碼和和一些看似細小的改變。但在可讀性和可維護性上産生了巨大的不同——當一個人讀第一個子產品的時候會看到這個函數,就會知道這是一個改變X值的接口。盡管我們并不能避免跨檔案改變的生,但我們也要最小化的使用。
其他通路全局變量的方法
# thismod.py
var =
def local():
var =
def glob1():
global var
var +=
def glob2():
var =
import thismod
thismod.var +=
def glob3():
var =
import sys
glob = sys.modules['thismod']
glob.var +=
def test():
print(var)
local(); glob1(); glob2(); glob3()
print(var)