python幕後的不爲人知(一)

python英文官方文檔:https://docs.python.org/3.8/tutorial/index.html
比較不錯的python中文文檔:https://www.runoob.com/python3/python3-tutorial.html

1. 寫在前面

疫情在家的這段時間,突然想補一些基礎知識了, 感覺有些基礎(python, numpy, pandas, sklearn等)需要系統的進行補補, 首先就是先從python開始,雖然現在寫各種機器學習代碼離不開python,但是python我純粹是半路出家, 憑着之前一些C和Java的基礎直接就上手,還沒有真正系統的學習過python,所以導致現在看別人寫的代碼時,有些編程的騷操作很是羨慕,但換成自己硬是寫不出來,所以我決定藉着宅在家裏的這段時間也進行基礎性的學習, 畢竟python代碼以簡潔,優雅著稱, 所以寫代碼的時候儘量還是簡潔,優雅,通俗易懂, 這篇文章是記錄在自己學習python基礎的過程中遇到的一些之前沒有注意到的點或者還沒有掌握的點, 至於這次學習python,我是直接跟着廖雪峯老師的python教程進行學習的, 沒有跟着視頻學習,一是視頻太浪費時間,二是感覺對於學習python本身,沒有這個必要, 不如邊看文檔邊擼代碼來的愉快。 當然也有一本《python學習手冊》在旁輔助,因爲是第一遍過基礎部分的,儘量過得細一些,當然後期遇到關於python不會的知識,同樣會整理到這裏,學習的過程也是一個積累的過程 😉

如果你已經精通python, 這篇文章就沒有看的必要了, 如果你也和我一樣是半路出家, 那麼可以簡單的瞅兩眼,不會花太多時間, 這裏面肯定會有你知道的知識, 但或許也有你之前沒有注意到的細節, 這樣才更有意思。

今天的整理分爲兩塊: python的基礎知識和函數。首先是基礎知識中的細節,比如字符串前面的小r知道代表什麼含義嗎?動態語言和靜態語言, 然後會梳理一些編碼的知識,像ASCII,GB2312,Utf-8,Unicode這些演變和關聯。 然後是python的核心數據類型的可變與不可變對象及各自的一些細節, 最後就是函數部分的各種參數,包括默認參數,可變參數*,關鍵字參數**, 命名關鍵字參數及函數的遞歸和尾遞歸等。

大綱如下:

  • Python基礎知識開始(數據類型,變量, 語句)
  • Python函數(函數的參數, 遞歸函數)
  • 最後總結串一遍

Ok, let’s go!

2. Python基礎知識

2.1 整數類型和變量

  1. python字符串前面加上’r’的作用
    這種情況其實是這樣子的, 比如下面這兩行代碼:

    s1 = 'hello, \'Adam\''
    s2 = r'hello, \'Adam\''
    
    print(s1)      # hello, 'Adam'
    print(s2)     # hello, \'Adam\'
    

    ‘r’是防止字符轉義的 如果路徑中出現’\t’的話 不加r的話\t就會被轉義 而加了’r’之後’\t’就能保留原有的樣子。

    在Python的string前面加上‘r’, 是爲了告訴編譯器這個string是個raw string,不要轉意backslash ‘’ 。 例如,\n 在raw string中,是兩個字符,\和n, 而不會轉意爲換行符。由於正則表達式和 \ 會有衝突,因此,當一個字符串使用了正則表達式後,最好在前面加上’r’。

  2. 動態語言 VS 靜態語言
    python中的變量往往是採用動態語言的方式,所謂動態語言, 就是說變量本身類型不固定, 我們看一下python中的變量:

    a = 123   # a是整數
    a = 'ABC'  # a變成字符串
    

    與動態語言對應的是靜態語言。靜態語言在定義變量時必須指定變量類型,如果賦值的時候類型不匹配,就會報錯。例如Java是靜態語言,賦值語句如下

    int a = 123; // a是整數類型變量
    a = "ABC"; // 錯誤:不能把字符串賦給整型變量
    
  3. 變量在進行賦值的時候,python解釋器到底在幹什麼?
    我們看下面的代碼, 捋一捋計算機內部的邏輯:

    a = 'ABC'
    b = a
    a = 'XYZ'
    print(b)    # ABC
    

    我們可能一下子就知道結果, 但是你知道解釋器到底在幹什麼事情嗎? 也就是爲什麼會有這樣的結果? 這個我直接引用廖雪峯老師畫的圖了:

    在這裏插入圖片描述
    Python支持多種數據類型,在計算機內部,可以把任何數據都看成一個“對象”,而變量就是在程序中用來指向這些數據對象的,對變量賦值就是把數據和變量給關聯起來。
    對變量賦值x=y是把變量x指向真正的對象, 該對象是變量y所指向的。 隨後對變量y的賦值不影響變量x的指向。

  4. Python中的兩種除法: / 和 //
    / 計算結果是浮點數,即使兩個整數恰好整除, 結果也是浮點數
    // 表示地板除法, 兩個整數的除法依然是整數, 浮點數相除最後也是整數

    9/3 = 3.0
    10 // 3 = 3
    

    PS: Python的整數沒有大小限制,而某些語言的整數根據其存儲長度是有大小限制的,例如Java對32位整數的範圍限制在-2147483648-2147483647。Python的浮點數也沒有大小限制,但是超出一定範圍就直接表示爲inf(無限大)。

