零基礎小白的Python學習之路(五)函數

2020-08-08 15:53:34

函數

回顧我們曾經在C語言中學過的函數,在程式設計過程中用到的次數真的是太多了,Python當中也是一樣,它是可重用的程式程式碼塊,且具有一致性,修改函數程式碼,則所有呼叫該函數的地方都能夠得到體現

基本概念

  • 一個程式由一個個任務組成;函數就是代表一個任務或者一個功能
  • 函數是程式碼複用的通用機制 機製

Python函數的分類

  1. 內建函數
     之前所使用的str()、list()、join()等函數,可以直接拿來使用的
  2. 標準庫函數
     通過import語句進行匯入庫,使用其中定義的函數(如turtle海歸繪圖用的函數)
  3. 第三方庫函數
     Python社羣提供的高品質的庫,需要下載安裝再import匯入使用
  4. 使用者自定義函數
     使用者自己去定義的函數

函數的定義和呼叫

基本格式:

def 函數名 ([參數列表]):
 ‘"文件字串’’’
 函數體 / 若幹語句

【注】

  1. 參數列表部分可選,可爲空
    -①圓括號內是形式參數列表,有多個參數則使用逗號隔開
    -②形參不需要宣告型別,也不需要指定函數返回值型別
    -③無參數,也必須保留空的圓括號
    -④實參列表必須與形參列表一一對應
  2. 用三個單引號括起來的文件字串可以作爲對函數功能等的解釋說明(相當於註釋)
  3. Python在執行def時,會建立一個函數物件,並系結到函數名變數上
  4. return返回值
    -①如果函數體中包含return語句,則結束函數執行並返回值
    -②如果函數體中不包含return語句,則返回None值
  5. 呼叫函數之前,必須要先定義函數,即先呼叫def建立函數物件
    -①內建函數物件會自動建立
    -②標準庫和第三方庫函數,通過import匯入模組時,會執行模組中的def語句
# 測試函數
def is_prime(n):
    flag = 1
    for i in range(2,n):
        if n % i == 0:
            flag = 0
            break
    if flag == 1:
        return True
    else:
        return False

num = int(input("Input a number:"))
if is_prime(num):
    print("{0}是素數".format(num))
else:
    print("{0}不是素數".format(num))

# 測試函數物件
print(id(is_prime))
print(type(is_prime))
print(is_prime)

在这里插入图片描述

形參和實參

# 測試函數
def printMax(a,b): # 形參(定義)
    if a > b:
        print(a,"較大值")
    else:
        print(b,"較大值")

printMax(10,20) # 實參(呼叫)
printMax(25,2)

在这里插入图片描述

文件字串(函數的註釋)

增強程式的可讀性,三個單引號 / 三個雙引號

# 測試函數
def printMax(a,b): # 形參(定義)
    '''輸入兩個數,判斷哪個值較大並列印較大值''' # 文件字串
    if a > b:
        print(a,"較大值")
    else:
        print(b,"較大值")

printMax(10,20) # 實參(呼叫)
printMax(25,2)

還可以通過help(函數名._ _ doc_ _)可以列印輸出函數的文件字串

help(printMax.__doc__)

在这里插入图片描述

返回值

return返回值要點:

  1. 如果函數體中包含return語句,則結束函數執行返回值
  2. 如果函數體中不包含return語句,則返回None值
  3. 要返回多個返回值,使用列表、元組、字典、集合將多個值「存起來」即可

【例】

# 測試返回值
# 有參 有返回值
def add(a,b):
    print("求兩數和:{0},{1},{2}".format(a,b,(a+b)))
    return (a+b)**2

# 無參 無返回值
def pri():
    print("Hello")
    print("Stefan")
    return # return兩個作用:1.返回具體值 2.結束函數執行
    print("Yes") # return之後不會執行

# 返回多個值
def calc(x,y,z):
    return [x*3,y*4,z*5] # 返回一個列表

s = add(7,9)
print("兩數和的平方:",s)
print(add(10,20)*3)

a = pri()
print(a) # 無返回值 輸出None

print(calc(5,7,9))

在这里插入图片描述

函數也是物件(記憶體底層分析)

Python中,一切即爲物件,在執行def所定義的函數之後,系統就建立了相應的函數物件,以後呼叫時就直接呼叫已建立好的那個函數物件,而不需要再建立一個
【例】

def func01():
    print("sssddd")

func01()
a = func01
a()

