關於python函數機制,看完這一篇長文就夠了!

目錄

1、什麼是“函數”

2、函數的語法

2.1、語法定義

2.2、形參與實參

2.3、值傳遞和引用傳遞

2.4、必選參數和可選參數

2.5、位置參數和關鍵字參數

2.6、變長參數

2.7、解包參數列表

3、lambda

4、嵌套和閉包


本文有視頻講解,視頻和實例源碼下載方式:點擊->我的主頁,查看個人簡介。

我儘量堅持每日更新一節。

更多python教程,請查看我的專欄《0基礎學python視頻教程》

 

1、什麼是“函數”

前面我們講過,所謂“程序”,就是通過邏輯去控制一系列數據按照預期處理,從而達成我們需要的某個特定功能。所以,理論上,我們只需要前面學的數據結構和控制語句,我們就可以寫所有的“程序”了。

但是,現實中,我們的程序要複雜得多。拿我平時工作中的產品爲例,其完整的代碼量可達數百萬行。如何組織這些代碼?如何更好的維護這些代碼?如何讓幾十幾百名工程師同時開發這些代碼?如何在這些代碼基礎上新增功能?等等,這些問題會比代碼語法本身更重要。解決這些問題的本質,是要對代碼進行合理的“抽象”和“封裝”,使其具備一定的組織結構,這就是所謂的“架構”。

作爲高級編程語言,python必須提供一些機制,以便於我們實現代碼“架構”。函數,就是這些機制中最基礎的一個。

接下來我們會花很多章節來學習這些機制,包括函數、模塊、包、類等等。

 

我們以實現list求和這樣一個簡單的功能爲例:

#  author: Tiger,    關注公衆號“跟哥一起學python”,ID:tiger-python

# file: ./9/9_1.py

# 實現 list 求和
list_1 = [100, 200, 300, 400]
list_sum = 0

for item in list_1:
    list_sum += item

print(list_sum)

上面一段代碼,實現了對list_1這個列表中所有元素的求和。沒有任何問題。代碼很清晰簡潔。下面我們對這個功能進行一些擴充,我們增加一個列表list_2,也需要求和。實現如下:

#  author: Tiger,       關注公衆號“跟哥一起學python”,ID:tiger-python

# file: ./9/9_1.py

# 實現 list 求和
list_1 = [100, 200, 300, 400]
list_2 = list(range(10, 100, 5))
list_sum = 0

for item in list_1:
    list_sum += item

print(list_sum)

list_sum = 0
for item in list_2:
    list_sum += item

print(list_sum)

也能實現我們需要的功能,可是這段代碼總讓人看着不那麼完美,不那麼優美。裏面兩段通過for循環求和的代碼塊,非常類似。試想,如果我們想得到多個list的和,這段代碼是不是會一直重複寫下去?

基於這個簡單的例子,我們來理解“抽象”和“封裝”的概念。

“抽象”,就是提取出代碼中的一部分“共性”邏輯或者功能。

“封裝”,就是將這些“共性”邏輯或者功能組織在一起,並提供給別人使用。

 

 

抽象和封裝,是作爲程序員必須具備的基本能力和基本素質。不論你是初級程序員,還是架構師,這兩項能力都始終貫穿我們的工作中。

這個例子,我們可以通過函數,來實現對求和邏輯的封裝。

 

通過封裝函數func_list_sum(),我們實現了求和邏輯的複用,它不僅可以給list1、list2使用,甚至可以給所有的列表使用。代碼的複用,可以給我們帶來很多好處,比如:代碼更加簡潔、提升開發效率、提升代碼可靠性等。

經過函數封裝後,這個實例實現如下:

#  author: Tiger,        關注公衆號“跟哥一起學python”,ID:tiger-python

# file: ./9/9_1.py


def func_list_sum(l):
    """
    實現 list 求和
    """
    s = 0
    for item in l:
        s += item
    return s