2.2 字符串和編碼

這一塊我需要好好的整理了, 之前沒太仔細的學習這塊, 導致有些東西似懂非懂。

  1. 字符編碼的問題(捋一捋ASCII,GB2312,Utf-8,Unicode)
    字符串也是一種數據類型,但是字符串比較特殊的是一個編碼問題, 因爲計算機只能處理數字, 如果要處理文本, 就需要先把文本轉換成數字。

    最早的計算機在設計時採用8個比特(bit)作爲一個字節(byte),所以,一個字節能表示的最大的整數就是255(二進制11111111=十進制255),如果要表示更大的整數,就必須用更多的字節, 兩個字節可以表示的最大整數是65535。

    計算機是美國人發明的, 因此,最早只有127個字符被編碼到計算機裏,也就是大小寫英文字母、數字和一些符號, 這個編碼表被稱爲ASCII編碼。 比如大寫字母A的編碼時65, 小寫字母z的編碼時122。

    但是要處理中文,顯然一個字節是不夠的, 至少需要兩個字節, 而且還不能和ASCII編碼衝突, 所以中國製定了GB2312編碼,用來把中文編進去。

    但是全世界有上百種語言, 像中國一樣,每個國家有各國的標準, 那很容易就亂了套, 出現衝突,也就是在多語言混合的文本中,會出現亂碼的問題。因此, Unicode應運而生, Unicode把所有語言都統一到一套編碼裏,就不會再有亂碼問題了, 看這個名字也能看出來

    現在就可以來看看ASCII編碼和Unicode編碼的區別:ASCII編碼是1個字節, 而Unicode編碼通常2個字節

    在這裏插入圖片描述
    但是這樣統一成Unicode編碼,亂碼問題從此消失, 但是,如果你寫的如果全是英文, 用Unicode編碼比ASCII編碼需要多一倍的存儲空間,在存儲和傳輸上都會造成一種浪費。所以本着節約的精神, 又出現了把Unicode編碼轉換成“可變長”的UTF-8編碼。UTF-8編碼把一個Unicode字符根據不同的數字大小編碼成1-6個字節,常用的英文字母被編碼成1個字節,漢字通常是3個字節,只有很生僻的字符纔會被編碼成4-6個字節。如果你要傳輸的文本包含大量英文字符,用UTF-8編碼就能節省空間

    所以這個就是這幾種編碼之間的關係了,之前遇到編碼問題總是心態一崩, 現在終於搞清楚了這些編碼之間的邏輯了。

    搞清楚了ASCII、Unicode和UTF-8的關係,我們就可以總結一下現在計算機系統通用的字符編碼工作方式:在計算機內存中,統一使用Unicode編碼,當需要保存到硬盤或者需要傳輸的時候,就轉換爲UTF-8編碼, 比如用記事本編輯的時候,從文件讀取的UTF-8字符被轉換爲Unicode字符到內存裏,編輯完成後,保存的時候再把Unicode轉換爲UTF-8保存到文件:

    在這裏插入圖片描述
    瀏覽網頁的時候,服務器會把動態生成的Unicode內容轉換爲UTF-8再傳輸到瀏覽器:
    在這裏插入圖片描述
    所以你看到很多網頁的源碼上會有類似 <meta charset="UTF-8" /> 的信息,表示該網頁正是用的UTF-8編碼。

  2. Python的字符串
    在最新的Python 3版本中,字符串是以Unicode編碼的,也就是說,Python的字符串支持多語言, 可以使用ord()函數獲取字符的整數表示, chr()函數把編碼轉成對應的字符:

    ord('A')    # 65
    ord('中')   # 20013
    chr(25991)   # 文
    

    由於Python的字符串類型是str, 在內存中以Unicode表示, 一個字符對應若干個字節。** 如果要在網絡上傳輸, 或者保存到磁盤上,就需要把str變爲以字節爲單位的bytes。** Python中bytes類型的數據用b前綴的單引號或者雙引號表示:

    X1 = 'ABC'
    X2 = b'ABC'
    

    注意,這兩個是不一樣的, X1是個字符串, X2雖然和X1顯示的內容一樣,但bytes的每個字符都只佔用一個字節。

    以Unicode表示的str通過encode()方法可以編碼爲指定的bytes

    'ABC'.encode('ascii')   # b'ABC'
    '中文'.encode('utf-8')    # b'\xe4\xb8\xad\xe6\x96\x87'
    '中文'.encode('ascii')    # 這個會報錯, 中文不能轉成ASCII編碼
    

    如果我們從網絡或磁盤上讀取了字節流, 那麼讀到的數據就是bytes。 要把bytes變爲str, 就需要用decode()方法。

    b'ABC'.decode('ascii')   # 'ABC'
    b'\xe4\xb8\xad\xe6\x96\x87'.decode('utf-8')  # 中文
    

    如果bytes中包含無法解碼的字節,用decode()方法會報錯, 如果只有一小部分是無效的字節, 可以傳入errors='ignore’忽略錯誤的字節。

    b'\xe4\xb8\xad\xff'.decode('utf-8')   # 會報錯
    b'\xe4\xb8\xad\xff'.decode('utf-8', errors='ignore')  # ‘中’
    
  3. len()函數計算字符串長度
    我們知道, 如果len(str)會計算包含多少個字符, 但是你知道嗎? 如果len(bytes)呢, 就會計算字節數:

    len('中文')    # 2
    len(b'\xe4\xb8\xad\xe6\x96\x87')   # 6
    

    由於Python源代碼也是一個文本文件,所以,當你的源代碼中包含中文的時候,在保存源代碼時,就需要務必指定保存爲UTF-8編碼。當Python解釋器讀取源代碼時,爲了讓它按UTF-8編碼讀取,我們通常在文件開頭寫上這兩行:

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    

    現在終於知道爲啥需要加這兩行了, 第一行註釋是爲了告訴Linux/OS X系統,這是一個Python可執行程序,Windows系統會忽略這個註釋;第二行註釋是爲了告訴Python解釋器,按照UTF-8編碼讀取源代碼,否則,你在源代碼中寫的中文輸出可能會有亂碼。

