1. 程式人生 > >Python中的函式(轉自linhaifeng)

Python中的函式(轉自linhaifeng)

一 函式知識體系

1 什麼是函式?
2 為什麼要用函式?
3 函式的分類:內建函式與自定義函式
4 如何自定義函式
   語法
 定義有引數函式,及有參函式的應用場景
 定義無引數函式,及無參函式的應用場景
 定義空函式,及空函式的應用場景
5 呼叫函式
  -  如何呼叫函式
  -  函式的返回值
  -  函式引數的應用:形參和實參,位置引數,關鍵字引數,預設引數,*args,**kwargs
6 高階函式(函式物件)
7 函式巢狀
8 作用域與名稱空間
9 裝飾器
10 迭代器與生成器及協程函式
11 三元運算,列表解析、生成器表示式
12 函式的遞迴呼叫
13 內建函式
14 面向過程程式設計與函數語言程式設計

二 函式基礎

引子

一 為何要用函式之不用函式的問題

1、程式碼的組織結構不清晰,可讀性差
2、遇到重複的功能只能重複編寫實現程式碼,程式碼冗餘
3、功能需要擴充套件時,需要找出所有實現該功能的地方修改之,無法統一管理且維護難度極大 

二 函式是什麼

針對二中的問題,想象生活中的例子,修理工需要實現準備好工具箱裡面放好錘子,扳手,鉗子等工具,然後遇到錘釘子的場景,拿來錘子用就可以,而無需臨時再製造一把錘子。

修理工===>程式設計師
具備某一功能的工具===>函式

要想使用工具,需要事先準備好,然後拿來就用且可以重複使用
要想用函式,需要先定義,再使用

三 函式分類

#1、內建函式
為了方便我們的開發,針對一些簡單的功能,python直譯器已經為我們定義好了的
函式即內建函式。對於內建函式,我們可以拿來就用而無需事先定義,如len(),
sum(),max()
ps:我們將會在最後詳細介紹常用的內建函式。
#2、自定義函式
很明顯內建函式所能提供的功能是有限的,這就需要我們自己根據需求,事先定製
好我們自己的函式來實現某種功能,以後,在遇到應用場景時,呼叫自定義的函式
即可。例如

二 定義函式

一 如何自定義函式?

#語法
def 函式名(引數1,引數2,引數3,...):
    '''註釋'''
    函式體
    return
返回的值 #函式名要能反映其意義
def auth(user:str,password:str)->int:
    '''
    auth function
    :param user: 使用者名稱
    :param password: 密碼
    :return: 認證結果
    '''
    if user == 'egon' and password == '123':
        return 1
# print(auth.__annotations__) #{'user': <class 'str'>, 'password': <class 'str'>, 'return': <class 'int'>}

user=input('使用者名稱>>: ').strip()
pwd=input('密碼>>: ').strip()
res=auth(user,pwd)
print(res)

二 函式使用的原則:先定義,再呼叫

函式即“變數”,“變數”必須先定義後引用。未定義而直接引用函式,就相當於在引用一個不存在的變數名
#測試一
def foo():
    print('from foo')
    bar()
foo() #報錯

#測試二
def bar():
    print('from bar')
def foo():
    print('from foo')
    bar()
foo() #正常

#測試三
def foo():
    print('from foo')
    bar()

def bar():
    print('from bar')
foo() #會報錯嗎?


#結論:函式的使用,必須遵循原則:先定義,後呼叫
#我們在使用函式時,一定要明確地區分定義階段和呼叫階段

#定義階段
def foo():
    print('from foo')
    bar()
def bar():
    print('from bar')
#呼叫階段
foo()

三 函式在定義階段都幹了哪些事?

#只檢測語法,不執行程式碼
也就說,語法錯誤在函式定義階段就會檢測出來,而程式碼的邏輯錯誤只有在執行時
才會知道

四 定義函式的三種形式

