Learning Python Part II 之 作用域(scope)

引言

當你在程序中使用一個變量名時,Python在一個稱爲命名空間(namespace)地方創建、改變、查找。命名空間是變量名存在的地方。Python會根據變量名第一次賦值的位置決定將變量名放到不同的命名空間。換句話說,在源代碼中給變量名賦值的位置決定了這個名字會存在於哪個命名空間和這個名字的作用域。例如,在函數內部賦值的變量名會被放到函數的命名空間,也就是說這個變量只在函數內有效。

進階

命名空間是可以嵌套的,函數定義了一個局部(local)作用域而模塊定義了一個全局(global)作用域,並有以下特性:

  • 封閉模塊是全局作用域
    每個模塊都是全局作用域——變量在頂層模塊文件中創建並存在以一個命名空間中。當模塊被引入之後,全局變量變成了模塊對象的屬性,但在模塊內部仍舊只是一個變量。
  • 全局作用域的範圍只是每個單獨文件
    Python中全局作用域是隻和每個模塊相聯繫的,而模塊爲單獨文件,如果想使用某個文件內的變量,必須先引入模塊
  • 變量名默認是局部作用域除非聲明globalnonlocal
    所有在函數內部賦值的變量默認爲局部變量,如果需要在函數內部創建全局變量,需要特殊聲明。
  • 每次函數調用都會創建一個新的局部作用域
    每次調用函數就會創建一個新的局部作用域——一個函數內部定義的變量存在的命名空間。可以認爲每個deflambda定義一個新的局部作用域,但是局部作用域是和函數調用相對應的。因爲函數允許循環調用自己——遞歸函數。

有一點要銘記,在交互式命令行中輸入的代碼也是存在於一個模塊中的。
也要注意,在函數內部所有類型的賦值都會定義一個一個變量爲局部變量。這包括=import中的模塊名、def中的函數名、函數參數名等等,如果你在def內賦值了一個變量,他都會默認爲是局部變量。相反的,就地改變一個對象並不會定義一個名字爲局域變量,只有實際上的賦值語句是。

LEGB規則

Python中處理變量名的解決方案稱之爲LEGB規則,不過這個規則只適用於變量名

  • 當你在函數內部使用一個無限制的變量時,Python會在4個作用域中搜索——局部作用域(Local),然後是局部作用域的任何封閉(Enclosing) deflambda,然後是全局作用域(Global),然後是內置(Built-in)作用域。
  • 當在函數內部賦值一個名字的時候,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 = 1
>>> y = 2
>>> def func():
...     global x
...     x = 0
...     y = 0
... 
>>> print(x, y)
1 2

程序設計:最小化全局變量

函數應該依賴於參數和返回值,而不是全局變量。默認情況下函數內的賦值是局部變量,如果你想改變函數外的變量你必須寫額外的代碼(如global),但你必須十分謹慎,避免日後潛在的麻煩和危險,程序會更難debug和理解,所以應該儘量避免使用global

程序設計:最小化跨文件改變

這是另一個作用域相關的設計問題——儘管我們可以在另一個文件中直接改變變量,但是應該儘量避免這麼做:

# first.py
X = 99 # This code doesn't know about second.py

# second.py
import first
print(first.X)
first.X = 88
    第一個文件定義了一個變量X,第二個文件打印了X,並在之後通過賦值語句改變了X。注意到我們必須在第二個人間中引入第一個文件才能訪問他的變量——正如我們之前瞭解到的,每一個模塊都有自己的命名空間,我們必須引入才能在另一個文件中訪問他的變量。這也是重點——通過分開變量在不同的人間中來避免命名衝突,同樣的方法也避免了不同函數內的變量衝突。

事實上,一旦被引入之後,模塊的全局作用域就變成了模塊對象的屬性命名空間——引入者能夠自動訪問文件的所有全局變量,因爲在引入之後文件的全局作用域轉變成了對象的屬性空間。

    引入第一個模塊之後,第二個文件打印變量並賦了一個新值,引用並打印變量這沒什麼問題——這正是在更大的系統中模塊是如何聯繫到一起的。問題的關鍵在於重新賦值,這有隱含性的危害——維護或重用第一個文件的人可能並不知道某個文件內在運行時會改變X的值。或許第二個文件在完全不同的目錄,這很難注意到。

儘管這種跨文件的變量改變是可能的,但這要比你想象的更微妙,這在兩個文件間建立了太強的聯繫——兩個文件都依賴於變量X,這使其很困難去理解或重用其中單獨一個文件。這樣的跨文件依賴頑固的代碼和嚴重的bug。
跨文件通信的最好方法是調用函數,傳參和返回值,在下列這個例子中我們定義了一個函數去管理這種改變:

# first.py
X = 99
def setX(new):
global X
X = new

# second.py
import first
first.setX(88)

這需要更多的代碼和和一些看似細小的改變。但在可讀性和可維護性上產生了巨大的不同——當一個人讀第一個模塊的時候會看到這個函數,就會知道這是一個改變X值的接口。儘管我們並不能避免跨文件改變的生,但我們也要最小化的使用。

其他訪問全局變量的方法

# thismod.py
var = 99
def local():
var = 0 
def glob1():
global var
var += 1 

def glob2():
var = 0
import thismod
thismod.var += 1 


def glob3():
var = 0
import sys
glob = sys.modules['thismod']
glob.var += 1 

def test():
print(var)
local(); glob1(); glob2(); glob3()
print(var)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章