list_1 = [100, 200, 300, 400]
list_2 = list(range(10, 100, 5))

print(func_list_sum(list_1))
print(func_list_sum(list_2))

封裝可以讓我們的代碼很方便的擴展,比如我們在求和時需要檢查成員是否是整數,函數封裝後,我們只需要修改這個函數裏面的代碼即可擴展該功能。

#  author: Tiger,        關注公衆號“跟哥一起學python”,ID:tiger-python

# file: ./9/9_1.py


def func_list_sum(l):
    """
    實現 list 求和
    """
    s = 0
    for item in l:
        if type(item) is not int:  # 必須是整數
            continue
        s += item
    return s


list_1 = [100, 200, 300, 400]
list_2 = list(range(10, 100, 5))

print(func_list_sum(list_1))
print(func_list_sum(list_2))

 

2、函數的語法

2.1、語法定義

函數的語法定義如下:

def func_nameparams…:

    func_body

‘def’是定義函數的保留字,func_name是函數名,其命名規則和變量名是一致的。後面的小括號裏面是該函數對應的入參列表,多個參數用逗號隔開,可以不帶參數。關於入參的寫法,下面我們會詳細介紹。小括號後面是冒號。func_body是函數體,可以爲空語句pass,或者多語句組成的代碼塊,甚至是一個函數(後面章節會講函數的嵌套)。

 

2.2、形參與實參

形參和實參的概念,在所有高級編程語言中都類似。

形參:形式上的參數,它是我們在定義函數時用到的參數;

實參:實際的參數,它是我們在真正調用函數時傳遞的參數。

 

下面我們通過一個函數來生成著名的Fibonacci (斐波那契)數列:

#  author: Tiger,      關注公衆號“跟哥一起學python”,ID:tiger-python

# file: ./9/9_2.py


def get_fib(max):
    """
    打印斐波那契數列 Fibonacci
    :param max:
    :return:
    """
    a, b = 0, 1
    while a < max:
        print(a, end=' ')
        a, b = b, a + b
    print()


input_max = int(input("input max number for Fibonacci:"))

print("\n1Fibonacci is:")
get_fib(input_max)

 

輸出爲:

input max number for Fibonacci:1000

 

Fibonacci is:

0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987

 

這個例子中,我們定義了函數get_fib(max)。在定義這個函數的時候,參數max它並沒有實際意義上的值,也沒有給它分配對象空間,它就是所謂的“形參”。

下面這行代碼調用這個函數:

get_fib(input_max)

傳入了變量input_max,它是一個真正的變量,是有對象空間的,它被稱爲“實參”。

 

2.3、值傳遞和引用傳遞

這個概念我們講可變數據類型和不可變數據類型的時候提到過。

值傳遞(pass-by-value):傳遞給函數的,是變量對應的值。函數裏面會重新分配一個對象,並將該值拷貝過去。函數裏面對新對象進行操作,與原變量無關。不可變數據類型,採用值傳遞的方式,比如字符串、數字、元組、不可變集合、字節等。值傳遞方式,需要使用return來返回結果。

引用傳遞(pass-by-reference):傳遞給函數的,是變量指向對象的引用(CPython中就是內存地址)。函數裏面直接對這個對象進行操作,會直接影響原變量。可變數據類型,採用引用傳遞的方式,比如列表、字典、可變集合等。

值傳遞和引用傳遞,取決於變量指向的對象數據類型。同一個函數的參數列表中,可以同時包含值傳遞和引用傳遞。

這裏不再舉例,大家可以回過頭去看看可變數據類型和不可變數據類型章節的例子。

 

2.4、必選參數和可選參數

有時候,我們希望調用函數時,只傳入一些必需的參數,而忽略其它參數。比如我們前面使用到的很多python內建函數(built-in)都是這樣的。