#1、無參:應用場景僅僅只是執行一些操作,比如與使用者互動,列印
#2、有參:需要根據外部傳進來的引數,才能執行相應的邏輯,比如統計長度,求最
    大值最小值
#3、空函式:設計程式碼結構

無參、有參

#定義階段
def tell_tag(tag,n): #有引數
    print(tag*n)

def tell_msg(): #無引數
    print('hello world')

#呼叫階段
tell_tag('*',12)
tell_msg()
tell_tag('*',12)

'''
************
hello world
************
'''

#結論:
#1、定義時無參,意味著呼叫時也無需傳入引數
#2、定義時有參,意味著呼叫時則必須傳入引數

空函式

def auth(user,password):                             
    '''                                                           
    auth function                                                 
    :param user: 使用者名稱                                              
    :param password: 密碼                                           
    :return: 認證結果                                                 
    '''                                                           
    pass                                                          

def get(filename):                                                
    '''                                                           
    :param filename:                                              
    :return:                                                      
    '''                                                           
    pass                                                          

def put(filename):                                                
    '''                                                           
    :param filename:                                              
    :return:                                                      
    '''                                                           
def ls(dirname):                                                  
    '''                                                           
    :param dirname:                                               
    :return:                                                      
    '''                                                           
    pass                                                          

#程式的體系結構立見 

三 呼叫函式

一 呼叫函式

函式的呼叫:函式名加括號
1 先找到名字
2 根據名字呼叫程式碼

二 函式返回值

return->None
return 1個值->返回1個值
return 逗號分隔多個值->元組
什麼時候該有返回值?
    呼叫函式,經過一系列的操作,最後要拿到一個明確的結果,則必須要有返回值
    通常有參函式需要有返回值,輸入引數,經過計算,得到一個最終的結果
什麼時候不需要有返回值?
    呼叫函式,僅僅只是執行一系列的操作,最後不需要得到什麼結果,則無需有返回值
    通常無參函式不需要有返回值

三 函式呼叫的三種形式

1 語句形式:foo()
2 表示式形式:3*len('hello')
3 當中另外一個函式的引數:range(len('hello'))

四 函式的引數

一 形參與實參

#形參即變數名,實參即變數值,函式呼叫時,將值繫結到變數名上,函式呼叫結束,
解除繫結

二 具體應用

乃重點知識!!!

#1、位置引數:按照從左到右的順序定義的引數
        位置形參:必選引數
        位置實參:按照位置給形參傳值

#2、關鍵字引數:按照key=value的形式定義的實參
        無需按照位置為形參傳值
        注意的問題:
                1. 關鍵字實參必須在位置實參右面
                2. 對同一個形參不能重複傳值

#3、預設引數:形參在定義時就已經為其賦值
        可以傳值也可以不傳值,經常需要變得引數定義成位置形參,變化較小的引數定義成預設引數(形參)
        注意的問題:
                1. 只在定義時賦值一次
                2. 預設引數的定義應該在位置形參右面
                3. 預設引數通常應該定義成不可變型別


#4、可變長引數:
        可變長指的是實參值的個數不固定
        而實參有按位置和按關鍵字兩種形式定義,針對這兩種形式的可變長,形參對應有兩種解決方案來完整地存放它們,分別是*args,**kwargs

        ===========*args===========
        def foo(x,y,*args):
            print(x,y)
            print(args)
        foo(1,2,3,4,5)

        def foo(x,y,*args):
            print(x,y)
            print(args)
        foo(1,2,*[3,4,5])


        def foo(x,y,z):
            print(x,y,z)
        foo(*[1,2,3])

        ===========**kwargs===========
        def foo(x,y,**kwargs):
            print(x,y)
            print(kwargs)
        foo(1,y=2,a=1,b=2,c=3)

        def foo(x,y,**kwargs):
            print(x,y)
            print(kwargs)
        foo(1,y=2,**{'a':1,'b':2,'c':3})


        def foo(x,y,z):
            print(x,y,z)
        foo(**{'z':1,'x':2,'y':3})

        ===========*args+**kwargs===========

        def foo(x,y):
            print(x,y)

        def wrapper(*args,**kwargs):
            print('====>')
            foo(*args,**kwargs)

