- Python基礎及應用
- 呂云翔 姚澤良 張揚等編著
- 4982字
- 2021-10-27 15:16:29
3.4 函數(shù)的定義與使用
還記得第2章提到過的一個“內(nèi)置函數(shù)”max嗎?對于不同的List和Tuple,這個函數(shù)總能給出正確的結(jié)果——當然有人說用for循環(huán)實現(xiàn)也很快很方便,但是有多少個List或Tuple就要寫多少個完全重復的for循環(huán),這是很讓人厭煩的,這時候就需要函數(shù)出場了。
本章會從數(shù)學中的函數(shù)引入,詳細講解Python中函數(shù)的基本用法。
3.4.1 認識Python的函數(shù)
函數(shù)的數(shù)學定義為:給定一個數(shù)集A,對A施加對應法則f,記作f(A),得到另一個數(shù)集B,也就是B=f(A),那么這個關系式就叫函數(shù)關系式,簡稱函數(shù)。
數(shù)學中的函數(shù)其實就是A和B之間的一種關系,我們可以理解為從A中取出任意一個輸入都能在B中找到特定的輸出,在程序中,函數(shù)也是完成這樣的一種輸入到輸出的映射,但是程序中的函數(shù)有著更大的意義。
它首先可以減少重復代碼,因為我們可以把相似的邏輯抽象成一個函數(shù),減少重復代碼,其次它有可以使程序模塊化并且提高可讀性。
以之前多次用到的一個函數(shù)print為例:

由于print是一個函數(shù),因此我們不用再去實現(xiàn)一遍打印到屏幕的功能,減少了大量的重復代碼,同時看到print就可以知道這一行是用來打印的,可讀性自然也提高了,另外如果打印出現(xiàn)問題只要去查看print函數(shù)的內(nèi)部就可以了,而不用再去看print以外的代碼,這體現(xiàn)了模塊化的思想。
但是,內(nèi)置函數(shù)的功能非常有限,我們需要根據(jù)實際需求編寫自己的函數(shù),這樣才能進一步提高程序的簡潔性、可讀性和可擴展性。
3.4.2 函數(shù)的定義和調(diào)用
1.定義
和數(shù)學中的函數(shù)類似,Python中的函數(shù)需要先定義才能使用,比如:


這是一個基本的函數(shù)定義,其中第1、4、6行是函數(shù)特有的,其他我們都已經(jīng)學習過了。
先看第1行:

這一行有四個關鍵點:
● def:函數(shù)定義的關鍵字,寫在最前面。
● ask_me_to:函數(shù)名,命名要求和變量一致。
● (string):函數(shù)的參數(shù),多個參數(shù)用逗號隔開。
● 結(jié)尾冒號:函數(shù)聲明的語法要求。
然后看第2到第5行:

它們都縮進了四個空格,意味著它們構(gòu)成了一個代碼塊,同時從第2行可以看到函數(shù)內(nèi)是可以接著調(diào)用函數(shù)的。
接著再看第4行:

這里引入了一個新關鍵字:return,它的作用是結(jié)束函數(shù)并返回到之前調(diào)用函數(shù)處的下一句。返回的對象是return后面的表達式,如果表達式為空則返回None。第6行跟第4行功能相同,這里不再贅述。
2.調(diào)用
在數(shù)學中函數(shù)需要一個自變量才會得到因變量,Python的函數(shù)也是一樣,只是定義的話并不會執(zhí)行,還需要調(diào)用,比如:

注意這里是兩個函數(shù)嵌套,首先調(diào)用的是我們自定義的函數(shù)ask_me_to,接著ask_me_to的返回值傳給了print,所以會輸出ask_me_to的返回值:

定義和調(diào)用都很好理解,接下來了解函數(shù)的參數(shù)怎么設置。
3.4.3 函數(shù)的參數(shù)
Python的函數(shù)參數(shù)非常靈活,我們已經(jīng)學習了最基本的一種,比如:

它擁有一個參數(shù),名字為string。
函數(shù)參數(shù)的個數(shù)可以為0個或多個,比如:

我們可以根據(jù)需求去選擇參數(shù)個數(shù),但是要注意的是,即使沒有參數(shù),括號也不可省略。
Python的一個靈活之處在于函數(shù)參數(shù)形式的多樣性,有以下幾種形式。
● 不帶默認參數(shù)的:deffunc(a):
● 帶默認參數(shù)的:deffunc(a,b=1):
● 任意位置參數(shù):deffunc(a,b=1,?c):
● 任意鍵值參數(shù):deffunc(a,b=1,?c,??d):
第一種就是我們剛才講到的一般形式,下面介紹剩下三種如何使用。
3.4.4 默認參數(shù)
有時候某個函數(shù)參數(shù)大部分時候為某個特定值,于是我們希望這個參數(shù)可以有一個默認值,這樣就不用頻繁指定相同的值給這個參數(shù)了。默認參數(shù)的用法看一個例子:

