Python基礎語法(三)——函數

(一)函數介紹

什麼是函數

請看如下代碼:

print ("                            _ooOoo_  ")
print ("                           o8888888o  ")
print ("                           88  .  88  ")
print ("                           (| -_- |)  ")
print ("                            O\\ = /O  ")
print ("                        ____/`---'\\____  ")
print ("                      .   ' \\| |// `.  ")
print ("                       / \\||| : |||// \\  ")
print ("                     / _||||| -:- |||||- \\  ")
print ("                       | | \\\\\\ - /// | |  ")
print ("                     | \\_| ''\\---/'' | |  ")
print ("                      \\ .-\\__ `-` ___/-. /  ")
print ("                   ___`. .' /--.--\\ `. . __  ")
print ("                ."" '< `.___\\_<|>_/___.' >'"".  ")
print ("               | | : `- \\`.;`\\ _ /`;.`/ - ` : | |  ")
print ("                 \\ \\ `-. \\_ __\\ /__ _/ .-` / /  ")
print ("         ======`-.____`-.___\\_____/___.-`____.-'======  ")
print ("                            `=---='  ")
print ("  ")
print ("         .............................................  ")
print ("                  佛祖鎮樓                  BUG辟易  ")
print ("          佛曰:  ")
print ("                  寫字樓裏寫字間,寫字間裏程序員;  ")
print ("                  程序人員寫程序,又拿程序換酒錢。  ")
print ("                  酒醒只在網上坐,酒醉還來網下眠;  ")
print ("                  酒醉酒醒日復日,網上網下年復年。  ")
print ("                  但願老死電腦間,不願鞠躬老闆前;  ")
print ("                  奔馳寶馬貴者趣,公交自行程序員。  ")
print ("                  別人笑我忒瘋癲,我笑自己命太賤;  ")
print ("                  不見滿街漂亮妹,哪個歸得程序員?")

運行後的現象:

想一想:
如果一個程序在不同的地方需要輸出“佛祖鎮樓”,程序應該怎樣設計?

 if 條件1:
	輸出‘佛祖鎮樓’

...(省略)...

if 條件2:
	輸出‘佛祖鎮樓’

...(省略)...

如果需要輸出多次,是否意味着要編寫這塊代碼多次呢?

抽象
抽象是數學中非常常見的概念。舉個例子:
計算數列的和,比如:1 + 2 + 3 + ... + 100,寫起來十分不方便,於是數學家發明了求和符號∑,可以把1 + 2 + 3 + ... + 100記作:
∑nn=1100
這種抽象記法非常強大,因爲我們看到 ∑ 就可以理解成求和,而不是還原成低級的加法運算。
而且,這種抽象記法是可擴展的,比如:
∑(n2+1)n=1100
還原成加法運算就變成了:
(1 x 1 + 1) + (2 x 2 + 1) + (3 x 3 + 1) + … + (100 x 100 + 1)
可見,藉助抽象,我們才能不關心底層的具體計算過程,而直接在更高的層次上思考問題。
寫計算機程序也是一樣,函數就是最基本的一種代碼抽象的方式。

小總結:

  • 如果在開發程序時,需要某塊代碼多次,但是爲了提高編寫的效率以及代碼的重用,所以把具有獨立功能的代碼塊組織爲一個小模塊,這就是函數

(二)函數定義和調用

(1)定義函數

定義函數的格式如下:

def 函數名():
	代碼

demo:

# 定義一個函數,能夠完成打印信息的功能
def printInfo():
    print ('------------------------------------')
    print ('         人生苦短,我用Python')
    print ('------------------------------------')

(2)調用函數

定義了函數之後,就相當於有了一個具有某些功能的代碼,想要讓這些代碼能夠執行,需要調用它
調用函數很簡單的,通過 函數名() 即可完成調用
demo:

# 定義完函數後,函數是不會自動執行的,需要調用它纔可以
printInfo()

注意:Python調用函數要放在定義函數後面,部分編程語言可放在前面

(3)練一練

要求:定義一個函數,能夠輸出自己的姓名和年齡,並且調用這個函數讓它執行

  • 使用def定義函數
  • 編寫完函數之後,通過 函數名() 進行調用

答案:https://blog.zeruns.tech/index.php/archives/150/

(三)函數的文檔說明

def test(a,b):
	"用來完成對2個數求和"
	print("%d"%(a+b))

如果執行,以下代碼

help(test)