print(id(func01))
print(id(a))

在这里插入图片描述
內層分析示意圖:
在这里插入图片描述

變數的作用域

變數的作用域,即變數的作用範圍

  • 全域性變數
  • 區域性變數

全域性變數

  1. 函數和類定義之外宣告的變數。作用域:從定義位置開始直到定義模組結束
  2. 全域性變數降低了函數的通用性和可讀性(儘量避免使用全域性變數)
  3. 全域性變數一般做常數使用
  4. 函數內要改變全域性變數的值,使用global關鍵字宣告一下

區域性變數

  1. 在函數體中(包含形參)宣告的變數
  2. 區域性變數的參照比全域性要快,優先考慮使用
  3. 如果區域性變數和全域性變數同名,則在函數內隱藏全域性變數,只使用同名的區域性變數

【例】

#全域性變數 區域性變數
a = 3 # 全域性變數

def func01():
    b = 4 # 區域性變數
    print(b*3)

func01()
print(a)
print(b) # 報錯

在这里插入图片描述
記憶體分析:
在这里插入图片描述
【如果想在函數內部改變全域性變數值,用global】

#全域性變數 區域性變數
a = 3 # 全域性變數

def func01():
    b = 4 # 區域性變數
    print(b*3)
    global a # 函數內要改變全域性變數的值,使用global關鍵字宣告
    a = 300

func01()
print(a)

在这里插入图片描述
【輸出區域性變數和全域性變數】

#全域性變數 區域性變數
a = 100
c = 6
def fun1(): # 函數名fun1也是全域性
    b = 5
    global a
    a = 300
    print(locals()) # 列印輸出的區域性變數
    print(globals()) # 列印輸出的全域性變數

fun1()
print(a)

{‘b’: 5}
{‘name’: ‘main’, ‘doc’: None, ‘package’: None, ‘loader’: <_frozen_importlib_external.SourceFileLoader object at 0x0330AF40>, ‘spec’: None, ‘annotations’: {}, ‘builtins’: <module ‘builtins’ (built-in)>, ‘file’: ‘C:/Users/Administrator/PycharmProjects/mypro01/test_if.py’, ‘cached’: None, ‘a’: 300, ‘c’: 6, ‘fun1’: <function fun1 at 0x033E7FA0>}
300

區域性變數和全域性變數效率測試

這裏又是一處可以進行優化的地方,尤其在回圈的時候,在特別強調效率的地方或者回圈次數較多的地方,可以通過將全域性變數轉爲區域性變數從而提高執行速度

# 測試區域性、全域性效率
import math
import time
def test01():
    start1 = time.time()
    for i in range(10000000):
        math.sqrt(30)
    end1 = time.time()
    print("耗時:{0}".format(end1 - start1))

def test02():
    b = math.sqrt
    start2 = time.time()
    for i in range(10000000):
        b(30)
    end2 = time.time()
    print("耗時:{0}".format(end2 - start2))

test01()
test02()

在这里插入图片描述

參數的傳遞

本質:實參——>形參
由於Python中一切都是物件,所以在Python中的參數傳遞都是「參照傳遞」(地址),而不是「值傳遞」

傳遞可變物件的參照

Python中,可變物件有:
 字典、列表、集合、自定義的物件等

對「可變物件」進行「寫操作」,直接作用於原物件本身

傳遞參數是可變物件時,實際傳遞的還是物件的參照。
在函數體中不建立新的物件拷貝,而是可以直接修改所傳遞的物件。

b = [10,20] # 可變物件
def f1(m):
    print("m:",id(m))
    m.append(30) # m = b 地址相同

f1(b)
print("b:",id(b))
print(b)

在这里插入图片描述
記憶體分析:
在这里插入图片描述

傳遞不可變物件的參照

Python中,不可變物件有:
 數位(int、float)、字串、元組、布爾值、function等

對「不可變物件」進行「寫操作」,會產生一個新的「物件空間」,並用新的值填充這塊空間(注意,只是起到其他語言「值傳遞」的效果,但本質上不是「值傳遞」)

傳遞參數是不可變物件時,實際傳遞的還是物件的參照
由於不可變物件無法修改,那麼系統會新建立一個物件用於「賦值操作」,沒有拷貝,依舊是參照。

a = 100
def f2(n):
    print("n:",id(n))
    n = n + 200
    print("n:",id(n))
    print(n)

f2(a)
print("a:",id(a))

在这里插入图片描述
記憶體分析:
在这里插入图片描述

