Skip to main content

Python 變數與變數存取範圍:

Variables and Scopes

Python 世界的邏輯是 We are all consenting adults!
所以,小標題寫的是 Naming Rules 而不是 Conventions。

我沒要求你,但你應該這樣做。
若做不到,我會在心裡笑你沒長大。

class name 以大寫字母開頭(same as Java)
module name (package name) 以小寫字母開頭(Java)
function name 以小寫字母開頭,以 Underscore 連接單詞

命名規則: Variable Naming Rules

說明
varNamepublic
_varNamemodule /package, 無法 from x import x
__varNamelocal variable 類別內
varName系統變數 system variables

與 Java 不同:

underscore _ (物件封裝) : 當不希望 caller 直接使用時的規則

_x 單一底線變數

單一底線變數: weak internal use indicator,隱含的 import 語法:

告知這個變數或方法應該限制在 class 之內使用,
不應該被任意 import。
但需注意的是 single underscore 並非一種限制,只是告知
所以還是可以經由 classInstance._x 取得內容。

且在 Object Oriented 結構下,單一底線變數 _x 他還隱含著: 這個變數將被子類別複寫的意思
所以要變免直接使用單一底線變數。

test.py 中的單一底線變數 _x
無法經由
from test import * 被匯入

但是可以明確的
from test import _x 被匯入

__x 雙底線變數

雙底線變數:類似私有變數

會被 Python 的 name managling 機制改名為, _classNamex。
因此無法經由 classInstance.
x 取得,而避免資訊被誤用
進而產生出 類似私有變數 (private variable)的結果。
但設計(改名機制)本意是,避免資料被誤用,或是方法被子類覆寫等情境。

__x__ 前後雙底線

前後雙底線:Python 系統所使用與定義的變數

用來提醒該變數是 Python 系統內建使用的變數。
特殊長相,一方面可用作提醒,另一方面也可避免與使用者自訂變數相衝突。
簡單的來說就是

求值策略: Argument Evaluation Binding

與大多數程式語言相同,易理解。

  • Primitive type(Immutable Arguments) : Call by Value
    • 數值, 字串
  • Object type(Mutable Arguments) : Call by reference
    • list, map, object...

注意:
匯入模組中的 值變數 value variable

from x import y  :  call by value
import y  :  call by reference

變數存取範圍: Variable Scope

Shadow Effect : LEGB Rule

  • Python 中變數查找的順序(由內而外)
    • 注意 _x 無法經由 import * 取得
    • 注意 __x 會被改名

Local  =>  Enclosing  =>  Global  =>  Built-in

嵌套作用域 Enclosing Scope

  • enclosing scope(嵌套作用域) 又稱 statically nested scope(靜態嵌套作用域)

這邊指的是槽串內部的 function 可以取得外部 host 的資訊。
下方範例 labmda 可取得外面 host_var。

範例: 嵌套作用域 Enclosing Scope

def host_fun(a, b):
host_var = 100
sum = lambda i, j: i + j + host_var
return sum(a, b)

print(host_fun(1, 3))
#104

範例: 嵌套作用域 Enclosing Scope 遇到迴圈

  • 下面範例釋疑:
    • 第一範例: 確實以迴圈建立多個 lambdas,每個 lambda 也確實可以拿到 host 的引數,但只拿到 reference,所以當迴圈不斷改變引數值時,真正使用的是最後的元素值。
    • 第二範例: 利用 python 未 function arguments 可指定預設值的特性來暫存迴圈的 index。
def magazine(bullet: int):
shoots = []
for i in range(bullet):
shoots.append(lambda : 'Got %d Points' % (i + 1))
# 此處因為 labmda 尚未執行,而 looping 過程中 i 值不斷改變。
# 最終造成每個 lambda 的 i 值都相同
return shoots

shooting = magazine(3)
for s in shooting:
print(s())
# Got 3 Points
# Got 3 Points
# Got 3 Points

def magazine(bullet: int):
shoots = []
for i in range(bullet):
shoots.append(lambda s=i: 'Got %d Points' % (s + 1))
# 此處因為 labmda 有將 i 值另存保留(指定單一 lambda 的預設),所以得到不同結果
return shoots

shooting = magazine(3)
for s in shooting:
print(s())
# Got 1 Points
# Got 2 Points
# Got 3 Points

工廠函數 Factory Function

Factory Function 又稱 closure
指的是 nested function 在 runtime 時可以記住 host 所屬變數值的特性。
而 host 函式因為具備建立內層函式所需要的環境,故被稱為 factory function。

範例: shoot function 可以記住 bullet 的總數

def magazine(bullet):
def shoot(fire):
return '%d bullets left' % (bullet - fire)
return shoot

shooting = magazine(10) # 返回內層的 shoot()
print(shooting(2)) # shoot() 保留了 bullets 總數,即使外層 scope 已消失
# 8 bullets left
# magazine(10)(2)

global 關鍵字

global 關鍵字只會出現在 非頂層 的結構中。 用來指像頂層的全域變數用(與 nonlocal 比較)。