#5、命名關鍵字引數:*後定義的引數,必須被傳值(有預設值的除外),且必須按照關鍵字實參的形式傳遞
可以保證,傳入的引數中一定包含某些關鍵字
        def foo(x,y,*args,a=1,b,**kwargs):
            print(x,y)
            print(args)
            print(a)
            print(b)
            print(kwargs)

        foo(1,2,3,4,5,b=3,c=4,d=5)
        結果:
            2
            (3, 4, 5)
            3
            {'c': 4, 'd': 5}

此乃重點知識!!!

三 函式物件、函式巢狀、名稱空間與作用域、裝飾器

一 函式物件

一 函式是第一類物件,即函式可以當作資料傳遞

#1 可以被引用
#2 可以當作引數傳遞
#3 返回值可以是函式
#3 可以當作容器型別的元素

二 利用該特性,優雅的取代多分支的if

def foo():
    print('foo')

def bar():
    print('bar')

dic={
    'foo':foo,
    'bar':bar,
}
while True:
    choice=input('>>: ').strip()
    if choice in dic:
        dic[choice]()

二 函式巢狀

一 函式的巢狀呼叫

def max(x,y):
    return x if x > y else y

def max4(a,b,c,d):
    res1=max(a,b)
    res2=max(res1,c)
    res3=max(res2,d)
    return res3
print(max4(1,2,3,4))

二 函式的巢狀定義

def f1():
    def f2():
        def f3():
            print('from f3')
        f3()
    f2()

f1()
f3() #報錯,為何?請看下一小節

三 名稱空間與作用域

一 什麼是名稱空間?

#名稱空間:存放名字的地方,三種名稱空間,(之前遺留的問題x=1,1存放於記憶體
中,那名字x存放在哪裡呢?名稱空間正是存放名字x與1繫結關係的地方)

二 名稱空間的載入順序

python test.py
#1、python直譯器先啟動,因而首先載入的是:內建名稱空間
#2、執行test.py檔案,然後以檔案為基礎,載入全域性名稱空間
#3、在執行檔案的過程中如果呼叫函式,則臨時產生區域性名稱空間

三 名字的查詢順序

區域性名稱空間--->全域性名稱空間--->內建名稱空間

#需要注意的是:在全域性無法檢視區域性的,在區域性可以檢視全域性的,如下示例

# max=1
def f1():
    # max=2
    def f2():
        # max=3
        print(max)
    f2()
f1()
print(max)

四 作用域

#1、作用域即範圍
        - 全域性範圍(內建名稱空間與全域性名稱空間屬於該範圍):全域性存活,全域性有效
      - 區域性範圍(區域性名稱空間屬於該範圍):臨時存活,區域性有效
#2、作用域關係是在函式定義階段就已經固定的,與函式的呼叫位置無關,如下
x=1
def f1():
    def f2():
        print(x)
    return f2
x=100
def f3(func):
    x=2
    func()
x=10000
f3(f1())

#3、檢視作用域:globals(),locals()


LEGB 代表名字查詢順序: locals -> enclosing function -> globals -> __builtins__
locals 是函式內的名字空間,包括區域性變數和形參
enclosing 外部巢狀函式的名字空間(閉包中常見)
globals 全域性變數,函式定義所在模組的名字空間
builtins 內建模組的名字空間

五 global與nonlocal關鍵字

四 閉包函式

一 什麼是閉包?

#內部函式包含對外部作用域而非全域性作用域的引用

#提示:之前我們都是通過引數將外部的值傳給函式,閉包提供了另外一種思路,包起來嘍,包起呦,包起來哇

        def counter():
            n=0
            def incr():
                nonlocal n
                x=n
                n+=1
                return x
            return incr

        c=counter()
        print(c())
        print(c())
        print(c())
        print(c.__closure__[0].cell_contents) #檢視閉包的元素

