戳藍字“聽風python”關注我們哦!
本闆塊文章取自GitHub的100天練就python高手計劃,循序漸進,逐漸學習,适合小白入門。
更多的python教學資源,視訊,電子書資料請到公衆号菜單欄擷取,如果連結失效請聯系我。這裡我推薦《全民一起玩python》系列的視訊,在公衆号發的資源中也有,我是看這個開始學習的,講解的很生動透徹有趣,都不像是程式設計課了,基礎的python安裝檢視之前的文章:python安裝教程
上期回顧:100天python計劃-Day3分支結構
在講解本章節的内容之前,我們先來研究一道數學題,請說出下面的方程有多少組正整數解。
事實上,上面的問題等同于将8個蘋果分成四組每組至少一個蘋果有多少種方案。想到這一點問題的答案就呼之欲出了。
可以用Python的程式來計算出這個值,代碼如下所示。
"""輸入M和N計算C(M,N)m = int(input('m = '))n = int(input('n = '))fm = 1for num in range(1, m + 1):fm *= numfn = 1for num in range(1, n + 1):fn *= numfm_n = 1for num in range(1, m - n + 1):fm_n *= numprint(fm // fn // fm_n)
函數的作用
不知道大家是否注意到,在上面的代碼中,我們做了3次求階乘,這樣的代碼實際上就是重複代碼。程式設計大師Martin Fowler先生曾經說過:“代碼有很多種壞味道,重複是最壞的一種!”,要寫出高品質的代碼首先要解決的就是重複代碼的問題。對于上面的代碼來說,我們可以将計算階乘的功能封裝到一個稱之為“函數”的功能子產品中,在需要計算階乘的地方,我們隻需要“調用”這個“函數”就可以了。
定義函數
在Python中可以使用
def
關鍵字來定義函數,和變量一樣每個函數也有一個響亮的名字,而且命名規則跟變量的命名規則是一緻的。在函數名後面的圓括号中可以放置傳遞給函數的參數,這一點和數學上的函數非常相似,程式中函數的參數就相當于是數學上說的函數的自變量,而函數執行完成後我們可以通過
return
關鍵字來傳回一個值,這相當于數學上說的函數的因變量。
在了解了如何定義函數後,我們可以對上面的代碼進行重構,所謂重構就是在不影響代碼執行結果的前提下對代碼的結構進行調整,重構之後的代碼如下所示。
"""輸入M和N計算C(M,N)def fac(num):"""求階乘"""result = 1for n in range(1, num + 1):result *= nreturn resultm = int(input('m = '))n = int(input('n = '))# 當需要計算階乘的時候不用再寫循環求階乘而是直接調用已經定義好的函數print(fac(m) // fac(n) // fac(m - n))
說明: Python的子產品中其實已經有一個名為
math
函數實作了階乘運算,事實上求階乘并不用自己定義函數。下面的例子中,我們講的函數在Python标準庫已經實作過了,我們這裡是為了講解函數的定義和使用才把它們又實作了一遍,實際開發中并不建議做這種低級的重複勞動。
factorial
函數的參數
函數是絕大多數程式設計語言中都支援的一個代碼的"建構塊",但是Python中的函數與其他語言中的函數還是有很多不太相同的地方,其中一個顯著的差別就是Python對函數參數的處理。在Python中,函數的參數可以有預設值,也支援使用可變參數,是以Python并不需要像其他語言一樣支援函數的重載,因為我們在定義一個函數的時候可以讓它有多種不同的使用方式,下面是兩個小例子。
from random import randintdef roll_dice(n=2):"""搖色子"""total = 0for _ in range(n):total += randint(1, 6)return totaldef add(a=0, b=0, c=0):"""三個數相加"""return a + b + c# 如果沒有指定參數那麼使用預設值搖兩顆色子print(roll_dice())# 搖三顆色子print(roll_dice(3))print(add())print(add(1))print(add(1, 2))print(add(1, 2, 3))# 傳遞參數時可以不按照設定的順序進行傳遞print(add(c=50, a=100, b=200))
我們給上面兩個函數的參數都設定了預設值,這也就意味着如果在調用函數的時候如果沒有傳入對應參數的值時将使用該參數的預設值,是以在上面的代碼中我們可以用各種不同的方式去調用
add
函數,這跟其他很多語言中函數重載的效果是一緻的。
其實上面的
add
函數還有更好的實作方案,因為我們可能會對0個或多個參數進行加法運算,而具體有多少個參數是由調用者來決定,我們作為函數的設計者對這一點是一無所知的,是以在不确定參數個數的時候,我們可以使用可變參數,代碼如下所示。
# 在參數名前面的*表示args是一個可變參數def add(*args):total = 0for val in args:total += valreturn total# 在調用add函數時可以傳入0個或多個參數print(add())print(add(1))print(add(1, 2))print(add(1, 2, 3))print(add(1, 3, 5, 7, 9))
用子產品管理函數
對于任何一種程式設計語言來說,給變量、函數這樣的辨別符起名字都是一個讓人頭疼的問題,因為我們會遇到命名沖突這種尴尬的情況。最簡單的場景就是在同一個.py檔案中定義了兩個同名函數,由于Python沒有函數重載的概念,那麼後面的定義會覆寫之前的定義,也就意味着兩個函數同名函數實際上隻有一個是存在的。
def foo():print('hello, world!')def foo():print('goodbye, world!')# 下面的代碼會輸出什麼呢?foo()
當然上面的這種情況我們很容易就能避免,但是如果項目是由多人協作進行團隊開發的時候,團隊中可能有多個程式員都定義了名為
foo
的函數,那麼怎麼解決這種命名沖突呢?答案其實很簡單,Python中每個檔案就代表了一個子產品(module),我們在不同的子產品中可以有同名的函數,在使用函數的時候我們通過
import
關鍵字導入指定的子產品就可以區分到底要使用的是哪個子產品中的
foo
函數,代碼如下所示。
module1.py
def foo():print('hello, world!')
module2.py
def foo():print('goodbye, world!')
test.py
from module1 import foo# 輸出hello, world!foo()from module2 import foo# 輸出goodbye, world!foo()
也可以按照如下所示的方式來區分到底要使用哪一個
foo
函數。
test.py
import module1 as m1import module2 as m2m1.foo()m2.foo()
但是如果将代碼寫成了下面的樣子,那麼程式中調用的是最後導入的那個
foo
,因為後導入的foo覆寫了之前導入的
foo
。
test.py
from module1 import foofrom module2 import foo# 輸出goodbye, world!foo()
test.py
from module2 import foofrom module1 import foo# 輸出hello, world!foo()
需要說明的是,如果我們導入的子產品除了定義函數之外還中有可以執行代碼,那麼Python解釋器在導入這個子產品時就會執行這些代碼,事實上我們可能并不希望如此,是以如果我們在子產品中編寫了執行代碼,最好是将這些執行代碼放入如下所示的條件中,這樣的話除非直接運作該子產品,if條件下的這些代碼是不會執行的,因為隻有直接執行的子產品的名字才是"__main__"。
module3.py
def foo():passdef bar():pass# __name__是Python中一個隐含的變量它代表了子產品的名字# 隻有被Python解釋器直接執行的子產品的名字才是__main__if __name__ == '__main__':print('call foo()')foo()print('call bar()')bar()
test.py
import module3# 導入module3時 不會執行子產品中if條件成立時的代碼 因為子產品的名字是module3而不是__main__
練習
練習1:實作計算求最大公約數和最小公倍數的函數。
參考答案:
def gcd(x, y):"""求最大公約數"""
(x, y) = (y, x) if x > y else (x, y)for factor in range(x, 0, -1):if x % factor == 0 and y % factor == 0:return factordef lcm(x, y):"""求最小公倍數"""return x * y // gcd(x, y)
練習2:實作判斷一個數是不是回文數的函數。
參考答案:
def is_palindrome(num):"""判斷一個數是不是回文數"""temp = numtotal = 0while temp > 0:total = total * 10 + temp % 10temp //= 10return total == num
練習3:實作判斷一個數是不是素數的函數。
參考答案:
def is_prime(num):"""判斷一個數是不是素數"""for factor in range(2, int(num ** 0.5) + 1):if num % factor == 0:return Falsereturn True if num != 1 else False
練習4:寫一個程式判斷輸入的正整數是不是回文素數。
參考答案:
if __name__ == '__main__':num = int(input('請輸入正整數: '))if is_palindrome(num) and is_prime(num):print('%d是回文素數' % num)
注意:通過上面的程式可以看出,當我們将代碼中重複出現的和相對獨立的功能抽取成函數後,我們可以組合使用這些函數來解決更為複雜的問題,這也是我們為什麼要定義和使用函數的一個非常重要的原因。
變量的作用域
最後,我們來讨論一下Python中有關變量作用域的問題。
def foo():b = 'hello'# Python中可以在函數内部再定義函數def bar():c = Trueprint(a)print(b)print(c)bar()# print(c) # NameError: name 'c' is not definedif __name__ == '__main__':a = 100# print(b) # NameError: name 'b' is not definedfoo()
上面的代碼能夠順利的執行并且列印出100、hello和True,但我們注意到了,在
bar
函數的内部并沒有定義
a
和
b
兩個變量,那麼
a
和
b
是從哪裡來的。我們在上面代碼的
if
分支中定義了一個變量
a
,這是一個全局變量(global variable),屬于全局作用域,因為它沒有定義在任何一個函數中。在上面的
foo
函數中我們定義了變量
b
,這是一個定義在函數中的局部變量(local variable),屬于局部作用域,在
foo
函數的外部并不能通路到它;但對于
foo
函數内部的
bar
函數來說,變量
b
屬于嵌套作用域,在
bar
函數中我們是可以通路到它的。
bar
函數中的變量
c
屬于局部作用域,在
bar
函數之外是無法通路的。事實上,Python查找一個變量時會按照“局部作用域”、“嵌套作用域”、“全局作用域”和“内置作用域”的順序進行搜尋,前三者我們在上面的代碼中已經看到了,所謂的“内置作用域”就是Python内置的那些辨別符,我們之前用過的
input
、
print
、
int
等都屬于内置作用域。
再看看下面這段代碼,我們希望通過函數調用修改全局變量
a
的值,但實際上下面的代碼是做不到的。
def foo():a = 200print(a) # 200if __name__ == '__main__':a = 100foo()print(a) # 100
在調用
foo
函數後,我們發現
a
的值仍然是100,這是因為當我們在函數
foo
中寫
a = 200
的時候,是重新定義了一個名字為
a
的局部變量,它跟全局作用域的
a
并不是同一個變量,因為局部作用域中有了自己的變量
a
,是以
foo
函數不再搜尋全局作用域中的
a
。如果我們希望在
foo
函數中修改全局作用域中的
a
,代碼如下所示。
def foo():global aa = 200print(a) # 200if __name__ == '__main__':a = 100foo()print(a) # 200
我們可以使用
global
關鍵字來訓示
foo
函數中的變量
a
來自于全局作用域,如果全局作用域中沒有
a
,那麼下面一行的代碼就會定義變量
a
并将其置于全局作用域。同理,如果我們希望函數内部的函數能夠修改嵌套作用域中的變量,可以使用
nonlocal
關鍵字來訓示變量來自于嵌套作用域,請大家自行試驗。
在實際開發中,我們應該盡量減少對全局變量的使用,因為全局變量的作用域和影響過于廣泛,可能會發生意料之外的修改和使用,除此之外全局變量比局部變量擁有更長的生命周期,可能導緻對象占用的記憶體長時間無法被垃圾回收。事實上,減少對全局變量的使用,也是降低代碼之間耦合度的一個重要舉措,同時也是對迪米特法則的踐行。減少全局變量的使用就意味着我們應該盡量讓變量的作用域在函數的内部,但是如果我們希望将一個局部變量的生命周期延長,使其在定義它的函數調用結束後依然可以使用它的值,這時候就需要使用閉包,這個我們在後續的内容中進行講解。
說明: 很多人經常會将“閉包”和“匿名函數”混為一談,但實際上它們并不是一回事,如果想了解這個概念,可以看看維基百科的解釋或者知乎上對這個概念的讨論。
說了那麼多,其實結論很簡單,從現在開始我們可以将Python代碼按照下面的格式進行書寫,這一點點的改進其實就是在我們了解了函數和作用域的基礎上跨出的巨大的一步。
def main():# Todo: Add your code herepassif __name__ == '__main__':main()
長按識别下圖二維碼關注我