淺拷貝和深拷貝

  • 淺拷貝:不拷貝子物件的內容,只是拷貝子物件的參照
  • 深拷貝:會連子物件的記憶體也全部拷貝一份,對子物件的修改不會影響源物件

使用內建函數:copy(淺拷貝)、deepcopy(深拷貝)

淺拷貝與深拷貝區別示意圖:
在这里插入图片描述
【測試淺拷貝】

# 測試 淺拷貝
import copy
a = [10,20,[5,6]]
b = copy.copy(a)

print("a:",a)
print("b:",b)

b.append(30)
b[2].append(7)

print("----淺拷貝----")
print("a:",a)
print("b:",b)

在这里插入图片描述
【淺拷貝具體分析】
在这里插入图片描述
【測試深拷貝】

# 測試 深拷貝
import copy
a = [10,20,[5,6]]
b = copy.deepcopy(a)

print("a:",a)
print("b:",b)

b.append(30)
b[2].append(7)

print("----深拷貝----")
print("a:",a)
print("b:",b)

在这里插入图片描述
【深拷貝具體分析】
在这里插入图片描述

傳遞不可變物件是淺拷貝

【注】
傳遞參數是不可變物件時(如int、float、字串、元組、布爾值),實際傳遞的還是物件的參照,但在「寫操作」時,會建立一個新的物件拷貝,要注意的是,這個拷貝使用的是「淺拷貝」,而不是「深拷貝」。

【測試】

a = (10,20,[5,6])
print("a:",id(a))

def f1(m):
    print("m:",id(m))
    m[2][0] = 888
    print("m:",m)
    print("m:",id(m))

f1(a)
print("a",a)

在这里插入图片描述
在这里插入图片描述

參數的幾種型別

位置參數

函數呼叫時,實參預設按位元置順序傳遞,需要個數和形參匹配
按位元置傳遞的參數,稱爲:位置參數

預設值參數

我們可以爲某些參數設定預設值,這樣這些參數在傳遞時就是可選的,稱爲「預設值參數」。
預設值參數放在位置參數後面

【例】

def func1(a,b,c = 10,d = 20):
    print(a,b,c,d)

func1(8,9)
func1(8,9,19)
func1(8,9,19,29)

在这里插入图片描述

命名參數(關鍵字參數)

在呼叫函數時,實參除了通過位置一一對應以外,還可以按照形參的名稱去傳遞參數
【例】

def func1(a,b,c):
    print(a,b,c)

func1(8,9,19)
func1(c = 8,a = 9,b = 19)

在这里插入图片描述

可變參數

可變參數:可變數量的參數

  1. *param(一個星號),將多個參數收集到一個「元組」物件中
  2. **param(兩個星號),將多個參數收集到一個"字典"物件中

【例】

def func1(a,b,*c): # c是一個元組
    print(a,b,c)

func1(8,9,19,20,45,6)

def func2(a,b,**c): # c是一個字典
    print(a,b,c)

func2(4,8,name = 'Stefan',age = 24)

def func3(a,*b,**c):
    print(a,b,c)

func3(2,4,6,7,9,name = 'Kitty',age = 22,address = 'New York')

在这里插入图片描述

強制命名參數

要想在帶星號的「可變參數」後面增加新的參數,必須是「強制命名參數

【測試】

def func1(*a,b,c):
    print(a,b,c)

func1(2,3,4) # 報錯

在这里插入图片描述
【必須強制命名】

def func1(*a,b,c):
    print(a,b,c)

func1(2,7,b = 3,c = 4)

在这里插入图片描述

lambda表達式和匿名函數

lambda表達式可以用來宣告匿名函數
lambda表達式是一種簡單的、在同一行中定義函數的方法,它實際上是生成了一個函數物件
【注】lambda表達式只允許包含一個表達式,不能包含複雜語句,該表達式的運算結果就是函數的返回值

基本格式:
lambda arg1,arg2,arg3…: <表達式>

【注】

  • arg1 / arg2 / arg3 爲函數的參數
  • <表達式> 相當於函數體
  • 運算結果:表達式的運算結果
# lambda測試
f = lambda a,b,c : a + b + c
print(f)
print(f(2,3,4))