2.3 list & tuple

  1. Python的核心數據類型
    Python的核心數據類型有如下幾種:

    • 數字 (1234, 3.14, 3+4j)
    • 字符串 (‘apple’, ‘1234’)
    • 列表 ([1, [1, 2], 2], list(range(10))
    • 字典 ({‘food’:‘spam’, ‘taste’:‘yum’}, dict(hous=10))
    • 元組 ((1, ‘apam’, 4, ‘U’), tuple(‘spam’))
    • 集合 (set(‘abc’))


    這些對象可以歸類爲可變的和不可變的。 數字,字符串和元組是不可變的, 列表,字典,集合是可變的, 這個可變和不可變理解起來,就是可變對象建立之後是可以修改的,即內部的內容是可以變化的。 比如就拿列表來說a=[1,5,3],我們可以a.append(4),此時a指向的對象就變成了[1,5,3,4],但其實是在原對象[1,2,3]上進行的添加,此過程沒有新對象產生。 我們還可以a.sort(), 這時候a指向的對象就變成了[1, 3, 4, 5], 但依然是原對象上進行的改變。

    而不可變對象是一旦創建之後,這個對象本身就定了,不能再去修改它本身的值,比如數字, 字符串等,所以雖然你常常會看到這樣的操作:a="abc", a="123", 你可能會有疑問,這不變了嗎? 其實這個過程中,變得只是a的指向, 而像"abc", "123"這種本身就沒法變了, 所以在這個過程中會有新對象產生。下面這個例子可能更可以說明情況:

    a = 'abc'
    a.replace('a', 'A')    # 這一個其實是新建立了一個對象, 所以如果想利用這個改變a的值的話, 應該a = a.replace('a', 'A')
    a      # 這時候a指向的對象依然是'abc'
    

    即對於不變對象來說,調用對象自身的任意方法,也不會改變該對象自身的內容。相反,這些方法會創建新的對象並返回,這樣,就保證了不可變對象本身永遠是不可變的,明白這個的意義就知道了在操作中是否會有新對象的產生, 有時候我們想改變對象值的時候,往往喜歡直接就調用對象自身有的函數(a.replace和列表的.append這樣的), 有時候卻發現改變不過來,或者不想改變值的時候有時候卻變了,那時候還很奇怪呢! 原因就在這裏, 我不知道你犯沒犯過這樣的錯誤:

    a = [1, 2, 3, 4]    # 我想在a的後面添加一個新元素5, 結果寫成下面這樣
    a = a.append(5)    # 你可以運行一下這個的結果, 其實正確的應該是下面的直接添加
    a.append(5)
    
  2. 關於元組
    如果元組中有一個元組, 這時候定義的時候必須加上逗號, 否則會產生歧義

    t = (1)   # 這其實是一個數,不是元組
    t = (1,)   # 這纔是個元組
    
  3. 關於元組不可變的理解
    tuple所謂的“不變”是說,tuple的每個元素,指向永遠不變。 這是什麼意思? 看個例子:

    t = ('a', 'b', ['A', 'B'])
    t[2][0] = 'X'
    t[2][1] = 'Y'
    t     # ('a', 'b', ['X', 'Y'])
    

    會發現上面的這個操作, t發生了改變,不是說一旦元組定義之後就不變了嗎? 怎麼又變了? 我們先看看這個過程具體是怎麼樣的(下面圖片來自廖雪峯老師的官方網站):
    在這裏插入圖片描述
    我們可以看到,第一行代碼,在內部其實是這個樣子的, 其中t指向了一個元組。 當我們把list的元素’A’和’B’修改爲’X’和’Y’後, 元組變爲:
    在這裏插入圖片描述
    表面上看,tuple的元素確實變了,但其實變的不是tuple的元素,而是list的元素。tuple一開始指向的list並沒有改成別的list,所以,tuple所謂的“不變”是說,tuple的每個元素,指向永遠不變。所以如果想創建一個不變的tuple, 就必須保證tuple的每個元素本身也不能變。

2.4 dict & set

  1. dict爲什麼查找速度這麼快?
    Python內置了字典:dict的支持,dict全稱dictionary,在其他語言中也稱爲map,使用鍵-值(key-value)存儲,具有極快的查找速度。但是爲什麼查找速度這麼快呢?

    這是因爲dict的實現原理和查字典是一樣的。假設字典包含了1萬個漢字,我們要查某一個字,一個辦法是把字典從第一頁往後翻,直到找到我們想要的字爲止,這種方法就是在list中查找元素的方法,list越大,查找越慢。第二種方法是先在字典的索引表裏(比如部首表)查這個字對應的頁碼,然後直接翻到該頁,找到這個字。無論找哪個字,這種查找速度都非常快,不會隨着字典大小的增加而變慢。dict就是採用了後者。但這種方式也有一個不好的地方,就是會佔用大量的內存, 是一種空間換時間的方法。

  2. dict.get()方法
    我們知道,如果想訪問字典中的值,我們可以直接用鍵,比如下面的代碼:

    score = {'Jack': 88, 'Amy': 90}
    # 我查Amy的分數
    score['Amy']     # 90
    # 我查Tom的分數
    score['Tom']    # 這樣就會報錯, 畢竟沒有Tom這個鍵嘛
    

    所以如果是這種沒有鍵的時候我們用什麼方法呢? 那就是.get()方法了。我們可以使用:

    score.get('Tom', -1)   # 這樣就不會報錯了,返回-1
    
  3. 關於dict的key要注意
    dict可以用在需要高速查找的很多地方,在Python代碼中幾乎無處不在,正確使用dict非常重要,需要牢記的第一條就是dict的key必須是不可變對象。

    這是因爲dict根據key來計算value的存儲位置,如果每次計算相同的key得出的結果不同,那dict內部就完全混亂了。這個通過key計算位置的算法稱爲哈希算法(Hash)。要保證hash的正確性,作爲key的對象就不能變。在Python中,字符串、整數等都是不可變的,因此,可以放心地作爲key。而list是可變的,就不能作爲key, 比如下面就會報錯:

    >>> key = [1, 2, 3]
    >>> d[key] = 'a list'
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: unhashable type: 'list'
    

    另外一條,請務必注意,dict內部存放的順序和key放入的順序是沒有關係的。

  4. 關於set
    set和dict類似,也是一組key的集合,但不存儲value。由於key不能重複,所以,在set中,沒有重複的key。所以如果想看看一個列表中究竟有多少個不同的元素就可以:

    s = set([1, 1, 2, 3, 3, 2])
    s # {1, 2, 3}
    

    .add()方法可以添加元素到set中, 可以重複添加, 但不會有效果。.remove()方法移除元素。
    set可以看成數學意義上的無序和無重複元素的集合,因此,兩個set可以做數學意義上的交集、並集等操作:

    >>> s1 = set([1, 2, 3])
    >>> s2 = set([2, 3, 4])
    >>> s1 & s2
    {2, 3}
    >>> s1 | s2
    {1, 2, 3, 4}
    

    set和dict的唯一區別僅在於沒有存儲對應的value,但是,set的原理和dict一樣,所以,同樣不可以放入可變對象,因爲無法判斷兩個可變對象是否相等,也就無法保證set內部“不會有重複元素”。 把list放入set,會報錯。看下面這幾個例子:

    a = {(1,2,3):4}   # 這個是可以的,因爲tuple是不可變對象
    a = {(1,2,[2,3]):4}     # 這塊會報錯,因爲tuple裏面有列表, 沒法保證列表的不可變
    b = set((1,2,3)   # 這個可以
    b = set((1,2,[2,3]))   # 這塊也會報錯, 道理和上面一樣
    

3. 函數

  1. 給函數起別名
    函數名其實就是指向一個函數對象的引用,完全可以把函數名賦給一個變量,相當於給這個函數起了一個“別名”:

    >>> a = abs # 變量a指向abs函數
    >>> a(-1) # 所以也可以通過a調用abs函數
    1
    

    這個在用Pytorch搭建神經網絡的時候經常遇到這種起別名的操作。

  2. 空函數
    Python中有個比較騷的操作就是可以定義一個函數,但是什麼都不寫,

    def nop():
        pass
     
    if age >= 18:
    	pass          # 如果這時候缺少pass,就會報語法錯誤
    

    那有什麼用? 寫程序的時候,我們往往喜歡先搭一個簡單的框架出來, 把各種函數定義好放那,至於作用可能一時半會還沒想好,這時候就可以放一個pass, 讓代碼運行起來再說。

  3. 返回多個值
    Python貌似可以返回多個值呢? 但是真的是這樣嗎?

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

    上面代碼看起來,好像python的return可以同時返回多個值, 其實這是一種假象, python函數返回的仍然是單一值,只不過這個單一值是一個元組。 在語法上,返回一個tuple可以省略括號,而多個變量可以同時接收一個tuple,按位置賦給對應的值,所以,Python的函數返回多值其實就是返回一個tuple,但寫起來更方便。

  4. 函數的默認參數
    python函數定義的時候可以給出默認參數, 這樣簡化了函數調用的難度, 無論是簡單調用還是複雜調用, 函數只需要定義一個即可。 舉個例子體會一下:

    # 一個一年級小學生註冊的函數, 需要傳入name和gender兩個參數:
    def enroll(name, gender):
    	print(name)
    	print(gender)
    
    # 這樣調用的時候只需要傳入兩個參數
    enroll('Sarah', 'F')              # 就會輸出名字和性別
    

    但是如果我想傳入年齡, 城市等信息怎麼辦, 上面那個就不能用了, 所以看看有默認參數的函數:

    def enroll(name, gender, age=6, city='Beijing'):
        print('name:', name)
        print('gender:', gender)
        print('age:', age)
        print('city:', city)
    

    這樣, 大多數學生註冊的時候不需要提供年齡和城市,只提供必須的兩個參數性別和姓名。 依然可以:

    enroll('Sarah', 'F') 
    
    # 但這個還可以更復雜的調用
    enroll('Bob', 'M', 7)
    enroll('Adam', 'M', city='Tianjin')
    

    所以默認參數的使用還是非常方便的,在定義神經網絡函數的時候,就通常會有默認的學習率, 迭代次數等參數。 但也有一個注意的點: 首先就是設置的時候, 必選參數要放在前面,默認參數要放後面, 否則解釋器不知道你使用的是默認參數還是必選參數。 其次就是默認參數的順序, 變化大的放前面, 變化小的放後面。 最後,有多個默認參數時, 調用的時候既可以按順序提供默認參數,也可以不按順序提供, 但此時要把默認參數的參數名加上。(上面那個例子)

    關於默認參數的使用, 還會有坑:默認參數必須指向不變對象!, 這是啥意思, 下面演示一個例子:我定義了一個函數, 傳入一個list

    def add_end(L=[]):
    	L.append('END')
    	return L
    
    # 我正常調用
    add_end([1, 2, 3])    # [1, 2, 3, 'END']
    add_end(['x', 'y', 'z'])    # ['x', 'y', 'z', 'END']
    
    # 我使用默認參數調用
    a = add_end()
    b = add_end()
    c = add_end()
    
    # 你知道執行之後, a, b, c都是什麼值了嗎?
    print(a)     # ['END', 'END', 'END'] 
    print(b)     # ['END', 'END', 'END'] 
    print(c)     # ['END', 'END', 'END'] 
    

    爲什麼默認參數是[], 但是函數似乎每次都“記住了”上次添加了’END’後的list呢? 這是因爲Python函數在定義的時候, 默認參數L的值就被計算出來了,即[], 因爲默認參數L也是一個變量, 它指向對象[], 每次調用該函數, 如果改變了L的內容, 則下次調用的時候, 默認參數的內容就變了,不再是函數定義時的[]了。所以, 默認參數必須指向不變對象!。 要修改上面例子,我們可以使用不變對象None來實現:

    def add_end(L=None):
    	if L is None:
    		L = []
    	L.append('END')
    	return L
    
    a = add_end()
    b = add_end()
    c = add_end()
    print(a)   # ['END']
    print(b)   # ['END']
    print(c)    # ['END']
    
    # 可以和上面進行一個對比
    

    爲什麼要設計str、None這樣的不變對象呢? 因爲不變對象一旦創建, 對象內部的數據就不能修改, 這樣就減少了由於修改數據導致的錯誤。 此外,由於對象不變, 多任務環境下同時讀取對象不需要加鎖,同時讀問題不會產生。 我們編寫程序時,如果可以設計一個不變對象, 那就儘量設計成不變的對象。

  5. 函數的可變參數(* num)
    在Python函數中,還可以定義可變參數。顧名思義,可變參數就是傳入的參數個數是可變的,可以是1個、2個到任意個,還可以是0個。 這個之前確實不知道是啥意思,所以在這裏整理一下, 以一個數學題爲例子:

    給定一組數字a, b, c…, 計算a2+b2+c2.....a^2+b^2+c^2....., 要定義出這個函數,我們必須確定輸入的參數。由於參數個數不確定,我們首先想到可以把a,b,c……作爲一個list或tuple傳進來

    def calc(numbers):
    	sum = 0
    	for n in numbers:
    		sum += n * n
    	return sum
    
    # 分分鐘搞定, 然後我們調用, 得先組裝成一個數組
    calc([1, 2, 3])    # 14
    calc((1, 3, 5, 7))   # 84
    

    如果利用可變參數的話,我們就可以不用先組裝成數組往裏面傳:

    def calc(*numbers):
    	sum = 0
    	for n in numbers:
    		sum = sum + n * n
    	return sum
    
    # 這時候調用
    calc(1, 2, 3)
    calc(1, 3, 5, 7)
    # 甚至不傳都可以
    calc()
    

    定義可變參數和定義一個list或者tuple參數相比,僅僅在參數前面加了一個*(可千萬別認爲這成了指針了, python中就沒有指針一說),在函數內部, 參數numbers接收的是一個tuple, 因此函數代碼完全不變,調用該函數時,可以傳入任意個參數, 包括0個參數。

    如果此時有了一個list或者tuple, 要調用一個可變參數怎麼辦?

    # 可以這樣做
    nums = [1, 2, 3]
    calc(nums[0], nums[1], nums[2])
    
    # 但上面這樣更加繁瑣,所以可以直接在nums前加*, 把list或tuple元素變成可變參數傳入:
    calc(*nums)
    

    這種寫法也是非常常見, *nums表示把nums這個list的所有元素作爲可變參數傳進去。

  6. 函數的關鍵字參數(** kw)
    可變參數允許傳入0和或者任意個參數, 這些可變參數在調用時自動組裝成一個tuple。 而關鍵字參數允許傳入0個或者任意個含參數名的參數, 這些關鍵字參數在函數內部自動封裝成一個dict, 就是帶名字的參數,可以傳入任意個, 看例子就明白了:

    def person(name, age, **kw):
    	print(name, age, kw)
    
    person('Michael', 30)    # Michael  30  {}
    person('Bob', 35, city='Beijing')   # Bob  35  {'city':'Beijing'}
    person('Adam', 45, gender='M', job='Engineer')   # Adam 45 {'gender':'M', 'job':'Engineer'}
    

    關鍵字參數有什麼用呢? 它可以擴展函數的功能。比如,在person函數裏,我們保證能接收到name和age這兩個參數,但是,如果調用者願意提供更多的參數,我們也能收到。試想你正在做一個用戶註冊的功能,除了用戶名和年齡是必填項外,其他都是可選項,利用關鍵字參數來定義這個函數就能滿足註冊的需求。和可變參數類似,也可以先組裝出一個dict,然後,把該dict轉換爲關鍵字參數傳進去:

    extra = {'city':'Beijing', 'job':'Engineer'}
    person('Jack', 34, **extra)   # name: Jack age: 34 other: {'city': 'Beijing', 'job': 'Engineer'}
    

    **extra表示把extra這個dict的所有key-value用關鍵字參數傳入到函數的**kw參數,kw將獲得一個dict,注意kw獲得的dict是extra的一份拷貝,對kw的改動不會影響到函數外的extra

  7. 函數的命名關鍵字參數
    對於關鍵字參數,函數的調用者可以傳入任意不受限制的關鍵字參數,如果要限制關鍵字參數的名字,就可以用命名關鍵字參數:

    def person(name, age, *, city, job):
        print(name, age, city, job)
    

    和關鍵字參數**kw不同,命名關鍵字參數需要一個特殊分隔符**後面的參數被視爲命名關鍵字參數。調用的時候,必須傳入參數名, 否則,解釋器會報錯。

    person('Jack', 24, city='Beijing', job='Engineer')
    person('jack', 24, 'Beijing', 'Enginneer')   # 這種會報錯, 解釋器會看成四個位置參數, 但其實person函數有兩個位置參數, 後面的兩個叫做命名關鍵字參數
    

    如果函數定義中已經有了一個可變參數,後面跟着的命名關鍵字參數就不再需要一個特殊分隔符*了:

    def person(name, age, *args, city, job):
        print(name, age, args, city, job)
    

    使用命名關鍵字參數時,要特別注意,如果沒有可變參數,就必須加一個*作爲特殊分隔符。如果缺少*,Python解釋器將無法識別位置參數和命名關鍵字參數。

    在Python中定義函數,可以用必選參數、默認參數、可變參數、關鍵字參數和命名關鍵字參數,這5種參數都可以組合使用。但是請注意,參數定義的順序必須是:必選參數、默認參數、可變參數、命名關鍵字參數和關鍵字參數

    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)
    

    雖然可以組合多達5種參數,但不要同時使用太多的組合,否則函數接口的可理解性很差。 多了,自己都不知道咋調用了!

  8. 函數的遞歸調用
    遞歸函數定義簡單,邏輯清晰,但是要注意防止棧溢出。在計算機中, 函數調用時通過棧實現的, 每當進入一個函數調用, 棧就會加一層棧幀,每當函數返回,棧就會減一層棧幀。由於棧的大小不是無限的,所以,遞歸調用的次數過多,會導致棧溢出。 解決遞歸調用棧溢出的方法是通過尾遞歸優化,事實上尾遞歸和循環的效果是一樣的,所以,把循環看成是一種特殊的尾遞歸函數也是可以的。比如我們算階乘的那個例子:

    # 這是前向遞歸
    def fact(n):
        if n==1:
            return 1
        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)   # 參數這裏會有運算規則
    
    fact(5)
    

    算階乘的這個例子中,前向遞歸給我的感覺就是函數向下遞歸, 然後再向上返回結果來, 比如我算5的階乘, 那麼我得去算4的階乘,要算4的,我得先去算3的, 直到算到1的階乘爲1, 然後2的階乘就可以算出來,然後3的,4的,最後算出5的階乘。
    在這裏插入圖片描述
    而尾遞歸的話,就是把當前的運算結果或者路徑放在參數裏面傳給下層的函數, 這樣每遞歸一次就會算出相應的結果,在最後一層得到最終的結果。函數往下遞歸,不用返回來。比如我想算5的階乘, 那麼我這一層會給出一個5來,然後在函數參數裏面給出規則,然後我第二層的話就是讓5 * 4,得到20, 然後我再到第三層讓20 * 3,得到60, 然後後一層讓60 * 2,得到120, 最後120 * 1 得到結果。 可以體會一下是不是不一樣? 這樣的感覺:
    在這裏插入圖片描述
    這種情況下,沒有必要去保存任何局部變量了,只保留後一個函數的堆棧即可。 當然這裏理論上,實際上, 大多數編程語言沒有針對尾遞歸做優化,python中,即使改成尾遞歸,也會導致棧溢出。

