Python函數(shù)進(jìn)階

一、函數(shù)返回值

先看下幾個(gè)例子:

# return之后可以執(zhí)行嗎?
def showplus(x):
    print(x)
    return x + 1
    print('~~~~end~~~~') # return之后會(huì)執(zhí)行嗎? -- 不會(huì)!

showplus(5)

# 多條return語句都會(huì)執(zhí)行嗎?
def showplus(x):
    print(x) 
    return x + 1 # 會(huì)被執(zhí)行
    return x + 2 # 不會(huì)被執(zhí)行
    
print(showplus(5))

# 下例多個(gè)return可以執(zhí)行嗎? 
def guess(x):
    if x > 3:
        return "> 3"
    else:
        return "<= 3"
        
print(guess(10))

# 下面函數(shù)執(zhí)行的結(jié)果是什么?
def fn(x):
    for i in range(x):
        if i > 3:
            return i > 3
        else:
            print("{} is not in greater than 3".format(i))
            
print(fn(5))
print(fn(3))

總結(jié):

  • python函數(shù)使用return語句返回 "返回值"
  • 所有函數(shù)都有返回值,如果沒有return語句的,隱式調(diào)用return None
  • return語句不一定是函數(shù)語句塊的最后一句
  • 一個(gè)函數(shù)可以有多個(gè)return語句,但是只有一條可以被執(zhí)行。如果沒有一條被執(zhí)行到,則會(huì)隱式調(diào)用return None
  • 如果有必要,可以顯示調(diào)用return None,可以簡寫成return
  • 如果函數(shù)執(zhí)行return語句,函數(shù)就會(huì)返回,當(dāng)前被執(zhí)行的return語句之后的其他語句就不再運(yùn)行了
  • 返回語句的作用:結(jié)束函數(shù)調(diào)用、返回返回值

能夠一次返回多個(gè)值嗎?

def showvalues():
    return 1, 3, 5
    
print(showvalues()) # 返回了一個(gè)值,是一個(gè)三元組;并不是返回三個(gè)值
  • 函數(shù)不能同時(shí)返回多個(gè)值
  • return 1,3,5 看似返回多個(gè)值,其實(shí)隱式地被python封裝成了一個(gè)元組
  • 使用解構(gòu) x, y, z = showvalues() 提取返回值更加方便

二、函數(shù)作用域

1、作用域

一個(gè)標(biāo)識(shí)符地可見范圍,就是標(biāo)識(shí)符地作用域。一般常說的是變量地作用域。

def foo():
    x = 100
    
print(x) # 可以訪問地到嗎? -- 不行

上例的 x 不可以訪問到,會(huì)拋出異常 (NameError: name 'x' is not defined),原因在于函數(shù)是一個(gè)封裝,它會(huì)開辟一個(gè)作用域,x變量被限制在這個(gè)作用域中,所以在函數(shù)外部,x變量不可見。

注意:每一個(gè)函數(shù)都會(huì)開辟一個(gè)作用域

2、作用域的分類

2.1 全局作用域

  • 在整個(gè)程序運(yùn)行環(huán)境中都可見
  • 全局作用域中的變量稱為全局變量

2.2 局部作用域

  • 在函數(shù)、類等內(nèi)部可見
  • 局部作用域中的變量稱為局部變量,其使用范圍不能超過其所在局部作用域
# 局部變量
def fn1():
    x = 1 # 局部變量
    
def fn2():
    print(x) # 訪問不到x
    
print(x) # 訪問不到x
# 全局變量
x = 5 # 全局變量
def foo():
    print(x) # 訪問的到x

foo()
  • 一般來講,外部作用域變量在函數(shù)內(nèi)部可見,可以使用
  • 反過來,函數(shù)內(nèi)部的局部變量,不能再函數(shù)外部看到

3、函數(shù)嵌套

在函數(shù)中定義另一個(gè)函數(shù):

def outer():
    def inner():
        print("inner")
    print("outer")
    inner()
    
outer()
inner() # NameError: name 'inner' is not defined

內(nèi)部函數(shù) inner 不能在外部直接使用,會(huì)拋出NameError異常,因?yàn)樗谄渌瘮?shù)外部不可見。

其實(shí),inner 不過就是一個(gè)標(biāo)識(shí)符,就是一個(gè)函數(shù) outer 內(nèi)部定義的變量而已。

3.1 嵌套結(jié)構(gòu)的作用域

