Python學習筆記(進階篇一)

筆記整理出處:廖雪峯教程java

進階

函數

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

咱們以自定義一個求絕對值的my_abs函數爲例:算法

def my_abs(x):
    if x >= 0:
        return x
    else:
        return -x複製代碼

請注意,函數體內部的語句在執行時,一旦執行到return時,函數就執行完畢,並將結果返回。所以,函數內部經過條件判斷和循環能夠實現很是複雜的邏輯。數組

若是沒有return語句,函數執行完畢後也會返回結果,只是結果爲None。
return None能夠簡寫爲return。
python中函數沒有返回值類型聲明,同時,函數名其實就是指向一個函數對象的引用,徹底能夠把函數名賦給一個變量,至關於給這個函數起了一個「別名」:數據結構

>>> a = abs # 變量a指向abs函數
>>> a(-1) # 因此也能夠經過a調用abs函數
1複製代碼
位置參數

咱們先寫一個計算x2的函數:app

def power(x):
    return x * x複製代碼

對於power(x)函數,參數x就是一個位置參數。函數

當咱們調用power函數時,必須傳入有且僅有的一個參數x:優化

默認參數
def power(x , y = 2):
    return x * y複製代碼

咱們調用時既能夠這樣用power(2,3),也能夠這樣用power(2),明顯的,當咱們不傳遞y這個參數時,方法內部會去y的默認值進行運算,也就是2spa

默認參數能夠簡化函數的調用。設置默認參數時,有幾點要注意:設計

  • 必選參數在前,默認參數在後,不然Python的解釋器會報錯(思考一下爲何默認參數不能放在必選參數前面);

  • 如何設置默認參數。
    當函數有多個參數時,把變化大的參數放前面,變化小的參數放後面。變化小的參數就能夠做爲默認參數。

使用默認參數有什麼好處?最大的好處是能下降調用函數的難度。由於有些參數,可能咱們大部分時間傳遞的是一樣的值。
注意事項:

  • 定義默認參數要牢記一點:默認參數必須指向不變對象!
  • 定義默認參數要牢記一點:默認參數必須指向不變對象!
  • 定義默認參數要牢記一點:默認參數必須指向不變對象!

舉例說明,先定義一個函數,傳入一個list,添加一個END再返回:

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']複製代碼

當你使用默認參數調用時,一開始結果也是對的:

>>> add_end()
['END']複製代碼

可是,再次調用add_end()時,結果就不對了:

>>> add_end()
['END', 'END']
>>> add_end()
['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複製代碼

如今,不管調用多少次,都不會有問題:

>>> add_end()
['END']
>>> add_end()
['END']複製代碼

爲何要設計str、None這樣的不變對象呢?由於不變對象一旦建立,對象內部的數據就不能修改,這樣就減小了因爲修改數據致使的錯誤。此外,因爲對象不變,多任務環境下同時讀取對象不須要加鎖,同時讀一點問題都沒有。咱們在編寫程序時,若是能夠設計一個不變對象,那就儘可能設計成不變對象。

可變參數

定義與java相似,基本使用方法以下:

def calc(*numbers):
    sum = 0
    for n in numbers:
        sum = sum + n * n
    return sum複製代碼

對於已經存在的list類型參數,可變參數的使用方法和java略有不一樣,不能直接傳入該變量,須要增長*

>>> nums = [1, 2, 3]
>>> calc(*nums)
14複製代碼

*nums表示把nums這個list的全部元素做爲可變參數傳進去。這種寫法至關有用,並且很常見。

關鍵字參數

可變參數容許你傳入0個或任意個參數,這些可變參數在函數調用時自動組裝爲一個tuple。而關鍵字參數容許你傳入0個或任意個含參數名的參數,這些關鍵字參數在函數內部自動組裝爲一個dict。請看示例:

def person(name, age, **kw):
    print('name:', name, 'age:', age, 'other:', kw)複製代碼

函數person除了必選參數name和age外,還接受關鍵字參數kw。在調用該函數時,能夠只傳入必選參數:

>>> person('Michael', 30)
name: Michael age: 30 other: {}複製代碼

也能夠傳入任意個數的關鍵字參數:

>>> person('Bob', 35, city='Beijing')
name: Bob age: 35 other: {'city': 'Beijing'}
>>> person('Adam', 45, gender='M', job='Engineer')
name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}複製代碼