能夠看到test函數的相關說明

Help on function test in module __main__:

test(a, b)
    用來完成對2個數求和
(END)

(四)函數參數①

思考一個問題,如下:

現在需要定義一個函數,這個函數能夠完成2個數的加法運算,並且把結果打印出來,該怎樣設計?下面的代碼可以嗎?有什麼缺陷嗎?

def add2num():
    a = 11
    b = 22
    c = a+b
    print(c)

爲了讓一個函數更通用,即想讓它計算哪兩個數的和,就讓它計算哪兩個數的和,在定義函數的時候可以讓函數接收數據,就解決了這個問題,這就是 函數的參數

(1)定義帶有參數的函數

示例如下:

def add2num(a, b):
	c = a+b
	print(c)

(2)調用帶有參數的函數

以調用上面的add2num(a, b)函數爲例:

def add2num(a, b):
	c = a+b
	print(c)

add2num(11, 22) #調用帶有參數的函數時,需要在小括號中,傳遞數據

調用帶有參數函數的運行過程:

(3)練一練

要求:定義一個函數,完成前2個數完成加法運算,然後對第3個數,進行減法;然後調用這個函數

  • 使用def定義函數,要注意有3個參數
  • 調用的時候,這個函數定義時有幾個參數,那麼就需要傳遞幾個參數

答案:https://blog.zeruns.tech/index.php/archives/150/

(4)調用函數時參數的順序

>>> def test(a,b):
...     print(a,b)
... 
>>> test(1,2)
1 2
>>> test(b=1,a=2)
2 1
>>> 
>>> test(b=1,2)
  File "<stdin>", line 1
SyntaxError: positional argument follows keyword argument
>>> 
>>>

(5)小總結

  • 定義時小括號中的參數,用來接收參數用的,稱爲 “形參”。
  • 調用時小括號中的參數,用來傳遞給函數用的,稱爲 “實參”。

(五)函數返回值①

(1)“返回值”介紹

現實生活中的場景:

我給兒子10塊錢,讓他給我買包煙。這個例子中,10塊錢是我給兒子的,就相當於調用函數時傳遞到參數,讓兒子買菸這個事情最終的目標是,讓他把煙給你帶回來然後給你對麼,此時煙就是返回值。

開發中的場景:

定義了一個函數,完成了獲取室內溫度,想一想是不是應該把這個結果給調用者,只有調用者擁有了這個返回值,才能夠根據當前的溫度做適當的調整。

綜上所述:

  • 所謂“返回值”,就是程序中函數完成一件事情後,最後給調用者的結果。

(2)帶有返回值的函數

想要在函數中把結果返回給調用者,需要在函數中使用return
如下示例:

def add2num(a, b):
	c = a+b
	return c

或者

def add2num(a, b):
	return a+b

(3)保存函數的返回值

在本小節剛開始的時候,說過的“買菸”的例子中,最後兒子給你煙時,你一定是從兒子手中接過來 對麼,程序也是如此,如果一個函數返回了一個數據,那麼想要用這個數據,那麼就需要保存。
保存函數的返回值示例如下:

#定義函數
def add2num(a, b):
	return a+b

#調用函數,順便保存函數的返回值
result = add2num(100,98)

#因爲result已經保存了add2num的返回值,所以接下來就可以使用了
print(result)

(六)4種函數的類型

函數根據有沒有參數,有沒有返回值,可以相互組合,一共有4種:

  • 無參數,無返回值
  • 無參數,有返回值
  • 有參數,無返回值
  • 有參數,有返回值

(1)無參數,無返回值的函數

此類函數,不能接收參數,也沒有返回值,一般情況下,打印提示燈類似的功能,使用這類的函數。

def printMenu():
	print('--------------------------')
	print('      xx涮涮鍋 點菜系統')
	print('')
	print('  1.  羊肉涮涮鍋')
	print('  2.  牛肉涮涮鍋')
	print('  3.  豬肉涮涮鍋')
	print('--------------------------')

結果:

無參數,有返回值的函數

此類函數,不能接收參數,但是可以返回某個數據,一般情況下,像採集數據,用此類函數。

 # 獲取溫度
def getTemperature():
#這裏是獲取溫度的一些處理過程
#爲了簡單起見,先模擬返回一個數據
	return 24

temperature = getTemperature()
print('當前的溫度爲:%d'%temperature)

(2)有參數,無返回值的函數