def print(self, *args, sep=' ', end='\n', file=None): # known special case of print

    """
    print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)
   
    Prints the values to a stream, or to sys.stdout by default.
    Optional keyword arguments:
    file:  a file-like object (stream); defaults to the current sys.stdout.
    sep:   string inserted between values, default a space.
    end:   string appended after the last value, default a newline.
    flush: whether to forcibly flush the stream.
    """
    pass

這是print函數的定義,它有很多參數,但通常我們只傳遞一個字符串進去,或者再追加一個end參數,其它參數都沒有填。

所以,我們定義的函數參數列表,是可以支持可選參數和必選參數。Python並沒有提供方法讓我們指定某個參數是否可選或者必選,而是通過設置參數的默認值,來達到此效果。

下面這個例子是官方文檔裏面的:

def ask_ok(prompt, retries=4, reminder='Please try again!'):

    while True:

        ok = input(prompt)

        if ok in ('y', 'ye', 'yes'):

            return True

        if ok in ('n', 'no', 'nop', 'nope'):

            return False

        retries = retries - 1

        if retries < 0:

            raise ValueError('invalid user response')

        print(reminder)

函數ask_ok有三個參數,第一個參數prompt是必選參數,後面兩個指定了缺省值,是可選參數。

這個函數可以通過幾種方式調用:

  1. 只給出必需的參數:ask_ok('Do you really want to quit?')
  2. 給出一個可選的參數:ask_ok('OK to overwrite the file?', 2)
  3. 或者給出所有的參數:ask_ok('OK to overwrite the file?', 2, 'Come on, only yes or no!')

 

函數參數的默認值可以指定爲變量,但是其只會在函數定義時執行一次。如下面的實例:

#  author: Tiger,        關注公衆號“跟哥一起學python”,ID:tiger-python

# file: ./9/9_3.py

# 參數默認值
x = 100

def func_test(a, b=x):  # 參數b的默認值會設置爲100,後面x改變不會隨着變
    return a + b

x = 200
print(func_test(200))

輸出爲:

300

 

參數b在函數定義時,設置了缺省值爲x也就是100,後面x改變爲200,b的缺省值還是100。

如果參數的缺省值是一個可變數據類型,它會怎麼樣呢?再看下面的實例:

# 參數默認值爲可變數據類型

def list_test(a, b=[]):
    print(id(b))
    b.append(a)
    return b

print(list_test(300))
print(list_test(400))

print(list_test(500, [800]))
print(list_test(600))

輸出爲:

2658215980992

[300]

2658215980992

[300, 400]

2658217265856

[800, 500]

2658215980992

[300, 400, 600]

Process finished with exit code 0

還記得前面講的引用傳遞嗎?如果參數b缺省值設置爲一個list,那麼b缺省指向的是一個對象的引用。即使b=[ ],系統也會給它創建一個空的列表對象,並把其引用賦給它。

上面例子還有一個有意思的地方,當我們給b傳遞了一個實參[800 ],b指向了一個新的對象。之後,我們再次只傳遞實參a,b採用缺省值,我們發現系統還能記住之前的缺省值。這說明,python函數處理機制中,一旦給參數設置了缺省值,那麼這個值會一直存在。

對於這種可變類型作爲缺省值的情況,如果你不想讓這個缺省值被共享下去,可以將參數的缺省值設置爲None。None是一個空對象。如下實例:

# 參數默認值爲None

print('set to None'.center(30, '-'))

def list_test(a, b=None):
    if b is None:
        b = []
    print(id(b))
    b.append(a)
    return b

print(list_test(300))
print(list_test(400))

輸出爲:

---------set to None----------

2176805826496

[300]

2176805826496

[400]

可以看出,當函數判斷b是缺省值None時,會執行b=[ ],這將b重新初始化爲一個空列表。這樣,重複多次調用函數時,b的值不會相互影響。

 

上面函數中,我們只有兩個參數,第一個沒有缺省值,第二個有缺省值。如果我們把兩個參數交換一下順序,會出現什麼情況呢?

