Python進階細節

焱城發表於2019-02-16

Python進階細節

根據慕課網七月老師視訊整理

一切皆物件

對與Python來說,一切皆物件,包括函式。在其他語言比如c++中,函式只是一段可執行的程式碼,只要你獲得入口地址就可以呼叫這段程式碼。但是Python中不一樣,Python中一切皆物件。Python中的函式,可以作為另一個函式的引數傳入到另外一個函式裡,也可以當作另外一個函式的返回值,甚至可以賦值給一個變數。

def a():
    pass

print(type(a))


<class `function`>   # 可見是一個class,是一個類。

閉包

閉包指的是:函式+環境變數(環境變數不能是全域性變數)。
python在函式內部還可以定義函式,但該函式作用域只在外部函式內部有效,除非作為外部函式的返回值被返回,在外部函式的外部用一個變數接收後就可以呼叫它了。

def curve_pre():
    a = 25  # 這裡定義了環境變數
    def curve(x):
        return a*x*x
    return curve

a = 10   # 在外部定義了a為10
f = curve_pre()
print(f(2))
print(f.__closure__)    # 可以通過這個內建變數來檢視閉包裡的內容
print(f.__closure__[0].cell_contents)


100
(<cell at 0x0000016832868708: int object at 0x0000000065DCECD0>,)
25

可見返回了一個物件。你再在外面定義了變數,也不會改變閉包內的環境變數。
閉包的意義在於返回了一個現場,如果沒有閉包,很容易被外部的變數所影響。

1. 閉包的經典誤區

閉包返回現場的環境變數,不能在閉包裡定義的函式裡面再被定義了,而且函式裡必須要有呼叫環境變數的地方,否則就不叫做閉包了。

def f1():
    a = 20
    def f2():
        a = 10  # 重複定義
        return a
    return f2
f = f1()
print(f.__closure__)

None  # 可見此時返回了None,不再是閉包了。本質上是認為此時a被認為是一個區域性變數,不再是環境變數了!

--------------------------------------------------------------------------

# 如果想要環境變數在函式裡被改變,可以這樣:

def f1():
    a = 25
    def f2():
        nonlocal a  # nonlocal關鍵字強制讓a不再為區域性變數,跳到上一級作為了環境變數。
        a = a + 10
        return a
    return f2
f = f1()
print(f.__closure__)
print(f())
print(f())
print(f())

(<cell at 0x000002A5CF348708: int object at 0x0000000065DCECD0>,)
35
45
55
# 可以看到a的值是可以儲存的,這是因為閉包的環境變數具有儲存現場的功能,記憶住上次呼叫的狀態,所以可以這樣做。

---------------------------------------------------------------------------


def f1():
    a = 20
    def f2():
        return 2  # 裡面不再呼叫a了
    return f2
f = f1()
print(f.__closure__)

None # 可見此時仍然不是閉包

---------------------------------------------------------------------------

def f1():
    a = 20
    def f2():
        s = a+20
        return 2
    return f2
f = f1()
print(f.__closure__)

(<cell at 0x00000294E8568708: int object at 0x0000000065DCEC30>,)
# 可見就算返回值裡不包括a,但是隻要呼叫了,就可以是一個閉包。

2. 閉包的優點

從閉包可以看出函數語言程式設計的優點。如果出現需要儲存值進行迭代的情況,就不得不定義一個全域性變數來儲存上一次的值。但是在閉包裡不需要使用到全域性變數,只需要閉包裡定義的環境變數即可記憶上一次的狀態,這樣就具有了封閉性,否則過多的使用全域性變數會使程式碼變得混亂。這裡再注意一個問題:

a = 10

def f1(x):
    a_new = a + x
    a = a_new

print(f1(5))

Traceback (most recent call last):
  File "c4.py", line 7, in <module>
    print(f1(5))
  File "c4.py", line 4, in f1
    a_new = a + x
UnboundLocalError: local variable `a` referenced before assignment

# 看起來美滋滋其實報錯了。再Python裡,如果再定義了a的話,無論是在哪裡,在定義的時候系統會預設a是區域性變數不再是全域性變數了。所以在執行程式碼的時候,就會出現了找不到區域性變數a的情況,因為f1中第一段程式碼中用到了區域性變數a,但是此時還沒有定義啊。