對(duì)比下面嵌套結(jié)構(gòu),代碼執(zhí)行效果的比較

def outer1():
    o = 65
    def inner():
        print("inner {}".format(o))
        print(chr(o))
        
    inner()
    print("outer {}".format(o))
    
outer1()


# 運(yùn)行結(jié)果:

inner 65
A
outer 65
def outer2():
    o = 65
    def inner():
        o = 97
        print("inner {}".format(o))
        print(chr(o))
        
    inner()
    print("outer {}".format(o))
    
outer2()


# 運(yùn)行結(jié)果:

inner 97
a
outer 65

從執(zhí)行結(jié)果可以看出:

  • 外部變量在內(nèi)部作用域可見
  • 內(nèi)部作用域中,如果定義了 o = 97 ,相當(dāng)于在當(dāng)前函數(shù)inner作用域中重新定義了一個(gè)新的變量o,但是,這個(gè)o并不能覆蓋掉外部作用域outer2中的變量o。只是對(duì)于inner函數(shù)來說,其只能看見自己作用域中定義的變量o。

4、一個(gè)賦值語句的問題

思考:

函數(shù)1 函數(shù)2
x = 5
def foo():
print(x)

foo()
x = 5
def foo():
y = x + 1 # 可以運(yùn)行
x += 1 # UnboundLocalError: local variable 'x' referenced before assignment
print(x)

foo()
正常執(zhí)行,函數(shù)外部的變量在函數(shù)內(nèi)部可見 執(zhí)行錯(cuò)誤,為什么?難道函數(shù)內(nèi)部又不可見了嗎?

原因分析:

  • x += 1 等價(jià)于 x = x + 1,相當(dāng)于在 foo內(nèi)部定義一個(gè)局部變量x,那么foo內(nèi)部所有x都是局部變量x了
  • x = x + 1 相當(dāng)于使用了全局變量x,但是這個(gè)x還沒完成賦值,就被右邊拿來做加1操作了

如何解決這個(gè)問題呢?

5、global語句

x = 5
def foo():
    global x # 全局變量聲明
    x += 1
    print(x)

foo()
  • 使用global關(guān)鍵字的變量,將foo內(nèi)的x聲明為使用外部全局作用域定義的x
  • 全局作用域中必須有x的定義

如果全局作用域中沒有x的定義會(huì)怎么樣?

# 有錯(cuò)嗎?
def foo():
    global x
    x += 1 
    print(x)
    
foo() # NameError: name 'x' is not defined
# 有錯(cuò)嗎?
def foo():
    global x
    x = 10
    x += 1
    print(x)
    
foo() 
print(x) # 可以嗎?-- 可以

使用global關(guān)鍵字定義全局變量,雖然在foo函數(shù)中聲明,但是這將告訴我們當(dāng)前foo函數(shù)作用域,這個(gè)x變量將使用外部全局作用域中的x。

即使是在foo中又寫了 x = 10,也不會(huì)在foo這個(gè)局部作用域中定義局部變量x了。

使用了global,foo中的 x 不再是局部變量了,它是全局變量。

5.1 總結(jié)

  • x += 1 這種特殊形式產(chǎn)生的錯(cuò)誤的原因是 -- 先引用后賦值。而python動(dòng)態(tài)語言是賦值才算定義,才能被引用。解決辦法,在這條語句之前加上 x = 0 之類的賦值語句,或者使用global告訴內(nèi)部作用域,去全局作用域查找變量定義
  • 內(nèi)部作用域使用 x = 10 之類的賦值語句會(huì)重新定義局部變量作用域使用的變量x,但是,一旦這個(gè)作用域中使用global聲明x為全局的,那么 x = 5 相當(dāng)于在為全局作用域的變量x賦值

5.2 global的使用原則

  • 外部作用域變量會(huì)在內(nèi)部作用域可見,但是也不要在這個(gè)內(nèi)部的局部作用域中直接使用,因?yàn)楹瘮?shù)的目的就是為了封裝,盡量和外界隔離
  • 如果函數(shù)需要使用外部全局變量,請(qǐng)盡量使用函數(shù)的形參定義,并在調(diào)用實(shí)參中傳實(shí)參解決
  • 一句話,不用global。學(xué)習(xí)他就是為了深入了解作用域而已

6、閉包

自由變量:未在本地作用域中定義的變量。例如定義在內(nèi)層函數(shù)外的外層函數(shù)的作用域中的變量。

