草根學Python(六) 函式

兩點水發表於2017-06-28

前言

前天創了個 Python 微信討論群,以為沒人進的,哈哈,想不到還真有小夥伴進群學習討論。如果想進群,可以加我微信: androidwed ,拉進群,就不貼微信群二維碼了,一是會失效,二影響文章。

目錄

草根學Python(六)  函式
草根學Python(六) 函式

一、Python 自定義函式的基本步驟

函式是組織好的,可重複使用的,用來實現單一,或相關聯功能的程式碼段。

自定義函式,基本有以下規則步驟:

  • 函式程式碼塊以 def 關鍵詞開頭,後接函式識別符號名稱和圓括號()
  • 任何傳入引數和自變數必須放在圓括號中間。圓括號之間可以用於定義引數
  • 函式的第一行語句可以選擇性地使用文件字串(用於存放函式說明)
  • 函式內容以冒號起始,並且縮排
  • return [表示式] 結束函式,選擇性地返回一個值給呼叫方。不帶表示式的 return 相當於返回 None。

語法示例:

def functionname( parameters ):
   "函式_文件字串"
   function_suite
   return [expression]複製程式碼

例項:

  1. def 定義一個函式,給定一個函式名 sum
  2. 宣告兩個引數 num1 和 num2
  3. 函式的第一行語句進行函式說明:兩數之和
  4. 最終 return 語句結束函式,並返回兩數之和
def sum(num1,num2):
    "兩數之和"
    return num1+num2

# 呼叫函式
print(sum(5,6))複製程式碼

輸出結果:

11複製程式碼

二、函式傳值問題

先看一個例子:

# -*- coding: UTF-8 -*-
def chagne_number( b ):
    b = 1000

b = 1
chagne_number(b)
print( b )複製程式碼

最後輸出的結果為:

1複製程式碼

這裡可能有些人會有疑問,為啥不是通過函式chagne_number更改了 b
的值嗎?為啥沒有變化,輸出的結果還是 1 ,這個問題很多程式語言都會講到,原理解釋也是差不多的。

這裡主要是函式引數的傳遞中,傳遞的是型別物件,之前也介紹了 Python 中基本的資料型別等。而這些型別物件可以分為可更改型別和不可更改的型別

在 Python 中,字串,整形,浮點型,tuple 是不可更改的物件,而 list , dict 等是可以更改的物件。

例如:

不可更改的型別:變數賦值 a = 1,其實就是生成一個整形物件 1 ,然後變數 a 指向 1,當 a = 1000 其實就是再生成一個整形物件 1000,然後改變 a 的指向,不再指向整形物件 1 ,而是指向 1000,最後 1 會被丟棄

可更改的型別:變數賦值 a = [1,2,3,4,5,6] ,就是生成一個物件 list ,list 裡面有 6 個元素,而變數 a 指向 list ,a[2] = 5則是將 list a 的第三個元素值更改,這裡跟上面是不同的,並不是將 a 重新指向,而是直接修改 list 中的元素值。

指向問題
指向問題

這也將影響到函式中引數的傳遞了:

不可更改的型別:類似 c++ 的值傳遞,如 整數、字串、元組。如fun(a),傳遞的只是 a 的值,沒有影響 a 物件本身。比如在 fun(a)內部修改 a 的值,只是修改另一個複製的物件,不會影響 a 本身。

可更改的型別:類似 c++ 的引用傳遞,如 列表,字典。如 fun(a),則是將 a 真正的傳過去,修改後 fun 外部的 a 也會受影響

因此,在一開始的例子中,b = 1,建立了一個整形物件 1 ,變數 b 指向了這個物件,然後通過函式 chagne_number 時,按傳值的方式複製了變數 b ,傳遞的只是 b 的值,並沒有影響到 b 的本身。具體可以看下修改後的例項,通過列印的結果更好的理解。

# -*- coding: UTF-8 -*-
def chagne_number( b ):
    print('函式中一開始 b 的值:{}' .format( b ) )
    b = 1000
    print('函式中 b 賦值後的值:{}' .format( b ) )


