不可或缺的自定義函數-day7

不可或缺的自定義函數

寫在前面

你好,我是禪墨!

實際工作生活中,不少初學者編寫的 Python 程序,長達幾百行的代碼中,卻沒有一個函數,通通按順序堆到一塊兒,不僅讓人讀起來費時費力,往往也是錯誤連連。

一個規範的值得借鑑的 Python 程序,除非代碼量很少(比如 10 行、20 行以下),基本都應該由多個函數組成,這樣的代碼才更加模塊化、規範化。

函數是 Python 程序中不可或缺的一部分。事實上,在前面,我們已經用到了很多 Python 的內置函數,比如 sorted() 表示對一個集合序列排序,len() 表示返回一個集合序列的長度大小等等。

函數基礎

那麼,到底什麼是函數,如何在 Python 程序中定義函數呢?

說白了,函數就是爲了實現某一功能的代碼段,只要寫好以後,就可以重複利用。我們先來看下面一個簡單的例子:


def my_func(message):
    print('Got a message: {}'.format(message))

# 調用函數 my_func()
my_func('Hello World')
# 輸出
Got a message: Hello World
  • def 是函數的聲明;
  • my_func 是函數的名稱;
  • 括號裏面的 message 則是函數的參數;
  • 而 print 那行則是函數的主體部分,可以執行相應的語句;
  • 在函數最後,你可以返回調用結果(return 或 yield),也可以不返回。

總結一下,大概是下面的這種形式:


def name(param1, param2, ..., paramN):
    statements
    return/yield value # optional

和其他需要編譯的語言(比如 C 語言)不一樣的是,def 是可執行語句,這意味着函數直到被調用前,都是不存在的。當程序調用函數時,def 語句纔會創建一個新的函數對象,並賦予其名字。

我們一起來看幾個例子,加深你對函數的印象:


def my_sum(a, b):
    return a + b

result = my_sum(3, 5)
print(result)

# 輸出
8

這裏,我們定義了 my_sum() 這個函數,它有兩個參數 a 和 b,作用是相加;隨後,調用 my_sum() 函數,分別把 3 和 5 賦於 a 和 b;最後,返回其相加的值,賦於變量 result,並輸出得到 8。

再來看一個例子:


def find_largest_element(l):
    if not isinstance(l, list):
        print('input is not type of list')
        return
    if len(l) == 0:
        print('empty input')
        return
    largest_element = l[0]
    for item in l:
        if item > largest_element:
            largest_element = item
    print('largest element is: {}'.format(largest_element)) 
      
find_largest_element([8, 1,-3, 2, 0])

# 輸出
largest element is: 8

這個例子中,我們定義了函數 find_largest_element,作用是遍歷輸入的列表,找出最大的值並打印。因此,當我們調用它,並傳遞列表 [8, 1, -3, 2, 0] 作爲參數時,程序就會輸出 largest element is: 8。

需要注意,主程序調用函數時,必須保證這個函數此前已經定義過,不然就會報錯,比如:


my_func('hello world')
def my_func(message):
    print('Got a message: {}'.format(message))
    
# 輸出
NameError: name 'my_func' is not defined

但是,如果我們在函數內部調用其他函數,函數間哪個聲明在前、哪個在後就無所謂,因爲 def 是可執行語句,函數在調用之前都不存在,我們只需保證調用時,所需的函數都已經聲明定義:


def my_func(message):
    my_sub_func(message) # 調用my_sub_func()在其聲明之前不影響程序執行
    
def my_sub_func(message):
    print('Got a message: {}'.format(message))

my_func('hello world')

# 輸出
Got a message: hello world

另外,Python 函數的參數可以設定默認值,比如下面這樣的寫法:

def func(param = 0):

這樣,在調用函數 func() 時,如果參數 param 沒有傳入,則參數默認爲 0;而如果傳入了參數 param,其就會覆蓋默認值。

前面說過,Python 和其他語言相比的一大特點是,Python 是 dynamically typed 的,可以接受任何數據類型(整型,浮點,字符串等等)。對函數參數來說,這一點同樣適用。比如還是剛剛的 my_sum 函數,我們也可以把列表作爲參數來傳遞,表示將兩個列表相連接:


print(my_sum([1, 2], [3, 4]))

# 輸出
[1, 2, 3, 4]

同樣,也可以把字符串作爲參數傳遞,表示字符串的合併拼接:


print(my_sum('hello ', 'world'))

# 輸出
hello world

當然,如果兩個參數的數據類型不同,比如一個是列表、一個是字符串,兩者無法相加,那就會報錯:

print(my_sum([1, 2], ‘hello’))
TypeError: can only concatenate list (not “str”) to list

我們可以看到,Python 不用考慮輸入的數據類型,而是將其交給具體的代碼去判斷執行,同樣的一個函數(比如這邊的相加函數 my_sum()),可以同時應用在整型、列表、字符串等等的操作中。