此類函數,能接收參數,但不可以返回數據,一般情況下,對某些變量設置數據而不需結果時,用此類函數。

(3)有參數,有返回值的函數

此類函數,不僅能接收參數,還可以返回某個數據,一般情況下,像數據處理並需要結果的應用,用此類函數。

# 計算1~num的累積和
def calculateNum(num):
	result = 0
	i = 1
	while i<=num:
		result = result + i
		i+=1
	return result

result = calculateNum(100)
print('1~100的累積和爲:%d'%result)

(4)小總結

  • 函數根據有沒有參數,有沒有返回值可以相互組合
  • 定義函數時,是根據實際的功能需求來設計的,所以不同開發人員編寫的函數類型各不相同

(七)函數的嵌套調用

def testB():
	print('---- testB start----')
	print('這裏是testB函數執行的代碼...(省略)...')
	print('---- testB end----')


def testA():
	print('---- testA start----')
	testB()
	print('---- testA end----')

testA()

結果:

---- testA start----
---- testB start----
這裏是testB函數執行的代碼…(省略)…
---- testB end----
---- testA end----

小總結:

  • 一個函數裏面又調用了另外一個函數,這就是所謂的函數嵌套調用
  • 如果函數A中,調用了另外一個函數B,那麼先把函數B中的任務都執行完畢之後纔會回到上次 函數A執行的位置

(八)局部變量

(1)什麼是局部變量

如下圖所示:

(2)小總結

  • 局部變量,就是在函數內部定義的變量
  • 不同的函數,可以定義相同的名字的局部變量,但是各用個的不會產生影響
  • 局部變量的作用,爲了臨時保存數據需要在函數中定義變量來進行存儲,這就是它的作用

(九)全局變量

(1)什麼是全局變量

如果一個變量,既能在一個函數中使用,也能在其他的函數中使用,這樣的變量就是全局變量
demo如下:
# 定義全局變量
a = 100

def test1():
	print(a)

def test2():
	print(a)

# 調用函數
test1()
test2()

運行結果:

100
100

(2)全局變量和局部變量名字相同問題

看如下代碼:

(3)修改全局變量

既然全局變量,就是能夠在所以的函數中進行使用,那麼可否進行修改呢?
代碼如下:

(4)總結1:

  • 在函數外邊定義的變量叫做全局變量
  • 全局變量能夠在所有的函數中進行訪問
  • 如果在函數中修改全局變量,那麼就需要使用global進行聲明,否則出錯
  • 如果全局變量的名字和局部變量的名字相同,那麼使用的是局部變量的,小技巧強龍不壓地頭蛇

(5)可變類型的全局變量

>>> a = 1
>>> def f():
...     a += 1
...     print(a)
...
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in f
UnboundLocalError: local variable 'a' referenced before assignment
>>>
>>>
>>> li = [1,]
>>> def f2():
...     li.append(1)
...     print(li)
...
>>> f2()
[1, 1]
>>> li
[1, 1]

(6)總結2:

  • 在函數中不使用global聲明全局變量時不能修改全局變量的本質是不能修改全局變量的指向,即不能將全局變量指向新的數據。
  • 對於不可變類型的全局變量來說,因其指向的數據不能修改,所以不使用global時無法修改全局變量。
  • 對於可變類型的全局變量來說,因其指向的數據可以修改,所以不使用global時也可修改全局變量。

(十)函數返回值②

在python中我們可不可以返回多個值?

>>> def divid(a, b):
...     shang = a//b
...     yushu = a%b 
...     return shang, yushu
...
>>> sh, yu = divid(5, 2)
>>> lb = divid(5, 2)
>>> sh
2
>>> yu
1
>>> lb
(2,1)

本質是利用了元組

(十一)函數參數②

(1)缺省參數

調用函數時,缺省參數的值如果沒有傳入,則被認爲是默認值。下例會打印默認的age,如果age沒有被傳入:

def printinfo( name, age = 35 ):
   # 打印任何傳入的字符串
   print("Name: ", name)
   print("Age ", age)

# 調用printinfo函數
printinfo(name="9527" )
printinfo( age=9,name="9527" )

以上實例輸出結果:

Name: 9527
Age 35
Name: 9527
Age 9

注意:帶有默認值的參數一定要位於參數列表的最後面(可設置多個帶默認值的參數)。

>>> def printinfo(name, age=35, sex):
...     print name
...
  File "<stdin>", line 1
SyntaxError: non-default argument follows default argument

(2)不定長參數

