官术网_书友最值得收藏!

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,記作fA),得到另一個數(shù)集B,也就是B=fA),那么這個關系式就叫函數(shù)關系式,簡稱函數(shù)。

數(shù)學中的函數(shù)其實就是AB之間的一種關系,我們可以理解為從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作為包管理器強大的地方,方便易用。

主站蜘蛛池模板: 灵宝市| 吉林省| 特克斯县| 临洮县| 天镇县| 东乡县| 册亨县| 和田市| 井研县| 龙里县| 余庆县| 札达县| 桃江县| 濮阳市| 桦川县| 浑源县| 夏河县| 云浮市| 桓仁| 资阳市| 炎陵县| 龙岩市| 安平县| 惠来县| 济南市| 永清县| 澄迈县| 特克斯县| 微山县| 竹北市| 大余县| 纳雍县| 绿春县| 兴义市| 塔城市| 定兴县| 余姚市| 芦溪县| 鄄城县| 靖西县| 榕江县|