在編程語言中,我們把這種行爲稱爲多態。這也是 Python 和其他語言,比如 Java、C 等很大的一個不同點。當然,Python 這種方便的特性,在實際使用中也會帶來諸多問題。因此,必要時請你在開頭加上數據的類型檢查。

Python 函數的另一大特性,是 Python 支持函數的嵌套。所謂的函數嵌套,就是指函數裏面又有函數,比如:


def f1():
    print('hello')
    def f2():
        print('world')
    f2()
f1()

# 輸出
hello
world

這裏函數 f1() 的內部,又定義了函數 f2()。在調用函數 f1() 時,會先打印字符串’hello’,然後 f1() 內部再調用 f2(),打印字符串’world’。你也許會問,爲什麼需要函數嵌套?這樣做有什麼好處呢?

其實,函數的嵌套,主要有下面兩個方面的作用。

第一,函數的嵌套能夠保證內部函數的隱私。內部函數只能被外部函數所調用和訪問,不會暴露在全局作用域,因此,如果你的函數內部有一些隱私數據(比如數據庫的用戶、密碼等),不想暴露在外,那你就可以使用函數的的嵌套,將其封裝在內部函數中,只通過外部函數來訪問。比如:


def connect_DB():
    def get_DB_configuration():
        ...
        return host, username, password
    conn = connector.connect(get_DB_configuration())
    return conn

這裏的函數 get_DB_configuration,便是內部函數,它無法在 connect_DB() 函數以外被單獨調用。也就是說,下面這樣的外部直接調用是錯誤的:


get_DB_configuration()

# 輸出
NameError: name 'get_DB_configuration' is not defined

我們只能通過調用外部函數 connect_DB() 來訪問它,這樣一來,程序的安全性便有了很大的提高。第二,合理的使用函數嵌套,能夠提高程序的運行效率。我們來看下面這個例子:


def factorial(input):
    # validation check
    if not isinstance(input, int):
        raise Exception('input must be an integer.')
    if input < 0:
        raise Exception('input must be greater or equal to 0' )
    ...

    def inner_factorial(input):
        if input <= 1:
            return 1
        return input * inner_factorial(input-1)
    return inner_factorial(input)


print(factorial(5))

這裏,我們使用遞歸的方式計算一個數的階乘。因爲在計算之前,需要檢查輸入是否合法,所以我寫成了函數嵌套的形式,這樣一來,輸入是否合法就只用檢查一次。而如果我們不使用函數嵌套,那麼每調用一次遞歸便會檢查一次,這是沒有必要的,也會降低程序的運行效率。

實際工作中,如果你遇到相似的情況,輸入檢查不是很快,還會耗費一定的資源,那麼運用函數的嵌套就十分必要了。

函數變量作用域

Python 函數中變量的作用域和其他語言類似。如果變量是在函數內部定義的,就稱爲局部變量,只在函數內部有效。一旦函數執行完畢,局部變量就會被回收,無法訪問,比如下面的例子:


def read_text_from_file(file_path):
    with open(file_path) as file:
        ...

我們在函數內部定義了 file 這個變量,這個變量只在 read_text_from_file 這個函數裏有效,在函數外部則無法訪問。

相對應的,全局變量則是定義在整個文件層次上的,比如下面這段代碼:


MIN_VALUE = 1
MAX_VALUE = 10
def validation_check(value):
    if value < MIN_VALUE or value > MAX_VALUE:
        raise Exception('validation check fails')

這裏的 MIN_VALUE 和 MAX_VALUE 就是全局變量,可以在文件內的任何地方被訪問,當然在函數內部也是可以的。不過,我們不能在函數內部隨意改變全局變量的值。比如,下面的寫法就是錯誤的:


MIN_VALUE = 1
MAX_VALUE = 10
def validation_check(value):
    ...
    MIN_VALUE += 1
    ...
validation_check(5)

如果運行這段代碼,程序便會報錯:

UnboundLocalError: local variable ‘MIN_VALUE’ referenced before assignment

這是因爲,Python 的解釋器會默認函數內部的變量爲局部變量,但是又發現局部變量 MIN_VALUE 並沒有聲明,因此就無法執行相關操作。所以,如果我們一定要在函數內部改變全局變量的值,就必須加上 global 這個聲明:


MIN_VALUE = 1
MAX_VALUE = 10
def validation_check(value):
    global MIN_VALUE
    ...
    MIN_VALUE += 1
    ...
validation_check(5)

這裏的 global 關鍵字,並不表示重新創建了一個全局變量 MIN_VALUE,而是告訴 Python 解釋器,函數內部的變量 MIN_VALUE,就是之前定義的全局變量,並不是新的全局變量,也不是局部變量。這樣,程序就可以在函數內部訪問全局變量,並修改它的值了。