二 閉包的意義與應用

#閉包的意義:返回的函式物件,不僅僅是一個函式物件,在該函式外還包裹了一層作用域,這使得,該函式無論在何處呼叫,優先使用自己外層包裹的作用域
#應用領域:延遲計算(原來我們是傳參,現在我們是包起來)
    from urllib.request import urlopen

    def index(url):
        def get():
            return urlopen(url).read()
        return get

    baidu=index('http://www.baidu.com')
    print(baidu().decode('utf-8'))

五 裝飾器

裝飾器就是閉包函式的一種應用場景

一 為何要用裝飾器

#開放封閉原則:對修改封閉,對擴充套件開放

二 什麼是裝飾器

裝飾器他人的器具,本身可以是任意可呼叫物件,被裝飾者也可以是任意可呼叫物件。
強調裝飾器的原則:1 不修改被裝飾物件的原始碼 2 不修改被裝飾物件的呼叫方式
裝飾器的目標:在遵循1和2的前提下,為被裝飾物件新增上新功能

三 裝飾器的使用

無參裝飾器

import time
def timmer(func):
    def wrapper(*args,**kwargs):
        start_time=time.time()
        res=func(*args,**kwargs)
        stop_time=time.time()
        print('run time is %s' %(stop_time-start_time))
        return res
    return wrapper

@timmer
def foo():
    time.sleep(3)
    print('from foo')
foo()

無參裝飾器

有參裝飾器

def auth(driver='file'):
    def auth2(func):
        def wrapper(*args,**kwargs):
            name=input("user: ")
            pwd=input("pwd: ")

            if driver == 'file':
                if name == 'egon' and pwd == '123':
                    print('login successful')
                    res=func(*args,**kwargs)
                    return res
            elif driver == 'ldap':
                print('ldap')
        return wrapper
    return auth2

@auth(driver='file')
def foo(name):
    print(name)

foo('egon')

有參裝飾器

四 裝飾器語法

被裝飾函式的正上方,單獨一行
        @deco1
        @deco2
        @deco3
        def foo():
            pass

        foo=deco1(deco2(deco3(foo)))

五 裝飾器補充:wraps

from functools import wraps

def deco(func):
    @wraps(func) #加在最內層函式正上方
    def wrapper(*args,**kwargs):
        return func(*args,**kwargs)
    return wrapper

@deco
def index():
    '''哈哈哈哈'''
    print('from index')

print(index.__doc__)

四 迭代器、生成器、面向過程程式設計

一 迭代器

一 迭代的概念

#迭代器即迭代的工具,那什麼是迭代呢?
#迭代是一個重複的過程,每次重複即一次迭代,並且每次迭代的結果都是下一次迭代的初始值
while True: #只是單純地重複,因而不是迭代
    print('===>') 

l=[1,2,3]
count=0
while count < len(l): #迭代
    print(l[count])
    count+=1

二 為何要有迭代器?什麼是可迭代物件?什麼是迭代器物件?

#1、為何要有迭代器?
對於序列型別:字串、列表、元組,我們可以使用索引的方式迭代取出其包含的元素。但對於字典、集合、檔案等型別是沒有索引的,若還想取出其內部包含的元素,則必須找出一種不依賴於索引的迭代方式,這就是迭代器

#2、什麼是可迭代物件?
可迭代物件指的是內建有__iter__方法的物件,即obj.__iter__,如下
'hello'.__iter__
(1,2,3).__iter__
[1,2,3].__iter__
{'a':1}.__iter__
{'a','b'}.__iter__
open('a.txt').__iter__

#3、什麼是迭代器物件?
可迭代物件執行obj.__iter__()得到的結果就是迭代器物件
而迭代器物件指的是即內建有__iter__又內建有__next__方法的物件

檔案型別是迭代器物件
open('a.txt').__iter__()
open('a.txt').__next__()