答案是編譯時會報錯,如下:

  File "D:/跟我一起學python/練習/9/9_3.py", line 29

    def list_test(b=None, a):

                  ^

SyntaxError: non-default argument follows default argument

錯誤是:非缺省值的參數不能放在缺省值參數後面。

Python設計這樣的規則是有原因的。因爲帶缺省值的參數,在我們調用函數傳遞實參時,是可選的。如果我們把這些可選參數放在了必選參數之前,那麼python將不知道實參和形參的對應關係。比如一個函數定義爲:

def func_test(arg1=None, arg2, arg3=None):

    pass

如果我們這樣調用這個函數:

func_test(a, b)

python解釋器不知道你傳遞的實參a對應的是arg1,還是對應的arg2,因爲arg1是可選的。

所以,python制定了規則:必選參數列表必須放在可選參數列表前面。

 

這並沒有完全解決所有的問題,如果我們有多個可選參數,也會出現問題。比如一個函數定義爲:

def func_test(arg1, arg2=None, arg3=None):

    pass

如果我們這樣調用這個函數:

func_test(a, b)

python解釋器會認爲b對應的是arg2,而可能我們想傳遞的是arg3。

這裏就得用到下節講的關鍵字參數。

 

2.5、位置參數和關鍵字參數

事實上,python有兩種方式來映射實參和形參:位置參數(position arguments)和關鍵字參數(keyword arguments)。

 

  • 位置參數

按照實參和形參的位置順序依次映射。

 

  • 關鍵字參數

調用函數時,採用“形參=實參”這種鍵值對的方式指明映射關係。採用關鍵字參數時,其順序不需要按照形參定義的順序。

 

位置參數和關鍵字參數是混用的,一次函數調用可以同時使用這兩種方式。

 

對於上節的例子,我們可以採用關鍵字參數的方式來明確指定arg3:

func_test(a, arg3=b)

這樣就不會出現歧義。

 

我們看一下參數比較多的例子:

def open(file, mode='r', buffering=None, encoding=None, errors=None, newline=None, closefd=True):

pass

這是python的內建函數,用於打開一個文件,它除了第一個參數file是必選,其它都是可選參數。以下的調用方式都是正確的:

f = open('hello.txt')

f = open('hello.txt', 'w')
f = open('hello.txt', encoding='utf-8', newline='\n')
f = open(file='hello.txt', encoding='utf-8', newline='\n')

但是下面的調用方式是錯誤的:

f = open(file='hello.txt', '\n', encoding='utf-8')  # error

會報錯:

File "D:/跟我一起學python/練習/9/9_4.py", line 16

    f = open(file='hello.txt', '\n', encoding='utf-8')  # error

                               ^

SyntaxError: positional argument follows keyword argument

在傳實參時,位置參數必須在關鍵字參數之前。

 

Python還提供了*和/方式來強制某些形參只能採用位置參數或者只能採用關鍵字參數。

def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):

      -----------      ----------      ----------

        |                 |                  |

        |        Positional or keyword   |

        |                                     - Keyword only

         -- Positional only

/之前的參數只能採用位置參數;

*之後的參數只能採用關鍵字參數;

兩者之間的參數不限制,可以採用任意方式。

 

看下面的實例:

#  author: Tiger,       關注公衆號“跟哥一起學python”,ID:tiger-python

# file: ./9/9_3.py

# 位置參數和關鍵字參數
def my_echo(name, age, /, sex, *, city):
    print(name, age, sex, city)

my_echo('xiaowang', 24, 'male', city='Beijing')  # ok
my_echo('xiaowang', 24, 'male', 'Beijing')  # error
my_echo('xiaowang', age=24, 'male', city='Beijing')  # error

後面兩種調用方法是錯誤的,會報語法錯誤。

 

參數列表中不能出現多個*或者/,如下的定義是錯誤的:

def my_echo(name, /, age, /, sex, *, city):

    print(name, age, sex, city)

 

