函式(三)作用域之變數作用域、函式巢狀中區域性函式作用域、預設值引數作用域

Eniac-W發表於2020-11-12

一、非巢狀函式下全域性變數與區域性變數的使用

x=200
>>> def f2():
...     print(x)
...
>>> f2()
200
x=200
>>> def f2():
...     x=100
...     print(x)
...
>>> f2()
100
在函式體外定義的變數,一定是全域性變數,例如:
add = "http://c.biancheng.net/shell/"
def text():
    print("函式體內訪問:",add)
text()
print('函式體外訪問:',add)
執行結果為:
函式體內訪問: http://c.biancheng.net/shell/
函式體外訪問: http://c.biancheng.net/shell/

在函式體內定義全域性變數。即使用 global 關鍵字對變數進行修飾後,該變數就會變為全域性變數。例如:
def text():
    global add
    add= "http://c.biancheng.net/java/"
    print("函式體內訪問:",add)
text()
print('函式體外訪問:',add)
執行結果為:
函式體內訪問: http://c.biancheng.net/java/
函式體外訪問: http://c.biancheng.net/java/
>>> x=200
>>> def f2():
...     print(x)
...     x=100
...
>>> f2()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in f2
UnboundLocalError: local variable 'x' referenced before assignment

x = 200
def fn():
    print(x)  報錯!該步執行不了!
    x += 1    只要在該作用域內賦值定義('='),該作用域內的所有該變數都為區域性變數!不管位置在哪
    print(x)
fn()
>>> x=200
>>> def f():
...     x           
...     x=x+1    x沒定義
...     print(x)
...
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in f
UnboundLocalError: local variable 'x' referenced before assignment


x=200
>>> def f():
...     x
...     print(x)
...
>>> f()
200

>>> x=200
>>> def f():
...     x=1
...     x=x+1
...     print(x)
...
>>> f()
2
>>> x
200

x = 200
def f():
    x = x + 1     報錯! 賦值即定義,即 x = x + 1 (區域性變數 = 區域性變數 + 1)print(x)
fn()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in f
UnboundLocalError: local variable 'x' referenced before assignment

>>> x=200
>>> def f():
...     t=x+1
...     print(t)
...
>>> f()
201

綜上只要在函式內部在該作用域內賦值定義(’=’),該作用域內的所有該變數都是區域性變數!不管賦值的位置在哪,因此在賦值之前必須對該變數進行宣告,即使該變數和全域性變數同名也需要再次宣告
宣告和全域性變數同名的變數時若沒有使用global關鍵字 該變數是區域性變數 對該變數的修改不影響與其同名的全域性變數的值
宣告和全域性變數同名的變數時若使用global關鍵字宣告,則函式內部對該變數的修改就是對同名全域性變數的修改

在使用 global 關鍵字修飾變數名時,不能直接給變數賦初值,否則會引發語法錯誤。


x = 100
def fn():
    global x  # 宣告全域性變數
    print(x)  # 100
    x += 1
    print(x)  # 101
fn()
print(x)      # 101

global 使用原則:
1> 外部作用域變數會在內部作用域可見,但也不要在這個內部的區域性作用域中直接使用,因為函式的目的就是為了封裝,儘量與外界隔離。
2> 如果函式需要使用外部全域性變數,使用函式的形參定義,並在呼叫傳實參解決。
3> 一句話:不用 global,學習它就是為了深入理解變數作用域,全域性變數一般情況不推薦修改,一旦在作用域中使用 global 宣告全域性變數,那麼相當於在對全域性變數賦值定義。

二、函式巢狀下的作用域

(1)區域性函式的作用域

函式內部定義的函式叫做區域性函式,和區域性變數一樣預設情況下區域性函式只能在其所在函式的作用域內使用

>>> def outdef():
...     def indef():
...             print("indef")
...     indef()  在區域性函式所在函式內呼叫該區域性函式
...
>>> outdef()     外部呼叫區域性函式所在的函式 達到間接呼叫區域性函式的效果
indef
>>> indef()      不能在區域性函式所在函式外直接呼叫該區域性函式
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'indef' is not defined