b = 1
chagne_number( b )
print( '最後輸出 b 的值:{}' .format( b )  )複製程式碼

列印的結果:

函式中一開始 b 的值:1
函式中 b 賦值後的值:1000
最後輸出 b 的值:1複製程式碼

當然,如果引數中的是可更改的型別,那麼呼叫了這個函式後,原來的值也會被更改,具體例項如下:

# -*- coding: UTF-8 -*-

def chagne_list( b ):
    print('函式中一開始 b 的值:{}' .format( b ) )
    b.append(1000)
    print('函式中 b 賦值後的值:{}' .format( b ) )


b = [1,2,3,4,5]
chagne_list( b )
print( '最後輸出 b 的值:{}' .format( b )  )複製程式碼

輸出的結果:

函式中一開始 b 的值:[1, 2, 3, 4, 5]
函式中 b 賦值後的值:[1, 2, 3, 4, 5, 1000]
最後輸出 b 的值:[1, 2, 3, 4, 5, 1000]複製程式碼

三、函式返回值

通過上面的學習,可以知道通過 return [表示式] 語句用於退出函式,選擇性地向呼叫方返回一個表示式。不帶引數值的 return 語句返回 None。

具體示例:

# -*- coding: UTF-8 -*-

def sum(num1,num2):
    # 兩數之和
    if not (isinstance (num1,(int ,float)) or isinstance (num2,(int ,float))):
        raise TypeError('引數型別錯誤')
    return num1+num2

print(sum(1,2))複製程式碼

返回結果:

3複製程式碼

這個示例,還通過內建函式isinstance()進行資料型別檢查,檢查呼叫函式時引數是否是整形和浮點型。如果引數型別不對,會報錯,提示 引數型別錯誤,如圖:

檢查函式引數是否正確
檢查函式引數是否正確

當然,函式也可以返回多個值,具體例項如下:

# -*- coding: UTF-8 -*-

def  division ( num1, num2 ):
    # 求商與餘數
         a = num1 % num2
         b = (num1-a) / num2
         return b , a 

num1 , num2 = division(9,4)
tuple1 = division(9,4)

print (num1,num2)
print (tuple1)複製程式碼

輸出的值:

2.0 1
(2.0, 1)複製程式碼

認真觀察就可以發現,儘管從第一個輸出值來看,返回了多個值,實際上是先建立了一個元組然後返回的。回憶一下,元組是可以直接用逗號來建立的,觀察例子中的 ruturn ,可以發現實際上我們使用的是逗號來生成一個元組。

四、函式的引數

1、預設值引數

有時候,我們自定義的函式中,如果呼叫的時候沒有設定引數,需要給個預設值,這時候就需要用到預設值引數了。

# -*- coding: UTF-8 -*-

def print_user_info( name , age , sex = '男' ):
    # 列印使用者資訊
    print('暱稱:{}'.format(name) , end = ' ')
    print('年齡:{}'.format(age) , end = ' ')
    print('性別:{}'.format(sex))
    return;

# 呼叫 print_user_info 函式

print_user_info( '兩點水' , 18 , '女')
print_user_info( '三點水' , 25 )複製程式碼

輸出結果:

暱稱:兩點水 年齡:18 性別:女
暱稱:三點水 年齡:25 性別:男複製程式碼

可以看到,當你設定了預設引數的時候,在呼叫函式的時候,不傳該引數,就會使用預設值。但是這裡需要注意的一點是:只有在形參表末尾的那些引數可以有預設引數值,也就是說你不能在宣告函式形參的時候,先宣告有預設值的形參而後宣告沒有預設值的形參。這是因為賦給形參的值是根據位置而賦值的。例如,def func(a, b=1) 是有效的,但是 def func(a=1, b) 是 無效 的。

預設值引數就這樣結束了嗎?還沒有的,細想一下,如果引數中是一個可修改的容器比如一個 lsit (列表)或者 dict (字典),那麼我們使用什麼來作為預設值呢?我們可以使用 None 作為預設值。就像下面這個例子一樣:

# 如果 b 是一個 list ,可以使用 None 作為預設值
def print_info( a , b = None ):
    if b is None :
        b=[]
    return;複製程式碼

