本文收錄在Python從入門到精通系列文章系列html
在分享本章節的內容以前,先來研究一道數學題,請說出下面的方程有多少組正整數解。程序員
事實上,上面的問題等同於將8個蘋果分紅四組每組至少一個蘋果有多少種方案。想到這一點問題的答案就呼之欲出了。編程
能夠用Python的程序來計算出這個值,代碼以下所示。閉包
""" 輸入M和N計算C(M,N) Version: 0.1 Author: along """ m = int(input('m = ')) n = int(input('n = ')) fm = 1 for num in range(1, m + 1): fm *= num fn = 1 for num in range(1, n + 1): fn *= num fmn = 1 for num in range(1, m - n + 1): fmn *= num print(fm // fn // fmn)
輕鬆得出答案爲:35dom
不知道你們是否注意到,在上面的代碼中,咱們作了3次求階乘,這樣的代碼實際上就是重複代碼。編程大師Martin Fowler先生曾經說過:「代碼有不少種壞味道,重複是最壞的一種!」,要寫出高質量的代碼首先要解決的就是重複代碼的問題。對於上面的代碼來講,咱們能夠將計算階乘的功能封裝到一個稱之爲「函數」的功能模塊中,在須要計算階乘的地方,咱們只須要「調用」這個「函數」就能夠了。編程語言
在Python中可使用def關鍵字來定義函數,和變量同樣每一個函數也有一個響亮的名字,並且命名規則跟變量的命名規則是一致的。在函數名後面的圓括號中能夠放置傳遞給函數的參數,這一點和數學上的函數很是類似,程序中函數的參數就至關因而數學上說的函數的自變量,而函數執行完成後咱們能夠經過return關鍵字來返回一個值,這至關於數學上說的函數的因變量。函數
在瞭解瞭如何定義函數後,咱們能夠對上面的代碼進行重構,所謂重構就是在不影響代碼執行結果的前提下對代碼的結構進行調整,重構以後的代碼以下所示。spa
def factorial(num): """求階乘""" result = 1 for i in range(1, num + 1): result *= i return result m = int(input('m = ')) n = int(input('n = ')) # 當須要計算階乘的時候不用再寫循環求階乘而是直接調用已經定義好的函數 print(factorial(m) // factorial(n) // factorial(m - n))
說明: Python的math模塊中其實已經有一個factorial函數了,事實上要計算階乘能夠直接使用這個現成的函數而不用本身定義。下面例子中的一些函數在Python中也都是現成的,咱們這裏是爲了講解函數的定義和使用才把它們又實現了一遍,實際開發中不建議作這種低級的重複性的工做。設計
函數是絕大多數編程語言中都支持的一個代碼的"構建塊",可是Python中的函數與其餘語言中的函數仍是有不少不太相同的地方,其中一個顯著的區別就是Python對函數參數的處理。在Python中,函數的參數能夠有默認值,也支持使用可變參數,因此Python並不須要像其餘語言同樣支持函數的重載,由於咱們在定義一個函數的時候可讓它有多種不一樣的使用方式,下面是兩個小例子。code
from random import randint def roll_dice(n=2): """搖色子""" total = 0 for _ in range(n): #print(_) total += randint(1, 6) #print(total) return total def 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): result = 0 for num in args: result += num return(result) # 在調用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函數,代碼以下所示。
(1)先在同級目錄下,建立2個py文件
module1.py
def foo(): print('hello world')
module2.py
def foo(): print('goodbye world')
(2)直接使用模塊使用函數
test.py
from module1 import foo foo() from module2 import foo foo()
(3)也能夠按照以下所示的方式來區分到底要使用哪個foo函數。
test.py
import module1 as m1 import module2 as m2 m1.foo() m2.foo()
(4)可是若是將代碼寫成了下面的樣子,那麼程序中調用的是最後導入的那個foo,由於後導入的foo覆蓋了以前導入的foo。
test.py
from module1 import foo from module2 import foo foo() from module2 import foo from module1 import foo # 輸出hello, world! foo()
須要說明的是,若是咱們導入的模塊除了定義函數以外還中有能夠執行代碼,那麼Python解釋器在導入這個模塊時就會執行這些代碼,事實上咱們可能並不但願如此,所以若是咱們在模塊中編寫了執行代碼,最好是將這些執行代碼放入以下所示的條件中,這樣的話除非直接運行該模塊,if條件下的這些代碼是不會執行的,由於只有直接執行的模塊的名字纔是"__main__"。
module3.py
def foo(): pass def 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__
實現計算求最大公約數和最小公倍數的函數。
參考答案:
def gcd(x, y): """求最大公約數""" #(x, y) = (y, x) if x > y else (x, y) if x > y: (x, y) = (y, x) else: (x, y) for factor in range(x, 0, -1): if x % factor == 0 and y % factor == 0: return factor def lcm(x, y): """求最小公倍數""" return x * y // gcd(x, y) print(gcd(9,12)) print(lcm(9,12))
實現判斷一個數是否是迴文數的函數。
參考答案:
def is_palindrome(num): """ 判斷一個數是否是迴文數 迴文數是指將一個正整數從左往右排列和從右往左排列值同樣的數 """ temp = num total = 0 while temp > 0: total = total * 10 + temp % 10 temp //= 10 return total == num
實現判斷一個數是否是素數的函數。
參考答案:
def is_prime(num): """判斷一個數是否是素數""" for factor in range(2, num): if num % factor == 0: return False return True if num != 1 else False print(is_prime(1))
寫一個程序判斷輸入的正整數是否是迴文素數。
參考答案:
if __name__ == '__main__': num = int(input('請輸入正整數: ')) if is_palindrome(num) and is_prime(num): print('%d是迴文素數' % num) else: print('%d不是迴文素數' % num)
注意:經過上面的程序能夠看出,當咱們將代碼中重複出現的和相對獨立的功能抽取成函數後,咱們能夠組合使用這些函數來解決更爲複雜的問題,這也是咱們爲何要定義和使用函數的一個很是重要的緣由。
(1)最後,咱們來討論一下Python中有關變量做用域的問題
def foo(): b = 'hello' # Python中能夠在函數內部再定義函數 def bar(): c = True print(a) print(b) print(c) bar() # print(c) # NameError: name 'c' is not defined if __name__ == '__main__': a = 100 # print(b) # NameError: name 'b' is not defined foo()
上面的代碼可以順利的執行而且打印出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等都屬於內置做用域。
其中,在foo函數中調用bar函數的變量c,會報錯;由於讀不到此變量。
(2)再看看下面這段代碼,咱們但願經過函數調用修改全局變量a的值,但實際上下面的代碼是作不到的。
def foo(): a = 200 print(a) # 200 if __name__ == '__main__': a = 100 foo() print(a) # 100
在調用foo函數後,咱們發現a的值仍然是100,這是由於當咱們在函數foo中寫a = 200的時候,是從新定義了一個名字爲a的局部變量,它跟全局做用域的a並非同一個變量,由於局部做用域中有了本身的變量a,所以foo函數再也不搜索全局做用域中的a。
(3)若是咱們但願在foo函數中修改全局做用域中的a,代碼以下所示。
def foo(): global a a = 200 print(a) # 200 if __name__ == '__main__': a = 100 foo() print(a) # 200
咱們可使用global關鍵字來指示foo函數中的變量a來自於全局做用域,若是全局做用域中沒有a,那麼下面一行的代碼就會定義變量a並將其置於全局做用域。
同理,若是咱們但願函數內部的函數可以修改嵌套做用域中的變量,可使用nonlocal關鍵字來指示變量來自於嵌套做用域,請你們自行試驗。
在實際開發中,咱們應該儘可能減小對全局變量的使用,由於全局變量的做用域和影響過於普遍,可能會發生意料以外的修改和使用,除此以外全局變量比局部變量擁有更長的生命週期,可能致使對象佔用的內存長時間沒法被垃圾回收。事實上,減小對全局變量的使用,也是下降代碼之間耦合度的一個重要舉措,同時也是對迪米特法則的踐行。減小全局變量的使用就意味着咱們應該儘可能讓變量的做用域在函數的內部,可是若是咱們但願將一個局部變量的生命週期延長,使其在定義它的函數調用結束後依然可使用它的值,這時候就須要使用閉包,這個咱們在後續的內容中進行講解。
說明: 不少人常常會將「閉包」和「匿名函數」混爲一談,但實際上它們並非一回事,若是想了解這個概念,能夠看看維基百科的解釋。
說了那麼多,其實結論很簡單,從如今開始咱們能夠將Python代碼按照下面的格式進行書寫,這一點點的改進其實就是在咱們理解了函數和做用域的基礎上跨出的巨大的一步。
def main(): # Todo: Add your code here pass if __name__ == '__main__': main()