a = [lambda x:x ** 2,lambda y:y // 5,lambda z:z + 4]
print(a[2](3),a[1](10),a[0](7))

在这里插入图片描述
【函數也是物件】

# lambda測試
f = lambda a,b,c : a + b + c
print(f)
print(f(2,3,4))

a = [lambda x:x ** 2,lambda y:y // 5,lambda z:z + 4]
print(a[2](3),a[1](10),a[0](7))

g = [f,a] # 函數也是物件
print(g[0](1,6,8))
print(g[1][0](4),g[1][1](25),g[1][2](9))

在这里插入图片描述

eval()函數

eval()函數,將字串str當成有效的表達式來求值並返回計算結果
基本語法:

eval(source [,globals [,locals]]) -> value

  • source:一個Python表達式 / 函數compile()返回的程式碼物件
  • globals:可選,必須是字典
  • locals:可選,任意對映物件

通俗來講,就是可以將檔案、以及別的地方的字串(這個字串是一段可執行程式碼)傳過來,傳到一個變數中,通過對這個變數存取來執行這段程式碼字串,當成有效表達式來執行。

# eval()函數測試
s = "print('Hello yaya')"
eval(s)

a = 10
b = 20
c = eval("a+b")
print(c)

dict1 = dict(a = 100,b = 200)
d = eval("a+b")
e = eval("a+b",dict1)
print(d)
print(e)

在这里插入图片描述

遞回函數

自己呼叫自己
在函數體內部直接或間接得自己呼叫自己
每個遞回函數必須包含兩個部分:

  1. 遞回終止條件:表示遞回什麼時候結束,一般用於返回值,不再呼叫自己
  2. 遞回步驟:把第n步的值和第n-1步相關聯

【例1】

# 遞回測試
def f(n):
    print("f:",n)
    if n == 0:
        print("over")
    else:
        f(n-1)
    print("f**:",n)

f(4)

在这里插入图片描述
【函數呼叫記憶體分析】
在这里插入图片描述
【例2】求階乘

# 遞回求階乘
def fac(n):
    if n == 1 or n == 0:
        return 1
    else:
        return n * fac(n-1)

for i in range(8):
    print("{0}!= {1}".format(i,fac(i)))

在这里插入图片描述

巢狀函數(內部函數)

巢狀函數:
 在函數內部定義的函數

# 巢狀函數
def outer():
    print("outer running!")
    def inner01(): # 僅限內部呼叫
        print("inner01 running!")
    inner01()

outer()

在这里插入图片描述
【注】一般在什麼情況下需要使用巢狀函數呢?

  1. 封裝(數據隱藏)
     外部無法存取內部的「巢狀函數」
  2. 貫徹DRY(Don’t Repeat Yourself)原則
     函數內部避免重複程式碼
  3. 閉包

nonlocal 和 global關鍵字

  • nonlocal:用來宣告外層的區域性變數
  • global:用來宣告全域性變數

【測試】

def outer():
    b = 10

    def inner():
        print("inner:",b)
        b = 20 # 無法修改 報錯
    inner()

outer()

在这里插入图片描述

def outer():
    b = 10

    def inner():
        nonlocal b # 宣告外部函數的區域性變數
        print("inner:",b)
        b = 20
    inner()
    print("outer b:",b)

outer()

在这里插入图片描述

a = 100
def outer():
    b = 10

    def inner():
        nonlocal b # 宣告外部函數的區域性變數
        print("inner:",b)
        b = 20
        global a
        a = 1000
    inner()
    print("outer b:",b)

outer()
print("a:",a)

在这里插入图片描述

LEGB規則

什麼是LEGB
Python在查詢「名稱」時,是按照LEGB規則查詢的:
Local–> Enclosed–> Global–> Built in

  • Local:函數或類的方法內部
  • Enclosed:巢狀函數(一個函數包裹另一個函數,閉包)
  • Global:模組中的全域性變數
  • Built in:Python爲自己保留的特殊名稱(內建)

如果某個name對映在區域性(local)名稱空間中沒有找到,接下來就會在閉包作用域(enclosed)進行搜尋,如果閉包作用域也沒有找到,Python就會到全域性(global)名稱空間中進行查詢,最後會在內建(built-in)名稱空間搜尋(如果一個名稱在所有名稱空間中都沒有找到,就會產生一個NameError)。

【測試】

# 測試LEGB
print(type(str))
# str = "global"
def outer():
    # str = "outer"
    def inner():
        # str = "inner"
        print(str)

    inner()

outer()

在这里插入图片描述
【注】這裏爲什麼不會報錯是因爲,Python中還有自動建立的內建函數str()在發揮作用