認真看下例子,會不會有這樣的疑問呢?在引數中我們直接 b=[] 不就行了嗎?也就是寫成下面這個樣子:

def print_info( a , b = [] ):
    return;複製程式碼

對不對呢?執行一下也沒發現錯誤啊,可以這樣寫嗎?這裡需要特別注意的一點:預設引數的值是不可變的物件,比如None、True、False、數字或字串,如果你像上面的那樣操作,當預設值在其他地方被修改後你將會遇到各種麻煩。這些修改會影響到下次呼叫這個函式時的預設值。

示例如下:

# -*- coding: UTF-8 -*-

def print_info( a , b = [] ):
    print(b)
    return b ;

result = print_info(1)

result.append('error')

print_info(2)複製程式碼

輸出的結果:

[]
['error']複製程式碼

認真觀察,你會發現第二次輸出的值根本不是你想要的,因此切忌不能這樣操作。

還有一點,有時候我就是不想要預設值啊,只是想單單判斷預設引數有沒有值傳遞進來,那該怎麼辦?我們可以這樣做:

_no_value =object()

def print_info( a , b = _no_value ):
    if b is _no_value :
        print('b 沒有賦值')
    return;複製程式碼

這裡的 object 是python中所有類的基類。 你可以建立 object 類的例項,但是這些例項沒什麼實際用處,因為它並沒有任何有用的方法, 也沒有任何例項資料(因為它沒有任何的例項字典,你甚至都不能設定任何屬性值)。 你唯一能做的就是測試同一性。也正好利用這個特性,來判斷是否有值輸入。

2、關鍵字引數

在 Python 中,可以通過引數名來給函式傳遞引數,而不用關心引數列表定義時的順序,這被稱之為關鍵字引數。使用關鍵引數有兩個優勢 :

一、由於我們不必擔心引數的順序,使用函式變得更加簡單了。

二、假設其他引數都有預設值,我們可以只給我們想要的那些引數賦值

# -*- coding: UTF-8 -*-

def print_user_info( name ,  age  , sex = '男' ):
    # 列印使用者資訊
    print('暱稱:{}'.format(name) , end = ' ')
    print('年齡:{}'.format(age) , end = ' ')
    print('性別:{}'.format(sex))
    return;

# 呼叫 print_user_info 函式

print_user_info( name = '兩點水' ,age = 18 , sex = '女')
print_user_info( name = '兩點水' ,sex = '女', age = 18 )複製程式碼

輸出的值:

暱稱:兩點水 年齡:18 性別:女
暱稱:兩點水 年齡:18 性別:女複製程式碼

3、不定長引數

有時我們在設計函式介面的時候,可會需要可變長的引數。也就是說,我們事先無法確定傳入的引數個數。Python 提供了一種元組的方式來接受沒有直接定義的引數。這種方式在引數前邊加星號 * 。如果在函式呼叫時沒有指定引數,它就是一個空元組。我們也可以不向函式傳遞未命名的變數。

例如:

# -*- coding: UTF-8 -*-

def print_user_info( name ,  age  , sex = '男' , * hobby):
    # 列印使用者資訊
    print('暱稱:{}'.format(name) , end = ' ')
    print('年齡:{}'.format(age) , end = ' ')
    print('性別:{}'.format(sex) ,end = ' ' )
    print('愛好:{}'.format(hobby))
    return;

# 呼叫 print_user_info 函式
print_user_info( '兩點水' ,18 , '女', '打籃球','打羽毛球','跑步')複製程式碼

輸出的結果:

暱稱:兩點水 年齡:18 性別:女 愛好:('打籃球', '打羽毛球', '跑步')複製程式碼

通過輸出的結果可以知道,*hobby是可變引數,且 hobby其實就是一個 tuple (元祖)

可變長引數也支援關鍵引數,沒有被定義的關鍵引數會被放到一個字典裡。這種方式即是在引數前邊加 **,更改上面的示例如下:

# -*- coding: UTF-8 -*-