閉包:就是一個(gè)概念,出現(xiàn)在嵌套函數(shù)中,指的是內(nèi)層函數(shù)引用到了外層函數(shù)的自由變量,就形成了閉包。很多語言都有這個(gè)概念,最熟悉的就是JavaScript。

def counter():
    c = [0]
    def inc():
        c[0] += 1 # 會(huì)報(bào)錯(cuò)嗎?為什么
        return c[0]
    return inc
foo = counter()
print(foo(), foo())  
c = 100
print(foo())

# 思考:
# 第4行會(huì)不會(huì)報(bào)錯(cuò)?
# 第8行打印結(jié)果是什么?
# 第10行打印結(jié)果是什么?

代碼分析:

  • 第7行會(huì)執(zhí)行counter函數(shù),返回inc對(duì)應(yīng)的函數(shù)對(duì)象,注意這個(gè)函數(shù)對(duì)象并不釋放,因?yàn)橛衒oo記著
  • 第4行會(huì)報(bào)錯(cuò)嗎?為什么
    • 不會(huì)
    • 因?yàn)閏已經(jīng)在counter中定義過了,而且inc中的使用方式為c修改元素值,而不是重新定義c變量,所以inc函數(shù)不認(rèn)為c變量是本地變量,而是引用了外部函數(shù)的自由變量c,沒有犯[前面講過的賦值語句引起的錯(cuò)誤](# 4、一個(gè)賦值語句的問題)
  • 第8行打印結(jié)果是什么?
    • 打印1 2
  • 第10行打印結(jié)果是什么?
    • 打印3
    • 第9行的c和counter定義的c不一樣,而inc使用的是自由變量正是counter中的變量c

這是python 2中實(shí)現(xiàn)閉包的方式,python 3還可以使用nonlocal關(guān)鍵字。

在看下面這段代碼,會(huì)報(bào)錯(cuò)嗎?使用global能解決嗎?

def counter():
    count = 0
    def inc():
        count += 1
        return count
    return inc
    
foo = counter()
print(foo(), foo())

肯定會(huì)報(bào)錯(cuò),因?yàn)榈?行犯了前面講過的賦值語句引起的錯(cuò)誤,使用global能解決:

def counter(): 
    global count #
    count = 0
    def inc():
        global count #
        count += 1
        return count
    return inc
    
foo = counter()
print(foo(), foo())

這是使用全局變量來解決,而不是閉包。

如果要對(duì)這個(gè)普通變量使用閉包,python 3中提供nonlocal關(guān)鍵字。

7、nonlocal語句

nonlocal:將變量標(biāo)記成不是本地的,而是在上級(jí)的的某一級(jí)作用域中定義的,但是不能是全局作用域中的變量。

def counter():
    count = 0
    def inc():
        nonlocal count # 聲明變量count不是本地變量
        count += 1
        return count
    return inc
    
foo = counter()
print(foo(), foo())

count是外層函數(shù)的局部變量,被內(nèi)部函數(shù)引用。內(nèi)部函數(shù)使用nonlocal關(guān)鍵字聲明count變量在上級(jí)作用域而非本地作用域中定義。內(nèi)層函數(shù)引用外層局部作用域中的自由變量,形成閉包。

a = 50
def counter():
    nonlocal a # SyntaxError: no binding for nonlocal 'a' found,上級(jí)作用域是全局
    a += 1
    print(a)
    count = 0
    def inc():
        global count
        count += 1 
        return count
    return inc

foo = counter()
foo()
foo()

上例是錯(cuò)誤的,nonlocal聲明a不在當(dāng)前作用域,但是往外就是全局作用域了,所以錯(cuò)誤。

三、默認(rèn)值的作用域

def foo(xyz=1):
    print(xyz)
    
foo()  # 1
foo()  # 1
print(xyz)  # NameError: name 'xyz' is not defined
def foo(xyz=[]):
    xyz.append(1)
    print(xyz)
    
foo() # [1]
foo() # [1, 1]
print(xyz) # NameError: name 'xyz' is not defined

為什么第二個(gè)函數(shù)第6行打印出來是 [1, 1] ?

  • 因?yàn)楹瘮?shù)也是對(duì)象,每個(gè)函數(shù)執(zhí)行完后,就生成了一個(gè)函數(shù)對(duì)象和函數(shù)名這個(gè)標(biāo)識(shí)符關(guān)聯(lián)
  • python把函數(shù)的默認(rèn)值放在了函數(shù)對(duì)象的屬性中,這個(gè)屬性就伴著這個(gè)函數(shù)對(duì)象的整個(gè)生命周期
  • 查看foo.__defaults__ 屬性,他是個(gè)元組