#4、注意:
迭代器物件一定是可迭代物件,而可迭代物件不一定是迭代器物件

三 迭代器物件的使用

dic={'a':1,'b':2,'c':3}
iter_dic=dic.__iter__() #得到迭代器物件,迭代器物件即有__iter__又有__next__,但是:迭代器.__iter__()得到的仍然是迭代器本身
iter_dic.__iter__() is iter_dic #True

print(iter_dic.__next__()) #等同於next(iter_dic)
print(iter_dic.__next__()) #等同於next(iter_dic)
print(iter_dic.__next__()) #等同於next(iter_dic)
# print(iter_dic.__next__()) #丟擲異常StopIteration,或者說結束標誌

#有了迭代器,我們就可以不依賴索引迭代取值了
iter_dic=dic.__iter__()
while 1:
    try:
        k=next(iter_dic)
        print(dic[k])
    except StopIteration:
        break

#這麼寫太醜陋了,需要我們自己捕捉異常,控制next,python這麼牛逼,能不能幫我解決呢?能,請看for迴圈

四 for迴圈

#基於for迴圈,我們可以完全不再依賴索引去取值了
dic={'a':1,'b':2,'c':3}
for k in dic:
    print(dic[k])

#for迴圈的工作原理
#1:執行in後物件的dic.__iter__()方法,得到一個迭代器物件iter_dic
#2: 執行next(iter_dic),將得到的值賦值給k,然後執行迴圈體程式碼
#3: 重複過程2,直到捕捉到異常StopIteration,結束迴圈

五 迭代器的優缺點

#優點:
  - 提供一種統一的、不依賴於索引的迭代方式
  - 惰性計算,節省記憶體
#缺點:
  - 無法獲取長度(只有在next完畢才知道到底有幾個值)
  - 一次性的,只能往後走,不能往前退

二 生成器

一 什麼是生成器

#只要函式內部包含有yield關鍵字,那麼函式名()的到的結果就是生成器,並且不會執行函式內部程式碼

def func():
    print('====>first')
    yield 1
    print('====>second')
    yield 2
    print('====>third')
    yield 3
    print('====>end')

g=func()
print(g) #<generator object func at 0x0000000002184360>

二 生成器就是迭代器

g.__iter__
g.__next__
#2、所以生成器就是迭代器,因此可以這麼取值
res=next(g)
print(res)

三 協程函式

#yield關鍵字的另外一種使用形式:表示式形式的yield
def eater(name):
    print('%s 準備開始吃飯啦' %name)
    food_list=[]
    while True:
        food=yield food_list
        print('%s 吃了 %s' % (name,food))
        food_list.append(food)

g=eater('egon')
g.send(None) #對於表示式形式的yield,在使用時,第一次必須傳None,g.send(None)等同於next(g)
g.send('蒸羊羔')
g.send('蒸鹿茸')
g.send('蒸熊掌')
g.send('燒素鴨')
g.close()
g.send('燒素鵝')
g.send('燒鹿尾')

三 面向過程程式設計

#1、首先強調:面向過程程式設計絕對不是用函式程式設計這麼簡單,面向過程是一種程式設計思路、思想,而程式設計思路是不依賴於具體的語言或語法的。言外之意是即使我們不依賴於函式,也可以基於面向過程的思想編寫程式

#2、定義
面向過程的核心是過程二字,過程指的是解決問題的步驟,即先幹什麼再幹什麼

基於面向過程設計程式就好比在設計一條流水線,是一種機械式的思維方式

#3、優點:複雜的問題流程化,進而簡單化

#4、缺點:可擴充套件性差,修改流水線的任意一個階段,都會牽一髮而動全身

#5、應用:擴充套件性要求不高的場景,典型案例如linux核心,git,httpd

#6、舉例
流水線1:
使用者輸入使用者名稱、密碼--->使用者驗證--->歡迎介面

流水線2:
使用者輸入sql--->sql解析--->執行功能