這是我面向小白寫(xiě)的Python編程教程的第七篇错蝴。拿勺子同學(xué)當(dāng)小白鼠講過(guò)一遍后洲愤,就把修改完的講義發(fā)出來(lái)啦。
如果你認(rèn)可這篇教程的價(jià)值顷锰,歡迎分享到朋友圈禽篱,分享給更多人!有看不懂的地方也可以留言或者問(wèn)詢(xún)馍惹。越多關(guān)注,作者就越多動(dòng)力及時(shí)更新吶 ??
.
"Functions should do one thing. They should do it well. They should do it only."
— Robert C. Martin
這節(jié)講講 Python 中的函數(shù)玛界,如何自定義函數(shù)万矾,如何理解變量作用域,最后捎帶粗講一下面向?qū)ο缶幊讨械?strong>封裝概念慎框。
.
A. 函數(shù)定義與基本用法
函數(shù)(function)是一組復(fù)合語(yǔ)句良狈,可以接收輸入值,執(zhí)行特定命令笨枯,選擇性地返回輸出結(jié)果薪丁。
Python 中函數(shù)與數(shù)學(xué)里的函數(shù)相似。比如這個(gè)代數(shù)函數(shù):
f(x) = x * 2
等式左邊定義了一個(gè)函數(shù) f
馅精,這個(gè)函數(shù)需要一個(gè)輸入值严嗜、自變量 x
——即為參數(shù)(parameter)。一個(gè)函數(shù)可以帶一個(gè)或多個(gè)參數(shù)洲敢,也可以不用參數(shù)漫玄。
等式右邊就是這個(gè)函數(shù)的具體定義了。這個(gè)函數(shù)可以把傳入的參數(shù)乘二,并返回結(jié)果睦优。
.
在代數(shù)和 Python 中渗常,呼出(= “調(diào)用”、使用)函數(shù)的語(yǔ)法都是一樣的:
FUNCTION_NAME(PARAMETERS_SEPERATED_BY_COMMA)
(在這套教程里汗盘,我用約定俗成的「大寫(xiě)字母 + 下劃線(xiàn)」來(lái)代表應(yīng)該用真實(shí)代碼取代的內(nèi)容皱碘;在編程中這樣的習(xí)慣用法很多。)
比如我們可以傳入?yún)?shù) 4
到上面的代數(shù)方程中隐孽,
f(4)
>> 8
再比如癌椿,print()
是一個(gè)我們已經(jīng)很熟悉的 Python 3 內(nèi)建函數(shù) (built-in function) 了:
>>> print("Whatever you like :p")
Whatever you like :p
.
自定義函數(shù)
由程序員來(lái)定義一個(gè)函數(shù)在 Python 中極度常見(jiàn)。比如缓醋,上面的代數(shù)方程用 Python 自定義函數(shù)表達(dá)即為:
def f(x):
return x * 2
這幾乎是最簡(jiǎn)形式的函數(shù)定義了如失,由此我們不難推斷出 Python 自定義函數(shù)的基本規(guī)則:
def FUNCTION_NAME(PARAMETERS_SEPERATED_BY_COMMA):
FUNCTION_DEFINITION
定義函數(shù)的關(guān)鍵字為 def
,函數(shù)名稱(chēng)可以隨意取送粱,但應(yīng)該避免 Python 關(guān)鍵字(keywords)褪贵;習(xí)慣上 Python 函數(shù)命名通常為「小寫(xiě)字母 + 下劃線(xiàn)」組合,例如 print_area
, multiply
, even_or_odd
.
函數(shù)名稱(chēng)后緊跟一對(duì)單括號(hào) ()
抗俄,如果有參數(shù)的話(huà)應(yīng)該放在括號(hào)里脆丁,多個(gè)參數(shù)以逗號(hào)隔開(kāi)。不要忘記句末冒號(hào)动雹。
# 零參數(shù)的函數(shù)
>>> def hello():
... return 'Hello Python!'
...
>>> greeting = hello()
>>> print(greeting)
Hello Python!
# 多個(gè)參數(shù)的函數(shù)
>>> def sum_all(x, y, z):
... return x + y + z
...
>>> my_sum = sum_all(1,2,3)
>>> print(my_sum)
6
.
所有跟在 def
行后面縮進(jìn)的語(yǔ)句都是該函數(shù)的定義槽卫。上面 f(x)
的函數(shù)定義只有一行,且用到了 return
關(guān)鍵字胰蝠。return
后跟著的表達(dá)式或值即為這個(gè)函數(shù)會(huì)返回的輸出值歼培。函數(shù)定義可以不包括 return
語(yǔ)句,沒(méi)有 return
的函數(shù)返回值為 None
. 比如:
# calling a function with return statement
>>> f(2)
4
>>> result = f(2)
>>> print(result)
4
# create a function without return statement
>>> def no_return(x):
... x = x * 2
... print('multiplied by 2:', x)
...
# call a function without return statement
>>> no_return(4)
multiplied by 2: 8
注意:很容易忽略的一點(diǎn)是茸塞,當(dāng)你想要保存一個(gè)函數(shù)的輸出值(以便之后使用)時(shí)躲庄,需要新建一個(gè)變量(variable),用這個(gè)變量來(lái)保存函數(shù)的輸出結(jié)果钾虐,比如上面的 result
變量噪窘。
.
函數(shù)的復(fù)用
為什么要?jiǎng)?chuàng)建、調(diào)用函數(shù)而不是直接寫(xiě)出具體指令呢效扫?
因?yàn)橛幸粋€(gè)現(xiàn)成函數(shù)可以大大降低程序員的碼字工作量倔监。想實(shí)現(xiàn)什么功能第一反應(yīng)應(yīng)該是,“是否有現(xiàn)成的 函數(shù)/方法/模塊 可用菌仁?“——而不是重復(fù)造輪子 (reinventing the wheel) 浩习。
To reinvent the wheel is to duplicate a basic method that has already previously been created or optimized by others.
— Wikipedia
當(dāng)前人已經(jīng)發(fā)明、優(yōu)化出一種解決問(wèn)題的方法后掘托,后人若還要自己從零開(kāi)始新造自己的方法瘦锹,這就被稱(chēng)為「重復(fù)造輪子」。在大部分情況下,應(yīng)該盡量避免「重復(fù)造輪子」弯院。
.
調(diào)用已經(jīng)寫(xiě)好的函數(shù)辱士,就是一種減少「自造輪子」,提高代碼復(fù)用性(reusability)的方式听绳。
例如颂碘,我們可以用這節(jié)課學(xué)到的函數(shù)自定義方法和上節(jié)課條件控制語(yǔ)句寫(xiě)一個(gè)“判斷輸入值奇偶性”的函數(shù):
def even_odd(x):
if x % 2 == 0:
print('input is even')
else:
print('input is odd')
接著調(diào)用這個(gè)函數(shù):
>>> even_odd(2)
input is even
>>> even_odd(3)
input is odd
接下來(lái)每次需要用到這個(gè)功能(functionality)的時(shí)候,只要一行代碼調(diào)用 even_odd
函數(shù)即可椅挣,大大減少了重復(fù)的工作量头岔。
.
嵌套函數(shù)
上一節(jié),我們認(rèn)識(shí)了嵌套條件語(yǔ)句(nested conditionals)鼠证,就是在一個(gè)條件語(yǔ)句的 True 分支后又接了一個(gè)條件句峡竣。在函數(shù)定義中,嵌套函數(shù)(nested functions)也是允許的量九。
例如适掰,
def outer():
print('Outer Funtion!')
def inner():
print('Inner Function!')
inner()
外層函數(shù)(outer function)與內(nèi)層函數(shù)(inner function)的命名也不言自明。
此時(shí)調(diào)用 outer
函數(shù)荠列,會(huì)是出現(xiàn)什么結(jié)果呢类浪?
>>> outer()
Outer Funtion!
Inner Function!
.
內(nèi)建函數(shù) Built-in functions
如果我們需要用的所有函數(shù)都要自己一一寫(xiě),那寫(xiě)代碼效率就太太低了肌似。因此费就,Python 是有自帶內(nèi)建函數(shù)庫(kù)的。
比如川队,剛剛提到的 print()
函數(shù)力细。常見(jiàn)的 Python 內(nèi)建函數(shù)還有以下這些:
# len() returns the length of an object
>>> len('Monty')
5
>>> len('12345')
5
# type() returns the data type of an object
>>> type('Welcome to SinanTalk!')
<class 'str'>
>>> type(98)
<class 'int'>
# str() takes an onject and returns a new object with a string data type
>>> str(100)
'100'
# int() takes an object and returns a new object with an integer data type
>>> int('99')
99
# float() takes an object and returns a new object with a floating point data type
>>> float(88)
88.0
# input() collects information from the user
>>> age = input('How old are you? ')
How old are you? 18
>>> age = int(age)
>>> if age < 28:
... print('Still young!')
... else:
... print('You must have seen a lot!')
...
Still young!
>>>
.
.
B. 變量作用域 Scope
是時(shí)候了解變量的作用域這個(gè)重要概念了!
作用域(scope)是變量(variable)的重要特性之一固额。
變量作用域決定了哪一部分程序可以訪(fǎng)問(wèn)某個(gè)特定的變量艳汽,即為對(duì)一個(gè)變量的「訪(fǎng)問(wèn)權(quán)限」。
A variable's scope: refers to what part of your program has access to the variable.
變量的訪(fǎng)問(wèn)權(quán)限是有這個(gè)變量的賦值位置決定的对雪。
為什么一開(kāi)始講變量的時(shí)候沒(méi)有涉及這個(gè)重要概念呢?
因?yàn)樵跊](méi)學(xué)自定義函數(shù)之前米绕,是很難理解變量作用域是怎么回事瑟捣。
.
根據(jù)作用域,變量可分為兩大類(lèi):
全局變量(global variable):定義/賦值在函數(shù)(或類(lèi))之外的變量栅干;
局部變量(local variable):定義/賦值在函數(shù)(或類(lèi))內(nèi)的變量迈套。
對(duì)于一個(gè)全局變量而言,在這個(gè)程序的任何位置都可以訪(fǎng)問(wèn)它碱鳞;而局部變量則只能在局部(即某個(gè)函數(shù)/類(lèi)的內(nèi)部)訪(fǎng)問(wèn)桑李,走出了這個(gè)函數(shù)/類(lèi),就不能再訪(fǎng)問(wèn)局部變量了。
.
打個(gè)比方贵白,在你面前站著一面放滿(mǎn)書(shū)的書(shū)柜率拒,你可以看到每本書(shū)的書(shū)名,但只有選其中一本書(shū)翻開(kāi)禁荒,才能看到這本書(shū)里提到的人名猬膨。
這面書(shū)柜就是一個(gè)程序,每本書(shū)都是一個(gè)代碼塊呛伴,寫(xiě)在書(shū)脊上的書(shū)名是全局變量勃痴,需要翻開(kāi)某本書(shū)才能看到的人名是局部變量——站在書(shū)架前是看不見(jiàn)局部變量的!
.
我們來(lái)看看在具體代碼中的全局/局部變量热康。
>>> x = 1
>>> y = 2
>>> z = 3
>>> print(x, y, z)
1 2 3
全局變量可以從程序的任何位置訪(fǎng)問(wèn)沛申,也包括函數(shù)內(nèi)部:
>>> def print_vals():
... print(x, y, z)
...
>>> print_vals()
1 2 3
.
如果在函數(shù)內(nèi)部定義了局部變量,則不能在函數(shù)外單獨(dú)訪(fǎng)問(wèn)姐军,否則會(huì)拋出 NameError
異常:
>>> def print_new_vals():
... val = 100
... print(val)
...
>>> val
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'val' is not defined
這種情況下铁材,Python 解釋器看不見(jiàn)局部變量 val
,有點(diǎn)像選擇性失明庶弃。
.
如果在函數(shù)內(nèi)容定義一個(gè)與已經(jīng)存在的全局變量名稱(chēng)相同的局部變量衫贬,那么在這個(gè)函數(shù)外再訪(fǎng)問(wèn)此變量,會(huì)返回什么值呢歇攻?
>>> x = 1
>>> def local_val():
... x = 100
...
>>> x
在 IDLE 內(nèi)敲一遍的話(huà)會(huì)發(fā)現(xiàn)固惯,x 返回的值是 1。這個(gè)例子更加充分地證明了局部變量和全局變量的作用域差別缴守。
.
如果想在函數(shù)內(nèi)部改變某個(gè)全局變量的話(huà)葬毫,可以用 global
這個(gè)關(guān)鍵字來(lái)表明這兒訪(fǎng)問(wèn)、修改的是全局變量屡穗。
>>> x = 1
>>> def f():
... global x
... x += 1
... print(x)
...
>>> f()
2
.
為什么編程語(yǔ)言中的變量普遍需要規(guī)定作用域呢贴捡?
如果不存在作用域限制的話(huà),一個(gè)程序中的任何變量在任何位置都可以訪(fǎng)問(wèn)村砂。那么在一個(gè)很長(zhǎng)的程序中烂斋,如果你在一個(gè)函數(shù)內(nèi)部使用了全局變量且不小心改變了這個(gè)變量的值或類(lèi)型,那在接下來(lái)的程序中這個(gè)變量就可能會(huì)擁有不同的特性础废,引發(fā)意想不到的錯(cuò)誤汛骂。
如果有多個(gè)程序員碰過(guò)同一個(gè)程序,可能并不是每個(gè)人都清楚別人命名的變量评腺,沒(méi)有作用域的訪(fǎng)問(wèn)限制的話(huà)帘瞭,很可能會(huì)出錯(cuò)。
.
變量的作用域不同同時(shí)也引出了編程中封裝(encapsulation)的概念蒿讥。
.
.
C. 函數(shù)的封裝
封裝是面向?qū)ο缶幊蹋╫bject-oriented programming)中的重要概念蝶念。今天我們只粗淺地介紹一點(diǎn)和函數(shù)有關(guān)的封裝行為抛腕。
封裝,顧名思義媒殉,就像把一些代碼封起來(lái)裝進(jìn)膠囊(capsule 在英文中可表示“膠囊”)或瓶子里去担敌,在膠囊外的代碼不能訪(fǎng)問(wèn)膠囊內(nèi)代碼。
When code is encapsulated, it means when it is called, the caller cannot access the code's internal data.
比如适袜,上面介紹的函數(shù)內(nèi)部的局部變量柄错,就是函數(shù)封裝行為的體現(xiàn)。
這樣做的好處很多苦酱。當(dāng)用戶(hù)(指所有使用這個(gè)程序的人)運(yùn)行你的代碼時(shí)售貌,他并不需要知道你的代碼里有什么函數(shù),每個(gè)函數(shù)內(nèi)部又存在怎樣的變量和運(yùn)算疫萤,他只需要知道如何運(yùn)行即可颂跨。另一方面,正常情況下扯饶,一個(gè)程序會(huì)需要反復(fù)優(yōu)化升級(jí)恒削,后期你可能會(huì)修改一個(gè)函數(shù)內(nèi)部的代碼,沒(méi)有封裝的話(huà)尾序,這個(gè)函數(shù)外部的代碼可能會(huì)直接訪(fǎng)問(wèn)內(nèi)部钓丰,一旦修改內(nèi)部代碼,就可能會(huì)造成“連鎖事故”每币。
所以說(shuō)携丁,封裝是提高代碼可維護(hù)性與可移植性的重要前提。
.
第7節(jié) 小結(jié)
掌握本節(jié)的自定義函數(shù)與上一節(jié)的條件語(yǔ)句兰怠,已經(jīng)能寫(xiě)很多 Python 短程序了呢梦鉴!
.
.