def foo(xyz=[]):
    xyz.append(1)
    print(xyz)

print(id(foo), foo.__defaults__)
foo()
print(id(foo), foo.__defaults__)
foo()
print(id(foo), foo.__defaults__)


# 運(yùn)行結(jié)果
2464804553352 ([],)
[1]
2464804553352 ([1],)
[1, 1]
2464804553352 ([1, 1],)

函數(shù)地址沒有變,就是說foo這個(gè)函數(shù)對(duì)象沒有動(dòng)過,調(diào)用它,它的屬性 _defaults_ 中使用元組保存默認(rèn)值

xyz默認(rèn)值是引用類型,引用類型的元素變動(dòng),并不是元組的變動(dòng)

非引用類型的默認(rèn)值

def foo(xyz, m=123, n='abc'):
    m = 456
    n = 'def'
    print(xyz)

print(id(foo), foo.__defaults__) # 1370273530504 (123, 'abc')
foo('pinginglab')                # pinginglab
print(id(foo), foo.__defaults__) # 1370273530504 (123, 'abc'),和前面一樣

屬性 _defaults_ 中使用元組保存所有參數(shù)的默認(rèn)值,它不會(huì)因?yàn)樵诤瘮?shù)體內(nèi)改變了局部變量(形參)的值而發(fā)生改變

keyword-only參數(shù)的默認(rèn)值

def foo(xyz, m=123, *, n='abc', t=[1, 2]):
    m = 456
    n = 'def'
    t.append(300)
    print(xyz, m, n, t)

print(id(foo), foo.__defaults__, foo.__kwdefaults__)
foo('pinginglab')
print(id(foo), foo.__defaults__, foo.__kwdefaults__)

屬性 _defaults_ 中使用元組保存所有位置參數(shù)的默認(rèn)值

屬性 _kwdefaults_ 中使用字典保存所有keyword-only參數(shù)的默認(rèn)值

def x(a=[]):
    a += [5]
    
print(x.__defaults__)
x()
x()
print(x.__defaults__)


def y(a=[]):
    a = a + [5]
    
print(y.__defaults__)
y()
y()
print(y.__defaults__)

只有一句話不同,輸出結(jié)果卻不同?

這是因?yàn)?a += [5]a = a + [5] 的區(qū)別:

  • + 表示兩個(gè)列表合并并返回一個(gè)新的列表
  • += 表示就地修改前一個(gè)列表,在其后面追加一個(gè)列表,就是相當(dāng)于 a.extend([5])

四、變量名解析原則LEGB

  • Local:本地作用域,局部作用域的local命名空間。函數(shù)調(diào)用時(shí)創(chuàng)建,調(diào)用結(jié)束消亡
  • Enclosing:python 2.2引入了嵌套函數(shù),實(shí)現(xiàn)了閉包,這個(gè)就是嵌套函數(shù)的外部函數(shù)的命名空間
  • Global:全局作用域,即一個(gè)模塊的命名空間。模塊被import時(shí)創(chuàng)建,解釋器退出時(shí)消亡
  • Build-in:內(nèi)置模塊命名空間,生命周期從python解釋器啟動(dòng)時(shí)創(chuàng)建到解釋器退出時(shí)消亡。例如print(open),print 和 open 都是內(nèi)置的函數(shù)標(biāo)識(shí)符

所以一個(gè)變量的查找順序就是LEGB。

<img src="https://gitee.com/jackyfmy/resource/raw/master/static/20200416003010.png" style="zoom: 67%;" />

五、函數(shù)的銷毀

定義一個(gè)函數(shù)就是生成一個(gè)函數(shù)對(duì)象,函數(shù)名指向的就是函數(shù)對(duì)象。

可以使用del語句刪除函數(shù),使其引用計(jì)數(shù)減一。

可以使用同名標(biāo)識(shí)符覆蓋原有定義,本質(zhì)上也是使其引用計(jì)數(shù)減一。

python程序結(jié)束時(shí),所有對(duì)象銷毀。

函數(shù)也是對(duì)象,也不例外,是否銷毀,還是看引用計(jì)數(shù)是否減為0。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容