----------------------------------------------------------------------

# 可以這麼解決:
a = 10

def f1(x):
    global a   # 定義global關鍵字強制認為是全域性變數
    a_new = a + x
    a = a_new
    return a

print(f1(5))
print(f1(10))

15
25

# 可見這時候全域性變數起到了儲存的功能,但相對閉包,就顯得很Low了。

3. 閉包的一個經典例子

def testFun():  
    temp = [lambda x : i*x for i in range(4)]
    return temp

for everyLambda in testFun(): 
    print (everyLambda(2))
    
# 執行後結果竟然是:    
6
6
6
6

這裡testfun()返回一個temp,temp是一個列表,everyLambda每次返回的都是temp裡列表的值,引數2賦給x。

三元表示式

python中的三元表示式和其他語言中的不太一樣。
條件為真時返回的結果 if 判斷條件 else 條件為假時返回的結果
x if x > y else y
其他很多語言中是這麼定義的:
x > y ? x:y

map類

map不是一個函式而是一個類。map和lambda表示結合起來一起用會非常好。
map(func, *iterables) --> map object
iterables是可迭代的型別,序列和元組都可以。*號表示是可變引數,可以傳入多個不同值的取值。

a = [1,2,3,4,5]
def square(x):
    return x*x
r = map(square, a)
print(r)
print(list(r))


<map object at 0x000001DC6AD0B0F0>
[1, 4, 9, 16, 25]

匿名函式(lambda表示式)

lambda表示式也叫做匿名函式。
lambda的定義:lambda parameter_list: expression
expression意思是表示式,所以後面只能是表示式,不能是語句,比如賦值語句等是不可以的。
lambda表示式最後返回的是表示式計算出的值,和return後是一樣的。

def add(x, y):
    return x+y

lambda x, y : x + y

lambda表示式一般和三元表示式和map連線在一起用會更加整潔。

x = 1,2,3,4,5,6
y = 1,2,3,4,5,6
r = map(lambda x,y:x+y, x,y)
print(tuple(r))


(2, 4, 6, 8, 10, 12)

reduce函式

reduce 是一個函式。
def reduce(function, sequence, initial=None)

  • function : 這裡需要注意函式必須且只能有兩個引數
  • sequence: 序列
  • initial: 初始值
  • reduce的含義是連續呼叫函式進行連續計算
from functools import reduce # 需要引入這個函式才可以使用
list_x = [`1`,`2`,`3`]
r = reduce(lambda x,y:x+y, list_x, `a`)
print(r)

a123
# 執行情況 :((`a`+`1`)+`2`)+`3`
# 每次執行完一次函式的結果在下一次呼叫函式的時候會傳入到函式的引數中進行計算。初始值是給出的開始的引數之一。

filter類

class filter(function or None, iterable)
表示過濾不符合條件的值。當函式返回為True時保留,False時剔除。當函式為None時,剔除調iterable中本來就為False的值

# ord()返回ascII碼值

list_x = [`a`, `B`, `c`, `D`]
r = filter(lambda x: False if ord(x)>64 and ord(x)<91 else True, list_x)
print(list(r))


[`a`, `c`]

裝飾器

編寫程式碼一個原則是:對修改是封閉的,對擴充是開放的。
如果想在很多個函式裡,每個函式都實現相同的功能,用裝飾器是最方便的,不用在每個函式裡重複定義,而且呼叫起來很方便,和“裝飾”的意思很像。

import time  

def decorator(func):
    def wrapper(*args, **kw): # *args是可變引數,**kw是可變關鍵字引數,這樣可以接受除了有預設引數型別以外所有型別的函式引數
        print(time.time())
        func(*args, **kw)
    return wrapper
 # 就是一個閉包,傳入的函式func是環境變數

@decorator  # @ 是一個語法糖
def f1(func_name):  
    print("this is f1" + func_name)

@decorator
def f2(func_name1, func_name2):
    print("this is f2" + func_name1 + func_name2)

@decorator
def f3(func_name1, func_name2=`f3`,*args, **kw):
    print("this is f3"+func_name1+func_name2)
    print(kw)
    
