1. 程式人生 > >聰哥哥教你學Python之函式

聰哥哥教你學Python之函式

什麼是函式?

從數學的角度分析:

函式的定義:給定一個數集A,假設其中的元素為x。現對A中的元素x施加對應法則f,記作f(x),得到另一數集B。假設B中的元素為y。則y與x之間的等量關係可以用y=f(x)表示。我們把這個關係式就叫函式關係式,簡稱函式。函式概念含有三個要素:定義域A、值域C和對應法則f。其中核心是對應法則f,它是函式關係的本質特徵。

或許會人問我,親愛的聰哥哥你是講數學嗎?數學的函式似乎與我們程式設計中的函式不一樣。

我沒有再講數學,但是你可以從平時的函式聯絡到數學。記得某位大師說過一句話,技術的核心,本質上就是數學。

其實你可以想一想,我們平時寫程式碼封裝的對應的函式,在某種意義上,是按照一種關係。這種關係本質就是數學。

還記得在高中的時候做數學題目嗎?

做題目除了看題仔細分析外,順便會用某種關係式太套。而且有的時候關係式並不適用。像我曾經做的一個題目,寫了一大堆,雖然最後12分的題目,全部拿到分數了,但是我卻在這個12分花費大量的時間,以至於相對容易的幾何題目和一些其他大題,雖說不一定分數全部拿到,至少一半應該沒問題。最讓我氣人的是,那個12分,我寫的很滿,到時,看到我的同桌居然非常的簡潔就拿到了12分,當時心中十分氣憤,頓時有種非常不爽的感覺。不過從中看出數學的簡潔之美。好了,扯了會蛋,該進入正題了。

各位奮戰在一線的開發者們,為什麼寫函式,我相信你們都明白,有很多時候,許多重複程式碼段,我們只需編寫一個對應的函式即可解決程式碼冗餘的問題,使程式碼變得優雅,變得可擴充套件性好。這個函式的表達,其實我們也在使用一定的關係式(函式式)來概括。

一、呼叫函式

Python內建很多函式,方便我們根據對應的需求來呼叫,即便有些函式沒有內建,我們可以使用對應的pip來安裝。

絕對值函式:abs()

返回最大值:max()

資料型別轉換函式:int()、float()、str()、bool()等

在此,聰哥哥有話說:

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

二、定義函式

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

返回單個值:

# -*- coding: utf-8 -*-
def my_abs(x):
	if x >= 0:
		return x
	else:
		return -x
		
print(my_abs(-99))

請注意,函式體內部的語句在執行時,一旦執行到return時,函式就執行完畢,並將結果返回。因此,函式內部通過條件判斷和迴圈可以實現非常複雜的邏輯。

如果沒有return語句,函式執行完畢後也會返回結果,只是結果為Nonereturn None可以簡寫為return

 

返回多個值:

# -*- coding: utf-8 -*-
import math
def move(x,y,step,angle=0):
	nx = x + step * math.cos(angle)
	ny = y - step * math.sin(angle)
	return nx,ny
r=move(100,100,60,math.pi/6)
print(r)

聰哥哥有話說:

定義函式時,需要確定函式名和引數個數;

如果有必要,可以先對引數的資料型別做檢查;

函式體內部可以用return隨時返回函式結果;

函式執行完畢也沒有return語句時,自動return None

函式可以同時返回多個值,但其實就是一個tuple。

 

三、函式的引數

定義函式的時候,我們把引數的名字和位置確定下來,函式的介面定義就完成了。對於函式的呼叫者來說,只需要知道如何傳遞正確的引數,以及函式將返回什麼樣的值就夠了,函式內部的複雜邏輯被封裝起來,呼叫者無需瞭解。

Python的函式定義非常簡單,但靈活度卻非常大。除了正常定義的必選引數外,還可以使用預設引數、可變引數和關鍵字引數,使得函式定義出來的介面,不但能處理複雜的引數,還可以簡化呼叫者的程式碼。

引數有很多種:

比如有位置引數、預設引數、可變引數、關鍵字引數、命名關鍵字引數、引數組合等等。

(1)位置引數

示例程式碼:

def power(x):
    return x * x
print(power(5))

(2)預設引數

示例程式碼:

def power(x, n=2):
    s = 1
    while n > 0:
        n = n - 1
        s = s * x
    return s
print(power(5))

(3)可變引數

示例程式碼:

def calc(*numbers):
    sum = 0
    for n in numbers:
        sum = sum + n * n
    return sum

print(calc(1,2))

(4)關鍵字引數

示例程式碼:

def person(name, age, **kw):
    print('name:', name, 'age:', age, 'other:', kw)

print(person('聰哥哥', 18, city='Beijing'))

(5)命名關鍵字引數

示例程式碼:

def person(name, age, **kw):
    if 'city' in kw:
        # 有city引數
        pass
    if 'job' in kw:
        # 有job引數
        pass
    print('name:', name, 'age:', age, 'other:', kw)

print(person('Jack', 24, city='Beijing', addr='Chaoyang', zipcode=123456))

(6)引數組合

示例程式碼:

def f1(a, b, c=0, *args, **kw):
    print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw)

def f2(a, b, c=0, *, d, **kw):
    print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw)

print(f1(1, 2))
print(f2(1, 2, d=99, ext=None))

 

聰哥哥有話說:

(1)Python的函式具有非常靈活的引數形態,既可以實現簡單的呼叫,又可以傳入非常複雜的引數。=;

(2)預設引數一定要用不可變物件,如果是可變物件,程式執行時會有邏輯錯誤;

(3)要注意定義可變引數和關鍵字引數的語法:

(4)*args是可變引數,args接收的是一個tuple;

(5)**kw是關鍵字引數,kw接收的是一個dict。

以及呼叫函式時如何傳入可變引數和關鍵字引數的語法:

可變引數既可以直接傳入:func(1, 2, 3),又可以先組裝list或tuple,再通過*args傳入:func(*(1, 2, 3))

關鍵字引數既可以直接傳入:func(a=1, b=2),又可以先組裝dict,再通過**kw傳入:func(**{'a': 1, 'b': 2})

使用*args**kw是Python的習慣寫法,當然也可以用其他引數名,但最好使用習慣用法。

命名的關鍵字引數是為了限制呼叫者可以傳入的引數名,同時可以提供預設值。

定義命名的關鍵字引數在沒有可變引數的情況下不要忘了寫分隔符*,否則定義的將是位置引數。

 

四、遞迴函式

什麼叫遞迴?

引用百度百科的回答:

程式呼叫自身的程式設計技巧稱為遞迴( recursion)。遞迴作為一種演算法程式設計語言中廣泛應用。 一個過程或函式在其定義或說明中有直接或間接呼叫自身的一種方法,它通常把一個大型複雜的問題層層轉化為一個與原問題相似的規模較小的問題來求解,遞迴策略只需少量的程式就可描述出解題過程所需要的多次重複計算,大大地減少了程式的程式碼量。遞迴的能力在於用有限的語句來定義物件的無限集合。一般來說,遞迴需要有邊界條件、遞迴前進段和遞迴返回段。當邊界條件不滿足時,遞迴前進;當邊界條件滿足時,遞迴返回。

示例程式碼:

# -*- coding: utf-8 -*-
def fact(n):
    if n==1:
        return 1
    return n * fact(n - 1)
	
print(fact(10))

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

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

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

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

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

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

 

聰哥哥怎麼看:

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

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

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