4. 總結梳理

最後梳理一遍知識, 首先python的基礎知識裏面摳了點細節,比如那個字符串前面的小r,這個代表轉義失效。 然後就是動態語言和靜態語言的區別,還有兩種除法。 在字符串編碼那塊整理了各種編碼以及之間的關係,從ASCII,GB2312, UNicode,utf-8。 然後就是基本的類型,像列表,元組,字典集合等一些細節部分,可變不可變對象等。 函數部分, 整理了函數起別名,空函數,返回多個值的內部原理。 還有各種函數參數,必選參數,默認參數,可變參數(*num),關鍵字參數(** kw), 命名關鍵字參數等。 最後介紹了兩種遞歸的細節,關於尾遞歸,參數裏面要傳入當前結果或者路徑。 下面一張思維導圖把知識拎起來:
在這裏插入圖片描述

這篇內容先整理兩塊, 基礎知識和函數, 這樣整理髮現,原理python裏面也是有很多的細節部分的,雖然每天用的不少,但這些東西確實不知道或者沒太上心, 所以藉着這次整理一下,這樣後面看人家的代碼也容易一些,之前看人家代碼裏面函數參數的時候,總是看到帶一個*, 兩個 ** 的,當時一臉懵逼,還以爲python裏面也有C那套指針,指針的指針, 原來人家這是可變參數和關鍵字參數,唉, 無知是多麼的可怕啊, 學起來吧 😉

下一篇初步三塊內容, 高級特徵、函數式編程和模塊部分, 具體的得看有多少未知的內容,繼續探索。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章