和可變參數相似,也能夠先組裝出一個dict,而後,把該dict轉換爲關鍵字參數傳進去:

>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, **extra)
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}複製代碼

**extra表示把extra這個dict的全部key-value用關鍵字參數傳入到函數的kw參數,kw將得到一個dict,

注意kw得到的dict是extra的一份拷貝,對kw的改動不會影響到函數外的extra。

命名關鍵字參數

對於關鍵字參數,函數的調用者能夠傳入任意不受限制的關鍵字參數。至於到底傳入了哪些,就須要在函數內部檢查。
若是要限制關鍵字參數的名字,就能夠用命名關鍵字參數,例如,只接收city和job做爲關鍵字參數。這種方式定義的函數以下:

def person(name, age, *, city, job):
    print(name, age, city, job)
和關鍵字參數**kw不一樣,命名關鍵字參數須要一個特殊分隔符*,*後面的參數被視爲命名關鍵字參數。

調用方式以下:
~~~python
>>> person('Jack', 24, city='Beijing', job='Engineer')
Jack 24 Beijing Engineer複製代碼

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

def person(name, age, *args, city, job):
    print(name, age, args, city, job)複製代碼

命名關鍵字參數必須傳入參數名,這和位置參數不一樣。若是沒有傳入參數名,調用將報錯:

>>> person('Jack', 24, 'Beijing', 'Engineer')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: person() takes 2 positional arguments but 4 were given複製代碼

因爲調用時缺乏參數名city和job,Python解釋器把這4個參數均視爲位置參數,但person()函數僅接受2個位置參數。

命名關鍵字參數能夠有缺省值,從而簡化調用:

def person(name, age, *, city='Beijing', job):
    print(name, age, city, job)
因爲命名關鍵字參數city具備默認值,調用時,可不傳入city參數:
~~~python
>>> person('Jack', 24, job='Engineer')
Jack 24 Beijing Engineer複製代碼

使用命名關鍵字參數時,要特別注意,若是沒有可變參數,就必須加一個做爲特殊分隔符。若是缺乏,Python解釋器將沒法識別位置參數和命名關鍵字參數:

def person(name, age, city, job):
    # 缺乏 *,city和job被視爲位置參數
    pass複製代碼
參數組合

在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)複製代碼

在函數調用的時候,Python解釋器自動按照參數位置和參數名把對應的參數傳進去。

遞歸函數

使用遞歸函數須要注意防止棧溢出。在計算機中,函數調用是經過棧(stack)這種數據結構實現的,每當進入一個函數調用,棧就會加一層棧幀,每當函數返回,棧就會減一層棧幀。因爲棧的大小不是無限的,因此,遞歸調用的次數過多,會致使棧溢出。
解決遞歸調用棧溢出的方法是經過尾遞歸優化,事實上尾遞歸和循環的效果是同樣的,因此,把循環當作是一種特殊的尾遞歸函數也是能夠的。

高級特性

切片

對常常取指定索引範圍的操做,用循環十分繁瑣,所以,Python提供了切片(Slice)操做符,能大大簡化這種操做。
取前3個元素,用一行代碼就能夠完成切片:

>>> L[0:3]
['Michael', 'Sarah', 'Tracy']複製代碼

前開後閉原則。默認從第一個開始取時能夠省略不寫0.
相似的,Python支持L[-1]取倒數第一個元素,那麼它一樣支持倒數切片:

>>> L[-2:]
['Bob', 'Jack']
>>> L[-2:-1]
['Bob']複製代碼

記住倒數第一個元素的索引是-1。
支持間隔取值,好比前10個數,每兩個取一個:

>>> L[:10:2]
[0, 2, 4, 6, 8]複製代碼

全部數,每5個取一個:

>>> L[::5]
[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]複製代碼