要想在外部直接呼叫區域性函式需要
將區域性函式作為所在函式的返回值,外部呼叫區域性函式所在的函式來接收作為返回值的區域性函式,然後再呼叫該返回值函式即可

閉包,又稱閉包函式或者閉合函式,其實和前面講的巢狀函式類似,不同之處在於,閉包中外部函式返回的不是一個具體的值,而是一個函式。一般情況下,返回的函式會賦值給一個變數,這個變數可以在後面被繼續執行呼叫。

就如同全域性函式返回其區域性變數,就可以擴大該變數的作用域一樣,
通過將區域性函式作為所在函式的返回值,也可以擴大區域性函式的使用範圍。例如,修改上面程式為:
#全域性函式
def outdef ():
    #區域性函式
    def indef():
        print("呼叫區域性函式")
    #呼叫區域性函式
    return indef
#呼叫全域性函式
new_indef = outdef()
呼叫全域性函式中的區域性函式
new_indef()


程式執行結果為:
呼叫區域性函式
#閉包函式,其中 exponent 稱為自由變數
def nth_power(exponent):
    def exponent_of(base):
        return base ** exponent
    return exponent_of # 返回值是 exponent_of 函式
    
square = nth_power(2) # 計算一個數的平方
cube = nth_power(3) # 計算一個數的立方

print(square(2))  # 計算 2 的平方 2**2
print(cube(2)) # 計算 2 的立方    2**3

程式執行結果
4
8

區域性函式的作用域可總結為:
如果所在函式沒有返回區域性函式,則區域性函式的可用範圍僅限於所在函式內部
如果所在函式將區域性函式作為返回值,則區域性函式的作用域就會擴大,既可以在所在函式內部使用,也可以在所在函式的作用域中使用

(2)變數的作用域

外層變數在內部作用域可見,內層作用域中如果定義了和外層相同的變數,相當於在當前函式作用域中重新定義了一個新的變數,該內層變數不能覆蓋掉外部作用域中的變數

def outer():
    o = 65    # 區域性變數、本地 local 變數、臨時變數
    def inner():
        o = 97
        print('inner', o)   
    print('outer 1 ', o)    外層變數在內部作用域可見
    inner()
    print('outer 2 ', o)    內層變數並不能覆蓋掉外部作用域中的變數
outer()   

執行結果    
outer 1  65    
inner 97    
outer 2  65

如果區域性函式中定義有和所在函式中變數同名的變數,會發生“遮蔽”的問題

#全域性函式
def outdef ():
    name = "所在函式中定義的 name 變數"
    #區域性函式
    def indef():
        print(name)
        name = "區域性函式中定義的 name 變數"
    indef()
#呼叫全域性函式
outdef()

UnboundLocalError: local variable 'name' referenced before assignment
區域性函式 indef() 中定義的 name 變數遮蔽了所在函式 outdef() 中定義的 name 變數。
再加上,indef() 函式中 name 變數的定義位於 print() 輸出語句之後,導致 print(name) 語句在執行時找不到定義的 name 變數,因此程式報錯。


>>> def outdef():
...     name="所在函式中定義的 name 變數"
...     def indef():
...             name="區域性函式中定義的 name 變數"
...             print(name)
...     indef()
...
>>> outdef()
"區域性函式中定義的 name 變數"



>>> def outdef():
...     name="out"
...     def indef():
...             print(name)    外層變數在內部作用域可見
...     indef()
...
>>> outdef()
out

>>> def outdef():
...     name="out"
...     def indef():
...             name+="123"   一旦有賦值就是區域性變數
...     indef()
...
>>> outdef()
UnboundLocalError: local variable 'name' referenced before assignment

其次python中name也不可修改
>>> def outdef():
...     name="out"
...     def indef():
...             name.append("123")
...     indef()
...
>>> outdef()
AttributeError: 'str' object has no attribute 'append'


>>> def outdef():
...     x=10
...     def indef():
...             x+=1
...     indef()
...
>>> outdef()
UnboundLocalError: local variable 'x' referenced before assignment