有時可能需要一個函數能處理比當初聲明時更多的參數。這些參數叫做不定長參數,聲明時不會命名。
基本語法如下:

def functionname([formal_args,] *args, **kwargs):
	"函數_文檔字符串"
	function_suite
	return [expression]

加了星號(*)的變量args會存放所有未命名的變量參數,args爲元組;而加**的變量kwargs會存放命名參數,即形如key=value的參數, kwargs爲字典。

>>> def fun(a, b, *args, **kwargs):
...     """可變參數演示示例"""
...     print("a =", a)
...     print("b =", b)
...     print("args =", args)
...     print("kwargs: ")
...     for key, value in kwargs.items():
...         print(key, "=", value)
...
>>> fun(1, 2, 3, 4, 5, m=6, n=7, p=8)  # 注意傳遞的參數對應
a = 1
b = 2
args = (3, 4, 5)
kwargs: 
p = 8
m = 6
n = 7
>>>
>>>
>>>
>>> c = (3, 4, 5)
>>> d = {"m":6, "n":7, "p":8}
>>> fun(1, 2, *c, **d)    # 注意元組與字典的傳參方式
a = 1
b = 2
args = (3, 4, 5)
kwargs: 
p = 8
m = 6
n = 7
>>>
>>>
>>>
>>> fun(1, 2, c, d) # 注意不加星號與上面的區別
a = 1
b = 2
args = ((3, 4, 5), {'p': 8, 'm': 6, 'n': 7})
kwargs:
>>>
>>>

(3)引用傳參

  • 可變類型與不可變類型的變量分別作爲函數參數時,會有什麼不同嗎?
  • Python有沒有類似C語言中的指針傳參呢?
>>> def selfAdd(a):
...     """自增"""
...     a += a
...
>>> a_int = 1
>>> a_int
1
>>> selfAdd(a_int)
>>> a_int
1
>>> a_list = [1, 2]
>>> a_list
[1, 2]
>>> selfAdd(a_list)
>>> a_list
[1, 2, 1, 2]

Python中函數參數是引用傳遞(注意不是值傳遞)。對於不可變類型,因變量不能修改,所以運算不會影響到變量自身;而對於可變類型來說,函數體中的運算有可能會更改傳入的參數變量。

想一想爲什麼

>>> def selfAdd(a):
...     """自增"""
...     a = a + a   # 我們更改了函數體的這句話
...
>>> a_int = 1
>>> a_int
1
>>> selfAdd(a_int)
>>> a_int
1
>>> a_list = [1, 2]
>>> a_list
[1, 2]
>>> selfAdd(a_list)
>>> a_list
[1, 2]      # 想一想爲什麼沒有變呢?

(十二)引用

在python中,值是靠引用來傳遞來的。
我們可以用id()來判斷兩個變量是否爲同一個值的引用。 我們可以將id值理解爲那塊內存的地址標示。

>>> a = 1
>>> b = a
>>> id(a) 
13033816
>>> id(b)   # 注意兩個變量的id值相同
13033816
>>> a = 2
>>> id(a)   # 注意a的id值已經變了
13033792
>>> id(b)   # b的id值依舊
13033816
>>> a = [1, 2]
>>> b = a
>>> id(a)
139935018544808
>>> id(b)
139935018544808
>>> a.append(3)
>>> a
[1, 2, 3]
>>> id(a)
139935018544808
>>> id(b)       # 注意a與b始終指向同一個地址
139935018544808
>>> b
[1, 2, 3]

可變類型與不可變類型
可變類型,值可以改變:

  • 列表 list
  • 字典 dict

不可變類型,值不可以改變:

  • 數值類型 int, long, bool, float
  • 字符串 str
  • 元組 tuple

(十三)遞歸函數

(1)什麼是遞歸函數

通過前面的學習知道一個函數可以調用其他函數。
如果一個函數在內部不調用其它的函數,而是自己本身的話,這個函數就是遞歸函數。

(2)遞歸函數的作用

舉個例子,我們來計算階乘 n! = 1 * 2 * 3 * ... * n
解決辦法1:

看階乘的規律

1! = 1
2! = 2 × 1 = 2 × 1!
3! = 3 × 2 × 1 = 3 × 2!
4! = 4 × 3 × 2 × 1 = 4 × 3!

n! = n × (n-1)!

解決辦法2:

原理

解決辦法3:

def fact(n):
    if n==1:
        return 1
    return n * fact(n - 1)

