1. 程式人生 > >Python入門學習---第三天

Python入門學習---第三天

函式

函式

基本上所有的高階語言都支援函式,Python也不例外。Python不但能非常靈活地定義函式,而且本身內建了很多有用的函式,可以直接呼叫。

抽象是數學中非常常見的概念。寫計算機程式也是一樣,函式就是最基本的一種程式碼抽象的方式。

呼叫函式

Python內建了很多有用的函式,我們可以直接呼叫(這也正是吸引眾多程式設計師來學習python的原因)。

例如:abs函式,可以在互動式命令列通過help(abs)檢視abs函式的幫助資訊。

>>> abs(100)
100
>>> abs(-20)
20
>>> abs
(12.34) 12.34 # 呼叫函式的時候,如果傳入的引數數量不對,會報TypeError的錯誤, # 並且Python會明確地告訴你:abs()有且僅有1個引數,但給出了兩個: >>> abs(1, 2) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: abs() takes exactly one argument (2 given) # 而max函式max()可以接收任意多個引數,並返回最大的那個: >>> max
(1, 2) 2 >>> max(2, 3, 1, -5) 3

資料型別轉換

Python內建的常用函式還包括資料型別轉換函式。

# 比如int()函式可以把其他資料型別轉換為整數:
>>> int('123')
123
>>> int(12.34)
12

>>> float('12.34')
12.34
>>> str(1.23)
'1.23'
>>> str(100)
'100'
>>> bool(1)
True
>>> bool('')
False

函式名其實就是指向一個函式物件的引用。

# 完全可以把函式名賦給一個變數,相當於給這個函式起了一個“別名”:
>>> a = abs # 變數a指向abs函式
>>> a(-1) # 所以也可以通過a呼叫abs函式
1

練習

請利用Python內建的hex()函式把一個整數轉換成十六進位制表示的字串:

# -*- coding: utf-8 -*-
n1 = 255
n2 = 1000

0xff
0x3e8

呼叫Python的函式,需要根據函式定義,傳入正確的引數。如果函式調用出錯,一定要學會看錯誤資訊,所以英文很重要!

定義函式

在Python中,定義一個函式要使用def語句,依次寫出函式名、括號、括號中的引數和冒號 : ,然後,在縮排塊中編寫函式體,函式的返回值用return語句返回

# -*- coding: utf-8 -*-
# 我們以自定義一個求絕對值的my_abs函式為例:
def my_abs(x):
    if x >= 0:
        return x
    else:
        return -x

如果你已經把 my_abs() 的函式定義儲存為abstest.py檔案了,那麼,可以在該檔案的當前目錄下啟動Python直譯器,用 from abstest import my_abs 來匯入 my_abs() 函式注意abstest是檔名(不含.py副檔名):

如下:

>>> from abstest import my_abs
>>> my_abs(-9)
9

import的用法在後續模組一節中會詳細介紹。

空函式

如果想定義一個什麼事也不做的空函式,可以用 pass語句

def nop():
    pass

pass語句什麼都不做,那有什麼用?實際上pass可以用來作為佔位符,比如現在還沒想好怎麼寫函式的程式碼,就可以先放一個pass,讓程式碼能執行起來。

pass還可以用在其他語句裡,比如:

# 缺少了pass,程式碼執行就會有語法錯誤。
if age >= 18:
    pass

引數檢查
呼叫函式時,如果引數個數不對,Python直譯器會自動檢查出來,並丟擲TypeError:

>>> my_abs(1, 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: my_abs() takes 1 positional argument but 2 were given

但是如果引數型別不對,Python直譯器就無法幫我們檢查。試試my_abs和內建函式abs的差別:

>>> my_abs('A')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in my_abs
TypeError: unorderable types: str() >= int()
>>> abs('A')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: bad operand type for abs(): 'str'

當傳入了不恰當的引數時,內建函式abs會檢查出引數錯誤,而我們定義的my_abs沒有引數檢查,會導致if語句出錯,出錯資訊和abs不一樣。所以,這個函式定義不夠完善

讓我們修改一下my_abs的定義,對引數型別做檢查,只允許整數和浮點數型別的引數。資料型別檢查可以用內建函式isinstance()實現

def my_abs(x):
    if not isinstance(x, (int, float)):
        raise TypeError('bad operand type')
    if x >= 0:
        return x
    else:
        return -x

添加了引數檢查後,如果傳入錯誤的引數型別,函式就可以丟擲一個錯誤:

>>> my_abs('A')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in my_abs
TypeError: bad operand type

返回多個值

函式可以返回多個值嗎?答案是肯定的。

比如在遊戲中經常需要從一個點移動到另一個點,給出座標、位移和角度,就可以計算出新的新的座標:

import math

def move(x, y, step, angle=0):
    nx = x + step * math.cos(angle)
    ny = y - step * math.sin(angle)
    return nx, ny

import math 語句表示匯入math包,並允許後續程式碼引用math包裡的sin、cos等函式

然後,我們就可以同時獲得返回值:

>>> x, y = move(100, 100, 60, math.pi / 6)
>>> print(x, y)
151.96152422706632 70.0

但其實這只是一種假象,Python函式返回的仍然是單一值:

>>> r = move(100, 100, 60, math.pi / 6)
>>> print(r)
(151.96152422706632, 70.0)

原來返回值是一個tuple!但是,在語法上,返回一個tuple可以省略括號,而多個變數可以同時接收一個tuple,按位置賦給對應的值,所以,Python的函式返回多值其實就是返回一個tuple,但寫起來更方便

小結

  • 定義函式時,需要確定函式名和引數個數
  • 如果有必要,可以先對引數的資料型別做檢查;
  • 函式體內部可以用return隨時返回函式結果
  • 函式執行完畢也沒有return語句時,自動return None
  • 函式可以同時返回多個值,但其實就是一個tuple

練習

請定義一個函式quadratic(a, b, c),接收3個引數,返回一元二次方程:

ax2+bx+c=0的兩個解。
# 提示:計算平方根可以呼叫math.sqrt()函式:
# -*- coding: utf-8 -*-

import math
def quadratic(a, b, c):
    x1 = (-b + math.sqrt(b*b-4*a*c))/(2*a)
    x2 = (-b - math.sqrt(b*b-4*a*c))/(2*a)
    return x1,x2

print('quadratic(2, 3, 1) =', quadratic(2, 3, 1))
print('quadratic(1, 3, -4) =', quadratic(1, 3, -4))

if quadratic(2, 3, 1) != (-0.5, -1.0):
    print('測試失敗')
elif quadratic(1, 3, -4) != (1.0, -4.0):
    print('測試失敗')
else:
    print('測試成功')

函式的引數

遞迴函式

在函式內部,可以呼叫其他函式。如果一個函式在內部呼叫自身本身,這個函式就是遞迴函式

舉個例子,我們來計算 階乘n! = 1 x 2 x 3 x … x n ,用函式fact(n)表示,可以看出:

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

>>> fact(1)
1
>>> fact(5)
120
>>> fact(100)
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000

使用遞迴函式需要注意防止棧溢位。在計算機中,函式呼叫是通過棧(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

解決遞迴呼叫棧溢位的方法是通過尾遞迴優化,事實上尾遞迴和迴圈的效果是一樣的,所以,把迴圈看成是一種特殊的尾遞迴函式也是可以的。

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)

我們來看看遞迴跟尾遞迴的區別:

遞迴過程:

===> fact(5)
===> 5 * fact(4)
===> 5 * (4 * fact(3))
===> 5 * (4 * (3 * fact(2)))
===> 5 * (4 * (3 * (2 * fact(1))))
===> 5 * (4 * (3 * (2 * 1)))
===> 5 * (4 * (3 * 2))
===> 5 * (4 * 6)
===> 5 * 24
===> 120

每當進入一個函式呼叫,棧就會加一層棧幀,每當函式返回,棧就會減一層棧幀。

尾遞迴過程:

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

可以看到,return fact_iter(num - 1, num * product)僅返回遞迴函式本身,num - 1和num * product在函式呼叫前就會被計算,不影響函式呼叫。

尾遞迴呼叫時,如果做了優化,棧不會增長,因此,無論多少次呼叫也不會導致棧溢位。

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

小結

使用遞迴函式的優點是邏輯簡單清晰,缺點是過深的呼叫會導致棧溢位。

針對尾遞迴優化的語言可以通過尾遞迴防止棧溢位。尾遞迴事實上和迴圈是等價的,沒有迴圈語句的程式語言只能通過尾遞迴實現迴圈。

Python標準的直譯器沒有針對尾遞迴做優化,任何遞迴函式都存在棧溢位的問題。

練習

遞迴函式

閱讀: 387346
在函式內部,可以呼叫其他函式。如果一個函式在內部呼叫自身本身,這個函式就是遞迴函式。

舉個例子,我們來計算階乘n! = 1 x 2 x 3 x … x n,用函式fact(n)表示,可以看出:

fact(n) = n! = 1 x 2 x 3 x … x (n-1) x n = (n-1)! x n = fact(n-1) x n

所以,fact(n)可以表示為n x fact(n-1),只有n=1時需要特殊處理。

於是,fact(n)用遞迴的方式寫出來就是:

def fact(n):
if n==1:
return 1
return n * fact(n - 1)
上面就是一個遞迴函式。可以試試:

fact(1)
1
fact(5)
120
fact(100)
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
如果我們計算fact(5),可以根據函式定義看到計算過程如下:

===> fact(5)
===> 5 * fact(4)
===> 5 * (4 * fact(3))
===> 5 * (4 * (3 * fact(2)))
===> 5 * (4 * (3 * (2 * fact(1))))
===> 5 * (4 * (3 * (2 * 1)))
===> 5 * (4 * (3 * 2))
===> 5 * (4 * 6)
===> 5 * 24
===> 120
遞迴函式的優點是定義簡單,邏輯清晰。理論上,所有的遞迴函式都可以寫成迴圈的方式,但迴圈的邏輯不如遞迴清晰。

使用遞迴函式需要注意防止棧溢位。在計算機中,函式呼叫是通過棧(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 - 1和num * 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)函式改成尾遞迴方式,也會導致棧溢位。

小結

使用遞迴函式的優點是邏輯簡單清晰,缺點是過深的呼叫會導致棧溢位。

針對尾遞迴優化的語言可以通過尾遞迴防止棧溢位。尾遞迴事實上和迴圈是等價的,沒有迴圈語句的程式語言只能通過尾遞迴實現迴圈。

Python標準的直譯器沒有針對尾遞迴做優化,任何遞迴函式都存在棧溢位的問題。

練習

漢諾塔的移動可以用遞迴函式非常簡單地實現。

請編寫move(n, a, b, c)函式,它接收引數n,表示3個柱子A、B、C中第1個柱子A的盤子數量,然後打印出把所有盤子從A藉助B移動到C的方法,例如:

# -*- coding: utf-8 -*-
def move(n, a, b, c): 
     if n == 1:   # 如果a只有1盤子
         print(a, '-->', c);   # 直接把盤子從a移到c
     else:   # 如果a有n個盤子(n > 1),那麼分三步
         move(n-1, a, c, b)   # 先把上面n-1個盤子,藉助c,從a移到b
         move(1, a, b, c)   # 再把最下面的1個盤子,藉助b,從a移到c
         move(n-1, b, a, c)   # 最後把n-1個盤子,藉助a,從b移到c

 move(4,'A','B','C')  # 測試