2.6、變長參數

有些函數,我們需要它的參數是變長的,也就是說它的參數個數是不確定的。Python提供了實現變長參數的機制。

在定義形參時,採用*或者**來定義變長參數。

*定義的形參,實參必須採用位置參數的方式傳遞;

**定義的形參,實參必須採用關鍵字參數的方式傳遞。

 

我們來看一個例子:

#  author: Tiger,        關注公衆號“跟哥一起學python”,ID:tiger-python

# file: ./9/9_5.py

# 變長參數
def goods_attrs(name, **kwargs):
    print(f"name: {name}, attributes: {kwargs}")

goods_attrs("跟哥一起學python", pages=300, price=45, version='2.0')

def courses(student, *args):
    print(f"name: {student}, 選修課程: {args}")

courses("小王", '高等數學', '計算機科學', '信號與系統', '英語')

輸出爲:

name: 跟哥一起學python, attributes: {'pages': 300, 'price': 45, 'version': '2.0'}

name: 小王, 選修課程: ('高等數學', '計算機科學', '信號與系統', '英語')

 

可以看到,**kwargs會自動將傳入的實參打包爲一個字典類型的對象,函數內部可以直接使用這個對象。同理,*args會自動將傳入的實參打包爲一個元組類型對象,函數內部也可以直接使用。

在函數定義時,**kwargs後面不能再有其它形參,而*args後面可以其它形參,但是必須採用關鍵字參數的方式來傳遞實參。這一點也比較好理解,因爲*args和**kwargs都是可變長參數,其參數個數是不確定的,如果後面有其它形參,那麼必須要能區分開。因爲*args要求必須是位置參數,所以在它後面的形參可以用關鍵字參數予以區分。但是**kwargs本身就是關鍵字參數,那麼它後面無論怎麼傳遞參數都無法區分。

 

大家想過一個問題嗎?**kwargs採用的是關鍵字參數,那麼如果關鍵字和前面定義的其它參數名重複了,怎麼辦呢?比如前面的例子,我們這樣調用就會出錯:

goods_attrs("跟哥一起學python", name="hello", pages=300, price=45, version='2.0')

輸出爲:

Traceback (most recent call last):

  File "D:/跟我一起學python/練習/9/9_5.py", line 15, in <module>

    goods_attrs("跟哥一起學python", name="hello", pages=300, price=45, version='2.0')

TypeError: goods_attrs() got multiple values for argument 'name'

因爲無論如何,python都會認爲name是我們定義的第一個形參,這樣調用它會認爲是參數重複了。

爲了解決這個問題,上一節我們講到的/語法就可以派上用場了,我們可以限定那麼參數必須採用位置參數的方式傳遞實參,這樣系統就能區分開。

def goods_attrs(name, /, **kwargs):

print(f"name: {name}, attributes: {kwargs}")



goods_attrs("跟哥一起學python", name="hello", pages=300, price=45, version='2.0')

輸出爲:

name: 跟哥一起學python, attributes: {'name': 'hello', 'pages': 300, 'price': 45, 'version': '2.0'}

 

變長參數的極端用法,是全部參數都是變長,比如我們定義一個函數,用於計算所有入參的平均數:

def calc_avrg(*args):

    total = 0
    for item in args:
        total += item
    else:
        if len(args) == 0:
            return 0
        else:
            return total / len(args)

print("average is: %.2f" % (calc_avrg()))
print("average is: %.2f" % (calc_avrg(1, 2, 100, 29, 39, 19, 30, 29)))

輸出爲:

average is: 0.00

average is: 31.12

 

2.7、解包參數列表

*和**同樣可以用於在傳遞實參時解包參數列表。

比如上一節的例子,我們也可以這樣調用:

# 解包參數列表

courses("小王", *('高等數學', '計算機科學', '信號與系統', '英語'))
goods_attrs("跟哥一起學python", **{'name':"hello", 'pages':300, 'price':45, 'version':'2.0'})

 