甚至什麼都不寫,只寫[:]就能夠原樣複製一個list

>>> L[:]
[0, 1, 2, 3, ..., 99]複製代碼

tuple也是一種list,惟一區別是tuple不可變。所以,tuple也能夠用切片操做,只是操做的結果還是tuple:

>>> (0, 1, 2, 3, 4, 5)[:3]
(0, 1, 2)複製代碼

字符串'xxx'也能夠當作是一種list,每一個元素就是一個字符。所以,字符串也能夠用切片操做,只是操做結果還是字符串:

>>> 'ABCDEFG'[:3]
'ABC'
>>> 'ABCDEFG'[::2]
'ACEG'複製代碼
迭代

只要是可迭代對象,不管有無下標,均可以迭代,好比dict就能夠迭代:

>>> d = {'a': 1, 'b': 2, 'c': 3}
>>> for key in d:
...     print(key)
...
a
c
b複製代碼

默認狀況下,dict迭代的是key。若是要迭代value,能夠用for value in d.values(),若是要同時迭代key和value,能夠用for k, v in d.items()。
因爲字符串也是可迭代對象,所以,也能夠做用於for循環。

那麼,如何判斷一個對象是可迭代對象呢?方法是經過collections模塊的Iterable類型判斷:

>>> from collections import Iterable
>>> isinstance('abc', Iterable) # str是否可迭代
True
>>> isinstance([1,2,3], Iterable) # list是否可迭代
True
>>> isinstance(123, Iterable) # 整數是否可迭代
False複製代碼

最後一個小問題,若是要對list實現相似Java那樣的下標循環怎麼辦?Python內置的enumerate函數能夠把一個list變成索引-元素對,這樣就能夠在for循環中同時迭代索引和元素自己:

>>> for i, value in enumerate(['A', 'B', 'C']):
...     print(i, value)
...
0 A
1 B
2 C複製代碼

上面的for循環裏,同時引用了兩個變量,在Python裏是很常見的,好比下面的代碼:

>>> for x, y in [(1, 1), (2, 4), (3, 9)]:
...     print(x, y)
...
1 1
2 4
3 9複製代碼

任何可迭代對象均可以做用於for循環,包括自定義的數據類型,只要符合迭代條件,就可使用for循環。

列表生成式

若是要生成[1x1, 2x2, 3x3, ..., 10x10]怎麼作?方法一是循環:

>>> L = []
>>> for x in range(1, 11):
...    L.append(x * x)
...
>>> L
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]複製代碼

可是循環太繁瑣,而列表生成式則能夠用一行語句代替循環生成上面的list:

>>> [x * x for x in range(1, 11)]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]複製代碼

寫列表生成式時,把要生成的元素x * x放到前面,後面跟for循環,就能夠把list建立出來。
還可使用兩層循環,能夠生成全排列:

>>> [m + n for m in 'ABC' for n in 'XYZ']
['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']複製代碼

鑑於列表生成式的便捷性,過於複雜的邏輯不建議直接使用生成式來寫(我的觀點)

生成器

經過列表生成式,咱們能夠直接建立一個列表。可是,受到內存限制,列表容量確定是有限的。並且,建立一個包含100萬個元素的列表,不只佔用很大的存儲空間,若是咱們僅僅須要訪問前面幾個元素,那後面絕大多數元素佔用的空間都白白浪費了。

因此,若是列表元素能夠按照某種算法推算出來,那咱們是否能夠在循環的過程當中不斷推算出後續的元素呢?這樣就沒必要建立完整的list,從而節省大量的空間。在Python中,這種一邊循環一邊計算的機制,稱爲生成器:generator。

要建立一個generator,有不少種方法。第一種方法很簡單,只要把一個列表生成式的[]改爲(),就建立了一個generator:

>>> L = [x * x for x in range(10)]
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x * x for x in range(10))
>>> g
<generator object <genexpr> at 0x1022ef630>複製代碼

建立L和g的區別僅在於最外層的[]和(),L是一個list,而g是一個generator。
若是要一個一個打印出來,能夠經過next()函數得到generator的下一個返回值:

>>> next(g)
0
>>> next(g)
1
>>> next(g)
4
...
81
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration複製代碼

咱們講過,generator保存的是算法,每次調用next(g),就計算出g的下一個元素的值,直到計算到最後一個元素,沒有更多的元素時,拋出StopIteration的錯誤。

固然,上面這種不斷調用next(g)實在是太變態了,正確的方法是使用for循環,由於generator也是可迭代對象:

>>> g = (x * x for x in range(10))
>>> for n in g:
...     print(n)
... 
0
1複製代碼

定義generator的另外一種方法。若是一個函數定義中包含yield關鍵字,那麼這個函數就再也不是一個普通函數,而是一個generator:

def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        a, b = b, a + b
        n = n + 1
    return 'done'

>>> f = fib(6)
>>> f
<generator object fib at 0x104feaaa0>複製代碼

這裏,最難理解的就是generator和函數的執行流程不同。函數是順序執行,遇到return語句或者最後一行函數語句就返回。而變成generator的函數,在每次調用next()的時候執行,遇到yield語句返回,再次執行時從上次返回的yield語句處繼續執行。

舉個簡單的例子,定義一個generator,依次返回數字1,3,5:

def odd():
    print('step 1')
    yield 1
    print('step 2')
    yield(3)
    print('step 3')
    yield(5)複製代碼

調用該generator時,首先要生成一個generator對象,而後用next()函數不斷得到下一個返回值:

>>> o = odd()
>>> next(o)
step 1
1
>>> next(o)
step 2
3
>>> next(o)
step 3
5
>>> next(o)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration複製代碼

能夠看到,odd不是普通函數,而是generator,在執行過程當中,遇到yield就中斷,下次又繼續執行。執行3次yield後,已經沒有yield能夠執行了,因此,第4次調用next(o)就報錯。

回到fib的例子,咱們在循環過程當中不斷調用yield,就會不斷中斷。固然要給循環設置一個條件來退出循環,否則就會產生一個無限數列出來。

一樣的,把函數改爲generator後,咱們基本上歷來不會用next()來獲取下一個返回值,而是直接使用for循環來迭代:

>>> for n in fib(6):
...     print(n)
...
1
1
2
3
5
8複製代碼

可是用for循環調用generator時,發現拿不到generator的return語句的返回值。若是想要拿到返回值,必須捕獲StopIteration錯誤,返回值包含在StopIteration的value中:

>>> g = fib(6)
>>> while True:
...     try:
...         x = next(g)
...         print('g:', x)
...     except StopIteration as e:
...         print('Generator return value:', e.value)
...         break
...
g: 1
g: 1
g: 2
g: 3
g: 5
g: 8
Generator return value: done複製代碼
迭代器

能夠直接做用於for循環的數據類型有如下幾種:

一類是集合數據類型,如list、tuple、dict、set、str等;

一類是generator,包括生成器和帶yield的generator function。

這些能夠直接做用於for循環的對象統稱爲可迭代對象:Iterable。

可使用isinstance()判斷一個對象是不是Iterable對象:

>>> from collections import Iterable
>>> isinstance([], Iterable)
True
>>> isinstance(100, Iterable)
False複製代碼

而生成器不但能夠做用於for循環,還能夠被next()函數不斷調用並返回下一個值,直到最後拋出StopIteration錯誤表示沒法繼續返回下一個值了。

能夠被next()函數調用並不斷返回下一個值的對象稱爲迭代器:Iterator。
可使用isinstance()判斷一個對象是不是Iterator對象:

>>> from collections import Iterator
>>> isinstance((x for x in range(10)), Iterator)
True
>>> isinstance([], Iterator)
False複製代碼

生成器都是Iterator對象,但list、dict、str雖然是Iterable,卻不是Iterator。
把list、dict、str等Iterable變成Iterator可使用iter()函數

>>> isinstance(iter([]), Iterator)
True
>>> isinstance(iter('abc'), Iterator)
True複製代碼
相關文章
相關標籤/搜索