這是一個格式化輸出日期的函數(shù),注意其中的月份和天數(shù)參數(shù),用一個等號表明賦了默認值。于是可以分別以1,2,3個參數(shù)調(diào)用這個函數(shù),同時也可以指定某個特定參數(shù),比如:

這段代碼會輸出:

我們依次看一下這些調(diào)用:
1)print_date(2018)這種情況下由于默認參數(shù)的存在等價于print_date(2018,1,1)。
2)print_date(2018,2,1)這種情況下所有參數(shù)都被傳入了,因此和無默認參數(shù)的行為是一致的。
3)print_date(2018,5)省略了day,因為參數(shù)是按照順序傳入的。
4)print_date(2018,day=3)省略了month,由于和聲明順序不一致,所以必須聲明參數(shù)名稱。
5)print_date(2018,month=2,day=5)全部聲明也是可以的。
使用默認參數(shù)可以讓函數(shù)的行為更加靈活。
3.4.5 任意位置參數(shù)
如果函數(shù)想接收任意數(shù)量的參數(shù),那么可以這樣聲明使用:

診斷代碼會輸出:

任意位置參數(shù)的特點就是它只占一個參數(shù),并且以 ? 開頭。其中args為一個List,包含了所有傳入的參數(shù),順序為調(diào)用時候的傳參的順序。
3.4.6 任意鍵值參數(shù)
除了接受任意數(shù)量的參數(shù),如果我們希望給每個參數(shù)一個名字,那么可以這么聲明參數(shù):

這段代碼會輸出:

跟之前講過的任意位置參數(shù)使用非常類似,但是kwargs這里是一個Dict(字典),其中Key和Value為調(diào)用時候傳入的參數(shù)的名稱和值,順序和傳參順序一致。
3.4.7 組合使用
我們現(xiàn)在知道了這四類參數(shù),它們可以同時使用,但是需要滿足一定的條件,比如:


可以看出,四種參數(shù)在定義時應該滿足這樣的順序:非默認參數(shù)、默認參數(shù)、任意位置參數(shù)、任意鍵值參數(shù)。
調(diào)用的時候,參數(shù)分為兩類:位置相關參數(shù)和無關鍵詞參數(shù),比如:

這句代碼會輸出:

其中前三個就是位置相關參數(shù),最后一個是關鍵詞參數(shù)。位置相關參數(shù)是順序傳入的,而關鍵詞參數(shù)則可以亂序傳入,比如:

這句代碼會輸出:

總之在調(diào)用的時候,參數(shù)順序應該滿足的規(guī)則是:
● 位置相關參數(shù)不能在關鍵詞參數(shù)之后。
● 位置相關參數(shù)優(yōu)先。
這么看有些抽象,不如看看兩個錯誤用法,第一個錯誤用法:

這句代碼會報錯:

報錯的意思是位置相關參數(shù)不能在關鍵詞參數(shù)之后。也就是說,必須先傳入位置相關參數(shù),再傳入關鍵詞參數(shù)。
再看第二個錯誤用法:

這句代碼會報錯:

報錯的意思是函數(shù)的參數(shù)arg1接收到了多個值。也就是說,位置相關參數(shù)會優(yōu)先傳入,如果再指定相應的參數(shù),那么就會發(fā)生錯誤。
3.4.8 修改傳入的參數(shù)
先補充有關傳入?yún)?shù)的兩個重要概念:
● 按值傳遞:復制傳入的變量,傳入函數(shù)的參數(shù)是一個和原對象無關的副本。
● 按引用傳遞:直接傳入原變量的一個引用,修改參數(shù)就是修改原對象。
在有些編程語言中,可能是兩種傳參方式同時存在、可供選擇,但是Python只有一種傳參方式就是按引用傳遞,比如:

注意在函數(shù)內(nèi)通過append修改了mylist的元素,由于mylist是list1的一個引用,因此實際上修改的就是list1的元素,所以這段代碼會輸出:

這是符合我們的預期的,但是看另一個例子:

按照之前的理論,number應該是num的一個引用,所以這里應該輸出3,但是實際上的輸出是:

為什么會這樣呢?在第6章提到:特別地,字符串是一個不可變的對象。實際上,包括字符串在內(nèi),數(shù)值類型和Tuple也是不可變的,而這里正是因為num是不可變類型,所以函數(shù)的行為不符合我們的預期。
為了深入探究其原因,我們引入一個新的內(nèi)建函數(shù)id,它的作用是返回對象的id。對象的id是唯一的,但是可能有多個變量引用同一個對象,比如下面這個例子:

我們可以得到這樣的輸出(這里id的輸出不一定跟本書一致,但是第1,2,4個id應該是相同的):

其實除了函數(shù)參數(shù)是引用傳遞,Python變量的本質(zhì)就是引用。這也就意味著在把alice賦值給bob的時候,實際上是把alice的引用給了bob,于是這時候alice和bob實際上引用了同一個對象,因此id相同。
接下來修改了alice的值,可以看到Bob的值并沒有改變,這符合我們的直覺。但是從引用上看,實際發(fā)生的操作是,bob的引用不變,但是alice獲得了一個新對象的引用,這個過程充分體現(xiàn)了數(shù)值類型不可變的性質(zhì)——已經(jīng)創(chuàng)建的對象不會修改,任何修改都是新建一個對象來實現(xiàn)的。
實際上,對于這些不可變類型,每次修改都會創(chuàng)建一個新的對象,然后修改引用為新的對象。在這里,alice和bob已經(jīng)引用兩個完全不同的對象了,這兩個對象占用的空間是完全不同的。
那么回到最開始的問題,為什么這些不可變對象在函數(shù)內(nèi)的修改不能體現(xiàn)在函數(shù)外呢?雖然函數(shù)參數(shù)的確引用了原對象,但是我們在修改的時候?qū)嶋H上是創(chuàng)建了一個新的對象,所以原對象不會被修改,這也就解釋了剛才的現(xiàn)象。如果一定要修改的話,可以這么寫:

這樣輸出就是我們預期的3了。
特殊地,這里舉例用了一個很大的數(shù)字是有原因的。由于0~256這些整數(shù)使用地比較頻繁,為了避免小對象的反復內(nèi)存分配和釋放造成內(nèi)存碎片,所以Python對0~256這些數(shù)字建立了一個對象池。

我們可以得到輸出(這里輸出的兩個id應該是一致的,但是數(shù)字不一定跟本書中的相同)為:

可以看出,雖然alice和bob無關,但是它們引用的是同一個對象,所以為了方便說明之前取了一個比較大的數(shù)字用于賦值。
3.4.9 函數(shù)的返回值
1.返回一個值
函數(shù)在執(zhí)行的時候,會在執(zhí)行到結(jié)束或者return語句的時候返回調(diào)用的位置。如果我們的函數(shù)需要返回一個值,那需要用return語句,比如最簡單地返回一個值:

這段代碼會輸出:

這個multiply函數(shù)將輸入的兩個參數(shù)相乘,然后返回結(jié)果。
2.什么都不返回
如果我們不想返回任何內(nèi)容,可以只寫一個return,它會停止執(zhí)行后面代碼的立即返回,比如:

這里只要函數(shù)參數(shù)不是' secret '就不會輸出任何內(nèi)容,因為return后面的代碼不會被執(zhí)行。另外return跟return None是等價的,也就是說默認返回的是None。
3.返回多個值
和大部分編程語言不同,Python支持返回多個參數(shù),比如:


這里要注意接收返回值的時候不能再像之前用一個變量,而是要用和返回值數(shù)目相同的變量接收,其中返回值賦值的順序是從左到右的,跟直覺一致。

所以這個函數(shù)的作用就是把輸入的三個變量順序翻轉(zhuǎn)一下。
3.4.10 函數(shù)的嵌套
我們可以在函數(shù)內(nèi)定義函數(shù),這對于簡化函數(shù)內(nèi)重復邏輯很有用,比如:

這段代碼會輸出:

需要注意的一點是,內(nèi)部的函數(shù)只能在它所處的代碼塊中使用,在上面這個例子中,inner在outer外面是不可見的,這個概念叫作作用域。
1.作用域
作用域是一個很重要的概念,我們看一個例子:

這里函數(shù)func2中能正常輸出x1的值嗎?
答案是不能。為了解決這個問題,需要用到Python的變量名稱查找順序,即LEGB原則:
● L: Local(本地)是函數(shù)內(nèi)的名字空間,包括局部變量和形參。
● E: Enclosing(封閉)外部嵌套函數(shù)的名字空間(閉包中常見)。
● G: Global(全局)全局函數(shù)定義所在模塊的名字空間。
● B: Builtin(內(nèi)建)內(nèi)置模塊的名字空間。
LEGB原則的含義是,Python會按照LEGB這個順序去查找變量,一旦找到就拿來使用,否則就到更外面一層的作用域去查找,如果都找不到就報錯。
可以通過一個例子來認識LEGB,比如:

其中要注意的是func3沒有Enclosing作用域,至于閉包是什么會在后面的章節(jié)中介紹到,這里只要理解LEGB原則就可以了。
2.global和nonlocal
根據(jù)上述LEGB原則,我們在函數(shù)中是可以訪問到全局變量的,比如:

但是LEGB規(guī)則仿佛出了點問題,因為會報錯:

這并不是Python的問題,反而是Python的一個特點,也就是說Python會在阻止用戶在不知情的情況下修改非局部變量,那么怎么訪問非局部變量呢?
為了修改非局部變量,需要使用global和nonlocal關鍵字,其中nonlocal關鍵字是Python3中才有的新關鍵字,看一個例子:


也就是說global會使得相應的全局變量在當前作用域內(nèi)可見,而nonlocal可以讓閉包中非全局變量可見,所以這段代碼會輸出:

3.4.11 使用輪子
這里的“使用輪子”可不是現(xiàn)實中那種使用輪子,而是指直接使用別人寫好并封裝好的易于使用的庫,進而極大地減少重復勞動,提高開發(fā)效率。
Python自帶的標準庫就是一堆魯棒性強,接口易用,涉獵廣泛的“輪子”,善于利用這些輪子可以極大地簡化代碼,這里簡單介紹一些常用的庫。
1.隨機庫
Python中的隨機庫用于生成隨機數(shù),比如:

它會輸出一個隨機的[1,5)范圍內(nèi)的整數(shù)。我們無需關心它的實現(xiàn),只要知道這樣可以生成隨機數(shù)就可以了。
其中import關鍵字的作用是導入一個包,有關包和模塊的內(nèi)容后面章節(jié)會細講,這里只講基本使用方法。
用import導入的基本語法是:import包名,包提供的函數(shù)的用法是包名.函數(shù)名。當然不僅函數(shù),包里面的常量和類都可以通過類似的方法調(diào)用,不過我們這里會用函數(shù)就夠了。
此外如果不想寫包名,也可以這樣:

然后就可以直接調(diào)用randint而不用寫前面的random了。
如果有很多函數(shù)要導入的話,我們還可以這么寫:

這樣random包里的一切就都包含進來了,可以不用random直接調(diào)用。不過不太推薦這樣寫,因為不知道包內(nèi)都有什么,容易造成名字的混亂。
特殊地,import random還有一種特殊寫法:

它和import random沒有本質(zhì)區(qū)別,僅僅是給了random一個方便輸入的別名rnd。
2.日期庫
這個庫可以用于計算日期和時間,比如:

這段代碼會輸出:

3.數(shù)學庫
這個庫有著常用的數(shù)學函數(shù),比如:

這段代碼會輸出:

其中第二個結(jié)果其實就是0,但是限于浮點數(shù)的精度問題無法精確表示為0,所以我們在編寫代碼涉及浮點數(shù)比較的時候一定要這么寫:

這里EPS就是指允許的誤差范圍。也就是說浮點數(shù)沒有真正的相等,只是在一定誤差范圍內(nèi)的相等。
4.操作系統(tǒng)庫
這個庫包含操作系統(tǒng)的一些操作,例如列出目錄:

在之后的文件操作章節(jié)還會見到這個庫。
5.第三方庫
可以用第3章講過的pip來方便地安裝各種第三方庫,比如:

通過一行指令我們就可以安裝numpy這個庫了,然后就可以在代碼中正常import這個庫:

這也正是pip作為包管理器強大的地方,方便易用。
- DB2 V9權(quán)威指南
- 復雜軟件設計之道:領域驅(qū)動設計全面解析與實戰(zhàn)
- OpenShift開發(fā)指南(原書第2版)
- The Android Game Developer's Handbook
- Visual Studio 2012 Cookbook
- Rust編程:入門、實戰(zhàn)與進階
- MySQL 8從入門到精通(視頻教學版)
- Visual C++應用開發(fā)
- HTML5入門經(jīng)典
- JavaScript 程序設計案例教程
- RabbitMQ Cookbook
- ExtJS高級程序設計
- Python深度學習原理、算法與案例
- SQL Server實用教程(SQL Server 2008版)
- Unity 5.X從入門到精通