def print_user_info( name ,  age  , sex = '男' , ** hobby ):
    # 列印使用者資訊
    print('暱稱:{}'.format(name) , end = ' ')
    print('年齡:{}'.format(age) , end = ' ')
    print('性別:{}'.format(sex) ,end = ' ' )
    print('愛好:{}'.format(hobby))
    return;

# 呼叫 print_user_info 函式
print_user_info( name = '兩點水' , age = 18 , sex = '女', hobby = ('打籃球','打羽毛球','跑步'))複製程式碼

輸出的結果:

暱稱:兩點水 年齡:18 性別:女 愛好:{'hobby': ('打籃球', '打羽毛球', '跑步')}複製程式碼

通過對比上面的例子和這個例子,可以知道,*hobby是可變引數,且 hobby其實就是一個 tuple (元祖),**hobby是關鍵字引數,且 hobby 就是一個 dict (字典)

4、只接受關鍵字引數

關鍵字引數使用起來簡單,不容易引數出錯,那麼有些時候,我們定義的函式希望某些引數強制使用關鍵字引數傳遞,這時候該怎麼辦呢?

將強制關鍵字引數放到某個*引數或者單個*後面就能達到這種效果,比如:

# -*- coding: UTF-8 -*-

def print_user_info( name , *, age  , sex = '男' ):
    # 列印使用者資訊
    print('暱稱:{}'.format(name) , end = ' ')
    print('年齡:{}'.format(age) , end = ' ')
    print('性別:{}'.format(sex))
    return;

# 呼叫 print_user_info 函式
print_user_info( name = '兩點水' ,age = 18 , sex = '女' )

# 這種寫法會報錯,因為 age ,sex 這兩個引數強制使用關鍵字引數
#print_user_info( '兩點水' , 18 , '女' )
print_user_info('兩點水',age='22',sex='男')複製程式碼

通過例子可以看,如果 age , sex 不適用關鍵字引數是會報錯的。

很多情況下,使用強制關鍵字引數會比使用位置參數列意更加清晰,程式也更加具有可讀性。使用強制關鍵字引數也會比使用 **kw 引數更好且強制關鍵字引數在一些更高階場合同樣也很有用。

五、匿名函式

有沒有想過定義一個很短的回撥函式,但又不想用 def 的形式去寫一個那麼長的函式,那麼有沒有快捷方式呢?答案是有的。

python 使用 lambda 來建立匿名函式,也就是不再使用 def 語句這樣標準的形式定義一個函式。

匿名函式主要有以下特點:

  • lambda 只是一個表示式,函式體比 def 簡單很多。
  • lambda 的主體是一個表示式,而不是一個程式碼塊。僅僅能在 lambda 表示式中封裝有限的邏輯進去。
  • lambda 函式擁有自己的名稱空間,且不能訪問自有引數列表之外或全域性名稱空間裡的引數。

基本語法

lambda [arg1 [,arg2,.....argn]]:expression複製程式碼

示例:

# -*- coding: UTF-8 -*-

sum = lambda num1 , num2 : num1 + num2;

print( sum( 1 , 2 ) )複製程式碼

輸出的結果:

3複製程式碼

注意:儘管 lambda 表示式允許你定義簡單函式,但是它的使用是有限制的。 你只能指定單個表示式,它的值就是最後的返回值。也就是說不能包含其他的語言特性了, 包括多個語句、條件表示式、迭代以及異常處理等等。

匿名函式中,有一個特別需要注意的問題,比如,把上面的例子改一下:

# -*- coding: UTF-8 -*-

num2 = 100
sum1 = lambda num1 : num1 + num2 ;

num2 = 10000
sum2 = lambda num1 : num1 + num2 ;

print( sum1( 1 ) )
print( sum2( 1 ) )複製程式碼

你會認為輸出什麼呢?第一個輸出是 101,第二個是 10001,結果不是的,輸出的結果是這樣:

10001
10001複製程式碼

這主要在於 lambda 表示式中的 num2 是一個自由變數,在執行時繫結值,而不是定義時就繫結,這跟函式的預設值引數定義是不同的。所以建議還是遇到這種情況還是使用第一種解法。

相關文章