遞歸函數的優點是定義簡單,邏輯清晰。理論上,所有的遞歸函數都可以寫成循環的方式,但循環的邏輯不如遞歸清晰。

使用遞歸函數需要注意防止棧溢出。在計算機中,函數調用是通過棧(stack)這種數據結構實現的,每當進入一個函數調用,棧就會加一層棧幀,每當函數返回,棧就會減一層棧幀。由於棧的大小不是無限的,所以,遞歸調用的次數過多,會導致棧溢出。可以試試fact(1000)

>>> fact(1000)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in fact
  ...
  File "<stdin>", line 4, in fact
RuntimeError: maximum recursion depth exceeded in comparison

解決遞歸調用棧溢出的方法是通過尾遞歸優化,事實上尾遞歸和循環的效果是一樣的,所以,把循環看成是一種特殊的尾遞歸函數也是可以的。

尾遞歸是指,在函數返回的時候,調用自身本身,並且,return語句不能包含表達式。這樣,編譯器或者解釋器就可以把尾遞歸做優化,使遞歸本身無論調用多少次,都只佔用一個棧幀,不會出現棧溢出的情況。

上面的fact(n)函數由於return n * fact(n - 1)引入了乘法表達式,所以就不是尾遞歸了。要改成尾遞歸方式,需要多一點代碼,主要是要把每一步的乘積傳入到遞歸函數中:

def fact(n):
    return fact_iter(n, 1)

def fact_iter(num, product):
    if num == 1:
        return product
    return fact_iter(num - 1, num * product)

可以看到,return fact_iter(num - 1, num * product)僅返回遞歸函數本身,num - 1num * product在函數調用前就會被計算,不影響函數調用。
fact(5)對應的fact_iter(5, 1)的調用如下:

===> fact_iter(5, 1)
===> fact_iter(4, 5)
===> fact_iter(3, 20)
===> fact_iter(2, 60)
===> fact_iter(1, 120)
===> 120

尾遞歸調用時,如果做了優化,棧不會增長,因此,無論多少次調用也不會導致棧溢出。

遺憾的是,大多數編程語言沒有針對尾遞歸做優化,Python解釋器也沒有做優化,所以,即使把上面的fact(n)函數改成尾遞歸方式,也會導致棧溢出。

(3)小結

  • 使用遞歸函數的優點是邏輯簡單清晰,缺點是過深的調用會導致棧溢出。
  • 針對尾遞歸優化的語言可以通過尾遞歸防止棧溢出。尾遞歸事實上和循環是等價的,沒有循環語句的編程語言只能通過尾遞歸實現循環。
  • Python標準的解釋器沒有針對尾遞歸做優化,任何遞歸函數都存在棧溢出的問題。

(十四)匿名函數

lambda關鍵詞能創建小型匿名函數。這種函數得名於省略了用def聲明函數的標準步驟。
lambda函數的語法只包含一個語句,如下:

lambda [arg1 [,arg2,.....argn]]:expression

如下實例:

sum = lambda arg1, arg2: arg1 + arg2

#調用sum函數
print("Value of total : ", sum( 10, 20 ))
print("Value of total : ", sum( 20, 20 ))

以上實例輸出結果:

Value of total : 30
Value of total : 40

Lambda函數能接收任何數量的參數但只能返回一個表達式的值
匿名函數不能直接調用print,因爲lambda需要一個表達式

應用場合

函數作爲參數傳遞函數作爲參數傳遞
①自己定義函數

>>> def fun(a, b, opt):
...     print("a =", a)
...     print("b =", b)
...     print("result =", opt(a, b))
...
>>> fun(1, 2, lambda x,y:x+y)
a = 1
b = 2
result = 3

②作爲內置函數的參數

想一想,下面的數據如何指定按age或name排序?

stus = [
    {"name":"zhangsan", "age":18}, 
    {"name":"lisi", "age":19}, 
    {"name":"wangwu", "age":17}
]

按name排序:

>>> stus.sort(key = lambda x:x['name'])
>>> stus
[{'age': 19, 'name': 'lisi'}, {'age': 17, 'name': 'wangwu'}, {'age': 18, 'name': 'zhangsan'}]

按age排序:

>>> stus.sort(key = lambda x:x['age'])
>>> stus
[{'age': 17, 'name': 'wangwu'}, {'age': 18, 'name': 'zhangsan'}, {'age': 19, 'name': 'lisi'}]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章