另外,如果遇到函數內部局部變量和全局變量同名的情況,那麼在函數內部,局部變量會覆蓋全局變量,比如下面這種:


MIN_VALUE = 1
MAX_VALUE = 10
def validation_check(value):
    MIN_VALUE = 3
    ...

在函數 validation_check() 內部,我們定義了和全局變量同名的局部變量 MIN_VALUE,那麼,MIN_VALUE 在函數內部的值,就應該是 3 而不是 1 了。

類似的,對於嵌套函數來說,內部函數可以訪問外部函數定義的變量,但是無法修改,若要修改,必須加上 nonlocal 這個關鍵字:


def outer():
    x = "local"
    def inner():
        nonlocal x # nonlocal關鍵字表示這裏的x就是外部函數outer定義的變量x
        x = 'nonlocal'
        print("inner:", x)
    inner()
    print("outer:", x)
outer()
# 輸出
inner: nonlocal
outer: nonlocal

如果不加上 nonlocal 這個關鍵字,而內部函數的變量又和外部函數變量同名,那麼同樣的,內部函數變量會覆蓋外部函數的變量。


def outer():
    x = "local"
    def inner():
        x = 'nonlocal' # 這裏的x是inner這個函數的局部變量
        print("inner:", x)
    inner()
    print("outer:", x)
outer()
# 輸出
inner: nonlocal
outer: local

閉包

這節課的第三個重點,我想再來介紹一下閉包(closure)。閉包其實和剛剛講的嵌套函數類似,不同的是,這裏外部函數返回的是一個函數,而不是一個具體的值。返回的函數通常賦於一個變量,這個變量可以在後面被繼續執行調用。

舉個例子你就更容易理解了。比如,我們想計算一個數的 n 次冪,用閉包可以寫成下面的代碼:


def nth_power(exponent):
    def exponent_of(base):
        return base ** exponent
    return exponent_of # 返回值是exponent_of函數

square = nth_power(2) # 計算一個數的平方
cube = nth_power(3) # 計算一個數的立方 
square
# 輸出
<function __main__.nth_power.<locals>.exponent(base)>

cube
# 輸出
<function __main__.nth_power.<locals>.exponent(base)>

print(square(2))  # 計算2的平方
print(cube(2)) # 計算2的立方
# 輸出
4 # 2^2
8 # 2^3

這裏外部函數 nth_power() 返回值,是函數 exponent_of(),而不是一個具體的數值。需要注意的是,在執行完square = nth_power(2)和cube = nth_power(3)後,外部函數 nth_power() 的參數 exponent,仍然會被內部函數 exponent_of() 記住。這樣,之後我們調用 square(2) 或者 cube(2) 時,程序就能順利地輸出結果,而不會報錯說參數 exponent 沒有定義了。

看到這裏,你也許會思考,爲什麼要閉包呢?上面的程序,我也可以寫成下面的形式啊!


def nth_power_rewrite(base, exponent):
    return base ** exponent

確實可以,不過,要知道,使用閉包的一個原因,是讓程序變得更簡潔易讀。設想一下,比如你需要計算很多個數的平方,那麼你覺得寫成下面哪一種形式更好呢?


# 不適用閉包
res1 = nth_power_rewrite(base1, 2)
res2 = nth_power_rewrite(base2, 2)
res3 = nth_power_rewrite(base3, 2)
...

# 使用閉包
square = nth_power(2)
res1 = square(base1)
res2 = square(base2)
res3 = square(base3)
...

顯然是第二種,是不是?首先直觀來看,第二種形式,讓你每次調用函數都可以少輸入一個參數,表達更爲簡潔。

其次,和上面講到的嵌套函數優點類似,函數開頭需要做一些額外工作,而你又需要多次調用這個函數時,將那些額外工作的代碼放在外部函數,就可以減少多次調用導致的不必要的開銷,提高程序的運行效率。

另外還有一點,我們後面會講到,閉包常常和裝飾器(decorator)一起使用。

總結

  1. Python 中函數的參數可以接受任意的數據類型,使用起來需要注意,必要時請在函數開頭加入數據類型的檢查;
  2. 和其他語言不同,Python 中函數的參數可以設定默認值;
  3. 嵌套函數的使用,能保證數據的隱私性,提高程序運行效率;
  4. 合理地使用閉包,則可以簡化程序的複雜度,提高可讀性。

寫在後面

今天發的晚了一點!
堅持一週了,值得慶祝一下!
基礎篇已經進行了一大半了,哇哈哈哈~~
有什麼需要改進的,歡迎私信我哦

CSDN:禪墨雲
知乎: 禪墨雲
個人博客網站:禪墨雲

這裏是引用

微信公衆號:興趣路人甲

這裏是引用

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