*是用於將元組解包爲一個一個成員;

**是用於將字典解包爲一個一個鍵值對。

它和我們上一節講的變長參數的過程剛好相反。

 

但是,需要注意的是,它們用於解包,不一定非得是變長參數。

def func_add(x, y):

    return x + y

print(func_add(1, 5))  # 直接賦值

tmp = (1, 5)
print(func_add(*tmp))  # 使用*解包

tmp_dict = {'y': 5, 'x': 1}
print(func_add(**tmp_dict))  # 使用**解包

 

 

3、lambda

有時候,某些函數會非常簡單,簡單得我們都懶得給它起名字,這種沒有名字的函數叫“匿名函數”。Python中採用lambda語法來定義匿名函數,其語法如下:

lambda [arg1 [,arg2,.....argn]]:expression

首先是保留字lambda,後面跟0個或者多個參數列表,隨後是冒號:,冒號後面跟一個表達式。

注意,expression只能是一個表達式,這個表達式的值會被缺省通過return返回出去。所以,lambda無法寫複雜邏輯,它非常簡單。

Lambda通常用於作爲一些函數的實參,比如下面官方手冊的一個例子:

#  author: Tiger,       關注公衆號“跟哥一起學python”,ID:tiger-python

# file: ./9/9_6.py

# lambda函數
list_1 = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]

list_1.sort(key=lambda item: item[1])
print(list_1)

輸出爲:

[(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')]

參數key是一個函數類型,這個函數需要返回一個用於排序的值。我們定義了一個lambda函數,它返回了成員元組的第二個值。

我們讓這種函數匿名,就是因爲它們太簡單了,並且沒有太大的複用價值。畢竟起名有時候也挺頭痛的。但是,這並不意味着匿名函數就不能複用,看看下面的例子:

# lambda函數

list_1 = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
list_2 = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
key_func = lambda item: item[1]
list_1.sort(key=key_func)
list_2.sort(key=key_func)
print(list_1)
print(list_2)

這個例子中,我們定義了一個變量key_func,這個變量指向了lambda函數(萬物皆對象,函數也是對象)。這個變量可以重複使用的,lambda函數被變相複用了。

這種使用方法,是被python官方所鄙視的。因爲你既然都定義變量了,爲啥不直接定義一個def的函數呢?這種情況下就不要用lambda了。

在很多公司的編程規範中,是不建議程序員使用或者大量使用lambda的,因爲它會影響程序的可讀性。下面的代碼你是否能一下子看明白是什麼意思:

lower = (lambda x, y: x if x < y else y)

lambda的功能可以完全使用def函數替代。我們使用lambda時,一定要保證其表達式是非常簡單明確且易讀的,否則就老老實實採用def函數的方式吧。

 

4、嵌套和閉包

函數是可以嵌套的,包括lambda也是可以嵌套的。

 

#  author: Tiger,        關注公衆號“跟哥一起學python”,ID:tiger-python

# file: ./9/9_7.py

# 函數嵌套
def calc_avrg(*args):
    def calc_sum(*args1):
        total = 0
        for item in args1:
            total += item
        else:
            return total

    tmp_total = calc_sum(*args)
    return tmp_total / len(args) if len(args) > 0 else 0


list_1 = [100, 200, 3, 4, 55]
print(calc_avrg(*list_1))

# lambda嵌套
y = lambda N: (lambda x: N * x)
func = y(2)
print(func(2))  # 輸出 4

上面第一個函數calc_avrg的內部又定義了一個函數calc_sum,形成了嵌套關係。在calc_avrg之外是不能調用calc_sum的,也就是說calc_sum的作用域僅限於calc_avrg內部。

第二個函數是一個lambda函數,它的表達式也是一個lambda函數,這就形成了函數嵌套關係。有趣的是,它返回的是一個lambda函數對象。這樣,我們在外部是可以調用內部嵌套的lambda函數。這種機制我們稱之爲“閉包(closure)”

 

閉包的概念理解起來會稍顯困難。我們對前面的def函數稍作改造:

# 函數嵌套

def calc_avrg_1(*args_outer):
    tmp_total = 0

    def calc_avrg_1_inner(*args):
        nonlocal tmp_total  # 這裏的tmp_total採用外層變量
        for item in args:
            tmp_total += item
        else:
            return tmp_total / len(args) if len(args) > 0 else 0

    return calc_avrg_1_inner(*args_outer)


list_1 = [100, 200, 3, 4, 55]
print(calc_avrg_1(*list_1))
print(calc_avrg_1(*list_1))
print(calc_avrg_1(*list_1))

輸出爲:

72.4

72.4

72.4

我們在外層函數定義了變量tmp_total,內層函數也使用該變量。我們調用了3次calc_avrg,最後的結果都是一樣的72.4。你肯定會說,這不廢話嗎?同一個函數,相同的實參,輸出的結果肯定一樣啊!

但是大家想過沒有,這三次調用同一個函數,爲什麼相互之間互不影響呢?原因在於,我們每次調用函數時,系統會給這個函數分配獨立的上下文運行環境,這裏面保存了本次調用的各種參數、臨時變量等等。所以,我們每次調用除了代碼段是一樣的,運行環境都是獨立的。

“閉包”機制改變了這一切!

我們再對這個函數進行改造,如下:

def calc_avrg():

    tmp_total = 0

    def calc_avrg_inner(*args):
        nonlocal tmp_total  # 這裏的tmp_total採用外層變量
        for item in args:
            tmp_total += item
        else:
            return tmp_total / len(args) if len(args) > 0 else 0

    return calc_avrg_inner


list_1 = [100, 200, 3, 4, 55]
func_calc = calc_avrg()
print(func_calc(*list_1))
print(func_calc(*list_1))
print(func_calc(*list_1))

輸出爲:

72.4

144.8

217.2

我們把這個函數改成了閉包的形式,calc_avrg()返回的是其內部函數calc_avrg_inner的對象引用。這樣,外部可以直接調用calc_avrg_inner。我們看到,調用三次,每次的結果都不一樣,就像tmp_total被共享了一樣。沒錯,形成閉包之後,其臨時變量會被共享,每次函數調用之間會相互影響。

形成閉包需要有兩個條件:

  1. 外部引用了內部嵌套函數對象;
  2. 內部函數引用了外層變量。

 

形成閉包之後,一個函數實例func_calc會攜帶一個屬性func_calc.__closure__,這個屬性就是閉包屬性,它裏面存儲了外層變量的引用,並且在實例化之後不能被修改。

所以,我們看到tmp_total的值在多次調用過程中被共享了。如果我們多創建幾個函數實例,它們相互之間是不會影響的。

 

list_1 = [100, 200, 3, 4, 55]

func_calc = calc_avrg()
print(func_calc(*list_1))
print(func_calc(*list_1))
func_calc2 = calc_avrg()
print(func_calc2(*list_1))

輸出爲:

72.4

144.8

72.4

func_calc和func_calc2創建了兩個函數實例,它們之間的閉包屬性是獨立的,互不影響。我們可以斷點查看閉包屬性,如下:

cell_contents裏面存的就是閉包函數引用的變量,每個實例會創建一個獨立的,並且在每次調用共享。

對於沒有閉包的函數calc_avrg_1,它的閉包屬性爲空None。

 

這就是閉包的概念,有點不好理解。閉包有啥用呢?其實我們後面要講到的裝飾器Decorator就是用到了閉包原理。

 


本文有視頻講解,視頻和實例源碼下載方式:點擊->我的主頁,查看個人簡介。

我儘量堅持每日更新一節。

更多python教程,請查看我的專欄《0基礎學python視頻教程》

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章