使用情境略分為下列三項:

  1. 解決區域變數與全域變數命名衝突
    用來宣告將 同名的區域變數指向全域變數
    Enclosing Scope(嵌套作用域)的運作下,
    函數或是一般的程式區塊預設是 可直接存取,但不能改變 全域變數的內容。
    因此若想在區塊內更改全域變數內容的話,
    會需要在區域區塊內將變數補上 global 修飾詞。

  2. 在程式區域區塊中聲明一個全域變數。
    按照 Python 經常出現的省略語法,可以直接想成:
    加 global 修飾詞的區域變數,等同於 在全域宣告了一個同名的全域變數。

  3. 若想在區塊內修改全域變數,則必須以 gloabl 關鍵告知 VM,此變數是全域變數。
    不然區塊內的變數預設視為區域變數。

關於 global 的語法面就如同上面敘述。

$ hidden $ insect-totem.net $ TODO:

回歸到程式語言的設計面, 到底 Python 什麼時候會用到 global/nonlocal?

  1. 為什麼會有工程師想要或是必要設計出命名衝突的變數或方法?
    LEGB Rule 變數查找過程預設會由內往外找。
    所以即使命名衝突那通常想要的也是 local 的那一個啊?
    若發生,我認為是誤用。
  2. 全域變數晉升。為什麼要在函數中定義一個不是自己能管轄的全域變數?
    蘋果應該長在蘋果樹上。全域的就應該放在全域吧?
    這對我來說,完全不合理。
    這情形就像是公司的獎逞制度定義在清潔人員身上一樣難以理解。
  3. 剩下 global 的用途便是想更改全域變數內容。
    我接受。 我願意接受在一個 module Scope 或 class Scope 中的全域變數,
    被一個明確指名異動該變數的方法中使用,不然會天下大亂。

解決區域變數與全域變數命名衝突 and 區塊內修改全域變數

address = 'Taipei, Taiwan'

# LEGB
def resetIp():
address = '0.0.0.0'
return address

# modify global var
def uproot(location:str):
global address
address = location
return address

print(address)
# print host address variable
# 'Taipei, Taiwan'


# uproot should be invoked out of this module
# and host address sholud be renamed to __address
uproot("Taichun, Taiwan")
print(address)
# update and print host address variable
# 'Taichun, Taiwan'


print(resetIp())
# LEGB
# 0.0.0.0

在程式區域區塊中聲明一個全域變數:全域變數晉升


def fun():
global public_X
public_X = 55
print(public_X)
# print(y)


def fun2():
global public_X
print(public_X)


fun() # 需告並初始化全域變數處
fun2() # 取用隱藏的全域變數
# 55
# 55

模組內全域變數被其他模組匯入

  • 如果在程式區塊(如:函式中)將 global 指向 import 進來的 variable。會發生?
    • 先記得一件事: primitive type 會 copy 進來。 object type 是傳進 reference。
    • imported vars 本身被視為是模組內的全域變數。
    • 所以,整個情境是跟 module 內全域變數的一樣。
    • 要注意 object type call by reference,也就是說你會改到 來源模組 的內容。也會 影響到其他 import 同模組 的程式片段。

這邊的重點在 封裝

匯入的資料預設是整個 module 的全域變數。
回歸前面說的 Pythonic Convention: underscore 變數名名規則,你應該先做 適當封裝
We are all consenting adults。變數放出來,就別怪其他使用者改變你的變數值。
也就是使用 __x 雙底線變數。

# 小心 資料被誤改啊.

nonlocal 關鍵字

  • 槽串結構中,例如 function 中又再定義 function 時,最內層(inner fun)可以藉由 nonlocal 取得外層(outer fun)中變數的 reference。
  • 這設計的邏輯到底是...... 真方便使用......
  • 避免命名衝突才是上策。

nonlocal 關鍵字只會出現在 槽串結構 的內層結構中。
用來指像外層的同名變數用。所以這邊隱含著 全域/中間層s/內層 至少出現 三次同名變數 的情境。
真的是來亂的。

與 global 不同處在於。
內層的區塊中的 global 指向全域同名變數(最外層)。

而 nonlacal 則是
內層的區塊中的 nonlacal 指向由內向外依序發現的第一個同名變數(內向外第一個發現)。

nonlocal 範例


passenger = "PASSENGER"


# without nonlocal
def tom():
passenger = "TOM"

def tom_jr():
passenger = "TOM JUNIOR"
print('tom_jr: ', passenger)

tom_jr() # tom_jr() ececuting
print('********** tom: ', passenger)


tom() # executing
print("passenger: ", passenger)
# tom_jr: TOM JUNIOR
# ********** tom: TOM **********
# passenger: PASSENGER


# with nonlocal
def gary():
passenger = "GARY" # modified by nonlocal var

def gary_jr():
nonlocal passenger # ref to var in gary()
passenger = "GARY JUNIOR"
print('gary_jr: ', passenger)

gary_jr() # tom_jr() ececuting
print('********** gary: ', passenger)


gary() # executing
print("passenger: ", passenger)
# gary_jr: GARY JUNIOR
# ********** gary: GARY JUNIOR **********
# passenger: PASSENGER