由於這裡的 name 變數也是區域性變數(在outdef函式中定義的),因此globals關鍵字並不適用於解決此問題。這裡可以使用 Python3 提供的 nonlocal 關鍵字
nonlocal將變數標記為不在本地作用域定義,而是在上級的某一級區域性作用域中定義,但不能是全域性作用域中
引入兩個概念:
自由變數:未在本地作用域中定義的變數,如定義在內層函式外的外層函式的作用域中的變數。
閉包:出現在巢狀函式中,指內層函式引用到了外層函式的自由變數,就形成了閉包

閉包:indef通過nonlocal引用到了outdef中的name
#全域性函式
def outdef ():
    name = "所在函式中定義的 name 變數"
    #區域性函式
    def indef():
        nonlocal name
        print(name)
        #修改name變數的值
        name = "區域性函式中定義的 name 變數"
    indef()
#呼叫全域性函式
outdef()


程式執行結果為:
所在函式中定義的 name 變數
python 2 實現閉包  使用可變型別列表等
def counter():
    c = [0]
    def inc():
        c[0] += 1  # 是賦值即定義嘛?不是!是修改值
        return c[0]
    return inc     # 返回識別符號,即函式物件

m = counter()
m()                # 呼叫函式 inc(),但是 c 消亡了嘛?沒有,內層函式沒有消亡,c 不消亡(閉包)
m()
m()
print(m())

執行結果 4
 推薦使用 nonlocal,python 3 實現閉包
def counter():
    c = 0
    def inc():
        nonlocal c    # 非當前函式的本地變數,當前函式之外的任意層函式的變數,絕非 global
        c += 1        # 是閉包嗎?是!
        return c
    return inc

m = counter()
m()
m()
m()
print(m())

執行結果 4

三、預設值引數的作用域

函式也是物件,每個函式定義被執行後就生成了一個函式物件和函式名這個識別符號關聯。python 把函式預設值放在函式物件的屬性中,該屬性伴隨著該函式物件的整個生命週期,檢視 _defaults _ 屬性它是個元組, _kwdefaults _是一個字典

不可變預設值引數型別(int string等)在函式內修改該引數 下次再呼叫該函式 預設值仍然不變
可變預設值引數型別(list等)在函式內修改該引數 下次再呼叫該函式 預設值是修改後的值

def foo(x=1):
    x += 1
    print(x)
foo()   
foo() 
  
執行結果
2
2
第二次執行x仍然等於2說明第二次進入函式時x值為1 

def bar(x=[]):    # x = [],引用型別
    x.append(1)   # [1]
    print(x)
bar()   
bar()    

執行結果
[1]
[1,1]


def foo(x, m=123, n='abc'):
    m=456
    n='def'
    print(x)
print(foo.__defaults__)    
foo('yang')
print(foo.__defaults__)   

執行結果
(123, 'abc')
'yang'
(123, 'abc')



def foo(x, m=123, *, n='abc', t=[1,2]):
    m=456
    n='def'
    t.append(12)
    #t[:].append(12)    # t[:],全新複製一個列表,避免引用計數
    print(x, m, n, t)

print(foo.__defaults__, foo.__kwdefaults__) #(123,) {'n': 'abc', 't': [1, 2]}
foo('yang')
print(foo.__defaults__, foo.__kwdefaults__) #(123,) {'n': 'abc', 't': [1, 2, 12]}

列表的 + 和 += 的區別:
+表示兩個列表合併並返回一個全新的列表。
+= 表示,就地修改前一個列表,在其後追加後一個列表,就是 extend 方法

def x(a=[]):
    a = a + [5]          加法的本質:返回新列表、新地址;賦值即定義
print(x.__defaults__)    ([],)
x()
x()
print(x.__defaults__)    ([],)

def y(a=[]):
    a += [5]             += 即 extend => a.extend([5])
print(y.__defaults__)    ([],)
y()
y()
print(y.__defaults__)    ([5, 5],)

相關文章