f1(`f1`)   # 可見雖然在定義的時候麻煩了一些,但是呼叫的時候很方便。
f2(`f2`,`f2`)
f3(`f3`,`f3`,a=`1`,b=`2`,c=`3`) # 可變關鍵字引數


1519276076.973657  #時間戳
this is f1f1
1519276076.9746575
this is f2f2f2
1519276076.9746575
this is f3f3f3
{`a`: `1`, `b`: `2`, `c`: `3`}

生成器

通過列表生成式可以直接建立一個列表,但是我們如果只想訪問前面幾個元素,不想利用後面的元素,我們可以定義一個生成器(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 0x104feab40>

如果需要列印出來元素,可以通過生成器的next()方法。

g.next()
0
g.next()
1
g.next()
4

generator是可以迭代的:

g = (x * x for x in range(10))
for n in g:
...     print n
...
0
1
4
9

可以把一個函式寫成生成器,把return改為yield

def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        a, b = b, a + b
        n = n + 1
        
fib(6)
<generator object fib at 0x104feaaa0>

for n in fib(6):
...     print n
...
1
1
2
3
5
8

定義成生成器後,generator的執行流程和函式並不一樣,函式是順序執行,遇到return語句或者最後一行函式語句就返回。而變成generator的函式,在每次呼叫next()的時候執行,遇到yield語句返回,再次執行時從上次返回的yield語句處繼續執行。

另外一個例子:

 def odd():
...     print `step 1`
...     yield 1
...     print `step 2`
...     yield 3
...     print `step 3`
...     yield 5
...
o = odd()
o.next()
step 1
1
o.next()
step 2
3
o.next()
step 3
5
o.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

字典來代替swich

Python中沒有swich這種結構,可以用字典來實現。

day = 0

def get_monday():
    return `Monday`

def get_Tuseday():
    return `Tuseday`

def get_Wednesday():
    return `Wednesday`

def default():
    return `Unknow`

#返回函式可以定義跟多操作,也可以直接返回值
swicher = {
    0: get_monday,
    1: get_Tuseday,
    2: get_Wednesday
}

# 字典的get方法可以獲取鍵對應的值,如果鍵不存在則返回指定的預設值

day_time = swicher.get(day, default())() # 後面再加一個括號來呼叫函式

print(day_time)


monday

列表推導式

對於列表推導式,其實不只是列表,也可以是元組,集合,字典進行推倒

a = [1,2,3,4,5]

b = [i**2 for i in a if i>=3]

print(b)


[9,16,25]
---------------------------------------------------

# 對於字典可以這樣:

sdict = {
    `q`:`烈焰衝擊`,
    `w`:`天女散花`,
    `e`:`致命一擊`
}

dic = {value:key for key,value in sdict.items()} # 最外面加花括號就是字典或者集合
dic1 = [key for key,value in sdict.items()] 
dic2 = (key for key,value in sdict.items()) 
print(dic)
print(dic1)
print(dic2)

# 如果是元組,元組是不可遍歷的物件,所以需要下面這樣取出物件
for i in dic2:
    print(i,end=`\ `)


{`烈焰衝擊`: `q`, `天女散花`: `w`, `致命一擊`: `e`}
[`q`, `w`, `e`]
<generator object <genexpr> at 0x0000021A3430E150>
q w e

None

None代表空,並不是False,也不是[],“

print(type(None))
print(type(False))
print(type([]))
print(type(``))

<class `NoneType`>
<class `bool`>
<class `list`>
<class `str`>

我們可以看見,這些在本質上都是不一樣的,型別都不一樣,只不過我們在進行邏輯判斷的時候,有時會像下面這樣做:


a = None/false/[]/``
if not a:
.....

# 判斷的時候會這樣做

不建議使用if a is None:這種語句,我們可以看到型別是不一樣的,有時會出錯。

物件存在不一定是True

在上面對None的分析中,在邏輯判斷的時候之所以可以判斷None為False,是因為每個物件和bool型別之間都是有聯絡的,所以可以進行邏輯判斷。但是我們自定義的物件卻不一定了,返回True和False因不同的物件而不同。我們自定義的物件,如類,和我們的內建方法有關係。
類中有兩個內建方法會影響對類的布林型別的取值:

  • __len__:這個內建方法返回的是類的長度,外部呼叫len()時會返回該方法的返回值,返回值只有布林型別或者int型別。
  • __bool__:這個內建方法返回的類的bool型別的取值,當這個方法在類裡面定義以後,返回值只看其返回值,__len__的返回值不再起作用。注意該方法的返回值只能是布林型別,即True或False。

class Rest():
    def __len__(self):
        return 5
    def __bool__(self):
        return False

print(len(Rest()))
print(bool(Rest()))

5
False

裝飾器的副作用

加上裝飾器後會改變函式的名字。

def decorator(func):
    def wrapper():
        print(`this is decorator`)
        func()
    return wrapper
@decorator
def f1():
    print(f1.__name__)  # 列印函式名字,不加裝飾器是f1
f1()


this is decorator
wrapper

可見會出錯,加上裝飾器後,如果不想改變名字,可以這樣做:

from functools import wraps


def decorator(func):
    @wraps(func)
    def wrapper():
        print(`this is decorator`)
        func()
    return wrapper

@decorator
def f1():
    print(f1.__name__)  # 列印函式名字,不加裝飾器是f1

f1()



this is decorator
f1

可雜湊(hashable)物件和不可變性(immutable)

  • 可雜湊(hashable)和不可改變性(immutable)

如果一個物件在自己的生命週期中有一雜湊值(hash value)是不可改變的,那麼它就是可雜湊的(hashable)的,因為這些資料結構內建了雜湊值,每個可雜湊的物件都內建了__hash__方法,所以可雜湊的物件可以通過雜湊值進行對比,也可以作為字典的鍵值和作為set函式的引數,但是list是unhashable,所以不可以作為字典的鍵值。所有python中所有不可改變的的物件(imutable objects)都是可雜湊的,比如字串,元組,也就是說可改變的容器如字典,列表不可雜湊(unhashable)。我們使用者所定義的類的例項物件預設是可雜湊的(hashable),它們都是唯一的,而hash值也就是它們的id()

  • 雜湊

它是一個將大體量資料轉化為很小資料的過程,甚至可以僅僅是一個數字,以便我們可以用在固定的時間複雜度下查詢它,所以,雜湊對高效的演算法和資料結構很重要。

  • 不可改變性

它指一些物件在被建立之後不會因為某些方式改變,特別是針對任何可以改變雜湊物件的雜湊值的方式

  • 聯絡

因為雜湊鍵一定是不可改變的,所以它們對應的雜湊值也不改變。如果允許它們改變,,那麼它們在資料結構如雜湊表中的儲存位置也會改變,因此會與雜湊的概念違背,效率會大打折扣

具體的我們可以參考官方文件和以下部落格:
關於python內建__eq__函式的探索
關於可雜湊物件的理解

python的深拷貝和淺拷貝

1.賦值方法:

list1 = [1,2,3]
list2 = list1
print(id(list1),id(list2))

2577180416904 2577180416904

可見這和在c語言中不一樣,二者的id是一樣的,換句話說,你改變 list2,同時也會改變 list1
2.淺拷貝:

import copy

list1 = [1,2,3]
list2 = copy.copy(list1)
print(id(list1),id(list2))
list2.append(4)
print(list1,list2)


2522465131400 2522465130824
[1, 2, 3] [1, 2, 3, 4]

但是淺拷貝,對於裡面的元素,如果是不可變型別,會直接拷貝,但是對於可變型別只是指向它而已,例如看下面的程式碼:

import copy

list1 = [1,2,[1,2,3]]
list2 = copy.copy(list1)
print(id(list1),id(list2))

list2[2].append(4)

print(list1,list2)

2710366738760 2710368236680
[1, 2, [1, 2, 3, 4]] [1, 2, [1, 2, 3, 4]] # 都變化了

3.深拷貝

import copy


list1 = [1,2,[1,2,3]]
list2 = copy.deepcopy(list1)
print(id(list1),id(list2))

list2[2].append(4)

print(list1,list2)

1660389185864 1660390683784
[1, 2, [1, 2, 3]] [1, 2, [1, 2, 3, 4]]

可見深拷貝才是真正的拷貝。

相關文章