Python視訊學習(七、Python高階)
重點回顧:
- GIL鎖是CPython直譯器的問題
- copy模組的deepcopy和copy方法對於tuple拷貝的區別
- 私有屬性的繼承問題和重整
- Python物件的__mro__ ,以及導致的 super呼叫順序,還有 類屬性解析順序
- property建立和使用
- 各種魔法屬性
- 上下文管理器的使用,返回物件是
__enter__
返回值
1. GIL鎖
1.1 多工不同情形下的CPU佔用率
使用的是htop
程式檢視的CPU佔用情況。
單執行緒死迴圈:
while True:
pass
雙執行緒死迴圈:
import threading
def doforever:
while True :
pass
t = threading.Thread(target = doforever)
t.start()
while True:
pass
雙程序死迴圈
import multiprocessing
def doforever:
while True:
pass
t = multiprocessing.Process(target = doforever)
t.start()
while True:
pass
1.2 GIL鎖的概念特點
GIL(global interpreter lock)是全域性直譯器鎖,它不是Python語言的特性,而是由於歷史原因,使得CPython直譯器在實現的時候加上的。以前guido嘗試過移除GIL鎖,但是移除並不是那麼簡單的,還要加上很多別的控制程式碼,所以最終的效能反而不如之前好
Guido的宣告:http://www.artima.com/forums/flat.jsp?forum=106&thread=214235 The language doesn’t require the GIL – it’s only the CPython virtual machine that has historically been unable to shed it.
GIL鎖的特點:
- 每個執行緒在執行的過程都需要先獲取GIL,保證同一時刻只有一個執行緒可以執行程式碼。所以就算有多個CPU,一個程式內同一時刻真正執行的執行緒數還是隻有一個。
- 執行緒釋放GIL鎖的情況: 在IO操作等可能會引起阻塞的system call之前,可以暫時釋放GIL,但在執行完畢後,必須重新獲取GIL Python 3.x使用計時器(執行時間達到閾值後,當前執行緒釋放GIL)或Python 2.x,tickets計數達到100
- 在Python中如果要真正使用全部的核心資源,應該使用多程序
- 多執行緒爬取比單執行緒效能有提升,因為遇到IO阻塞會自動釋放GIL鎖
GIL面試題如下 描述Python GIL的概念, 以及它對python多執行緒的影響?編寫一個多執行緒抓取網頁的程式,並闡明多執行緒抓取程式是否可比單執行緒效能有提升,並解釋原因。
1.3 如何避免GIL鎖問題
- 更換python直譯器(只有CPython有GIL鎖問題)
- 使用別的語言寫任務,然後用Python引入:
// libdead_loop.c
void DeadLoop(){
while(1){
;
}
}
gcc libdead_loop.c -shared -o libdead_loop.so
1.4 何時使用不同的多工模式:
因為多程序可以利用多個核心,而 多執行緒和協程,只是實現了阻塞時期的任務切換:
- 計算/CPU密集型程式:使用多程序
- IO密集型程式: 使用多執行緒、協程
2. 深淺拷貝
import copy
copy.copy
copy.deepcopy
(唯一的深拷貝方法)- 物件.copy
- 切片
import copy
a = [1,2,3]
b = copy.copy(a)
id(a)
2732216717384
id(b)
2732218239560
# 成功拷貝
import copy
a = [1,2,3]
b = [4,5,6]
c = [a,b]
d = copy.copy(c)
id(c)
2732216577864
id(d)
2732218221320
a.append(9)
c
[[1, 2, 3, 9], [4, 5, 6]]
d
[[1, 2, 3, 9], [4, 5, 6]]
# 一起受影響,所以這是 淺拷貝
import copy
a = [1,2,3]
b = [4,5,6]
c = [a,b]
d = copy.deepcopy(c)
id(c)
2732216577864
id(d)
2732217733576
a.append(8)
c
[[1, 2, 3, 9, 8], [4, 5, 6]]
d
[[1, 2, 3, 9], [4, 5, 6]]
# d不受影響,所以這是 深拷貝
拷貝元祖:
import copy
a = (1,2)
b = copy.copy(a)
id(a)
2732216884680
id(b)
2732216884680
# 對於不可變型別,因為沒有拷貝的必要,所以 copy是指向同一個元素
c = copy.deepcopy(c)
id(c)
2732216884680
# 深拷貝在拷貝只有 不可變物件的元祖時,也是使用的 直接指向
import copy
a = [1,2,3]
b = [4,5,6]
c = (a,b)
d = copy.copy(c)
e = copy.deepcopy(c)
id(c)
2732217828552
id(d)
2732217828552
id(e)
2732218154952
# 對於內部含有可變物件的元祖, deepcopy就會拷貝,而copy不拷貝
a.append(9)
c
([1, 2, 3, 9], [4, 5, 6])
d
([1, 2, 3, 9], [4, 5, 6])
e
([1, 2, 3], [4, 5, 6])
3. 私有化
幾種屬性/方法的定義名稱:
- 1.
xxxx
- 公有變數,可以繼承,子類和物件可以訪問
- 2.
_xxxx
: - 私有變數, 可以繼承,子類和物件可以訪問;
但是不推薦外部直接使用,而且無法被
from abc import xxxx
匯入 - 3.
__xxx
: - 私有變數,不會繼承,子類只能通過父類的公有方法來訪問,而且有名字重整
- 4.
__xxx__
: - 魔法屬性和方法,可以繼承。 比如父類自定義的
__init__
方法子類也會有,而且會預設呼叫父類的__init__
- 5.
xx_
: - 單下劃線後置,為的是避免和Python關鍵字的衝突,不推薦使用
私有屬性和名字重整
帶有__xxx
形式的屬性/方法,只有在類中自己新增的時候,才會名字重整。之後自己新增的不是私有屬性.
class A(object):
def __init__(self,name):
self.__name = name
def __hidden(self):
return self.__name
a = A("Miller")
print(a.__dict__)
# 名字重整的結果
# {'_A__name': 'Miller'}
print(A.__dict__)
# {'__module__': '__main__', '__init__': <function A.__init__ at 0x000001D5CA59FAE8>, '_A__hidden': <function A.__hidden at 0x000001D5CA59FA60>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
# 也有名字重整的 __hidden方法
# print(a.__name) 報錯
a.__age= 10
print(a.__age)
# 10
# 這個就不是隱藏屬性了
自動呼叫父類構造方法:
class A(object):
def __init__(self):
print("哈哈")
class B(A):
pass
b = B()
# 哈哈
# 自動呼叫了父親的構造方法
繼承父類構造方法
class A(object):
def __init__(self,name,age):
print("哈哈")
class B(A):
pass
b = B()
# TypeError: __init__() missing 2 required positional arguments: 'name' and 'age'
# 報錯,所以說子類繼承了父類的構造方法
重寫父類方法
class A(object):
def __init__(self,name,age):
print("哈哈")
class B(A):
def __init__(self):
print("你好")
b = B()
# 你好
# 子類重寫了父類方法
呼叫多個父類中的哪個方法
class A(object):
def __init__(self):
# self._name = name
print("哈哈")
class B(object):
def __init__(self):
print("嘿嘿")
class C(B,A):
pass
c = C()
# 嘿嘿
# 繼承的時候,誰寫在前面,就呼叫哪個父類的方法
4. 匯入模組的問題
4.1 重新匯入模組
from imp import reload
reload(模組名) # 注意,這個模組一定要之前匯入過,才能重新匯入
應用:可以在執行時修改程式碼,然後讓程式重新帶入新程式碼內容
4.2 多模組開發的問題
開發時經常多模組,公用的內容放在一個模組中:
# common
FLAG = True
MYLIST = []
然後另外2個模組,
- 無論是
import common
還是from common import MYLIST
, 他們指向的都是同一份MYLIST
- 如果是
import common
,那麼common.FLAG
指向的是同一個物件 - 如果是
from common import FLAG
,那麼每個模組中維護的FLAG物件不是同一份
5. 再談python物件
5.1 __class__
每個物件都有一個__class__
,能夠獲取他的類物件,從可以呼叫類的屬性:
class A(object):
count = 10
def __init__(self,name):
self.name = name
a = A("哈哈")
print(a.__class__)
print(a.__class__.count)
print(A.__class__)
# <class '__main__.A'>
# 10
# <class 'type'>
- 類屬性在記憶體中只儲存一份
- 例項屬性在每個物件中都要儲存一份
5.2 __dict__
__dict__
屬性內部是物件的 屬性鍵值對
print(a.__dict__)
# {'name': '哈哈'}
5.3 ★多繼承的MRO
在多繼承中,如果直接使用Parent.__init__(args)
的方法,會導致交叉的祖先類被多次呼叫構造方法:
class Parent(object):
def __init__(self, name):
print('parent的init開始被呼叫')
self.name = name
print('parent的init結束被呼叫')
class Son1(Parent):
def __init__(self, name, age):
print('Son1的init開始被呼叫')
self.age = age
Parent.__init__(self, name)
print('Son1的init結束被呼叫')
class Son2(Parent):
def __init__(self, name, gender):
print('Son2的init開始被呼叫')
self.gender = gender
Parent.__init__(self, name)
print('Son2的init結束被呼叫')
class Grandson(Son1, Son2):
def __init__(self, name, age, gender):
print('Grandson的init開始被呼叫')
Son1.__init__(self, name, age) # 單獨呼叫父類的初始化方法
Son2.__init__(self, name, gender)
print('Grandson的init結束被呼叫')
gs = Grandson('grandson', 12, '男')
print('姓名:', gs.name)
print('年齡:', gs.age)
print('性別:', gs.gender)
# 執行結果:
Grandson的init開始被呼叫
Son1的init開始被呼叫
parent的init開始被呼叫
parent的init結束被呼叫
Son1的init結束被呼叫
Son2的init開始被呼叫
parent的init開始被呼叫
parent的init結束被呼叫
Son2的init結束被呼叫
Grandson的init結束被呼叫
姓名: grandson
年齡: 12
性別: 男
使用super
呼叫父類方法
super().__init__()
super(類名, self).__init__()
每個類都有一個__mro__
,是Python直譯器使用C3演算法計算的,顯示的就是找方法的順序。
- 類中呼叫
super()
,實際上是在__mro__
顯示的元祖列表中去匹配,如果匹配到了型別,則呼叫的列表中的下一個型別的方法 - 類引用類屬性和類方法時,也是根據
__mro__
中的順序去查詢的:
class Parent(object):
count = 10
class Child(Parent):
pass
print(Child.count)
c = Child()
print(c.count)
# 10
# 10
#因為是根據 __mro__找到了父類Parent
class Parent(object):
def __init__(self, name, *args, **kwargs): # 為避免多繼承報錯,使用不定長引數,接受引數
print('parent的init開始被呼叫')
self.name = name
print('parent的init結束被呼叫')
class Son1(Parent):
def __init__(self, name, age, *args, **kwargs): # 為避免多繼承報錯,使用不定長引數,接受引數
print('Son1的init開始被呼叫')
self.age = age
super().__init__(name, *args, **kwargs) # 為避免多繼承報錯,使用不定長引數,接受引數
print('Son1的init結束被呼叫')
class Son2(Parent):
def __init__(self, name, gender, *args, **kwargs): # 為避免多繼承報錯,使用不定長引數,接受引數
print('Son2的init開始被呼叫')
self.gender = gender
super().__init__(name, *args, **kwargs) # 為避免多繼承報錯,使用不定長引數,接受引數
print('Son2的init結束被呼叫')
class Grandson(Son1, Son2):
def __init__(self, name, age, gender):
print('Grandson的init開始被呼叫')
# 多繼承時,相對於使用類名.__init__方法,要把每個父類全部寫一遍
# 而super只用一句話,執行了全部父類的方法,這也是為何多繼承需要全部傳參的一個原因
# super(Grandson, self).__init__(name, age, gender)
super().__init__(name, age, gender)
print('Grandson的init結束被呼叫')
print(Grandson.__mro__)
gs = Grandson('grandson', 12, '男')
print('姓名:', gs.name)
print('年齡:', gs.age)
print('性別:', gs.gender)
- 還能使用
super(類名, self).方法
, 這個結果是在__mro__
中根據這個傳入的類名去查詢,然後呼叫匹配順序的下一個類的方法。 所以super也能夠靈活的呼叫 繼承鏈中的方法
總結:
- super().__init__相對於類名.init,在單繼承上用法基本無差
- 但在多繼承上有區別,super方法能保證每個父類的方法只會執行一次,而使用類名的方3. 會導致方法被執行多次,具體看前面的輸出結果
- 多繼承時,使用super方法,對父類的傳引數,應該是由於python中super的演算法導致的原因,必須把引數全部傳遞,否則會報錯
- 單繼承時,使用super方法,則不能全部傳遞,只能傳父類方法所需的引數,否則會報錯
- 多繼承時,相對於使用類名.__init__方法,要把每個父類全部寫一遍, 而使用super方法,只需寫一句話便執行了全部父類的方法,這也是為何多繼承需要全部傳參的一個原因
6. ★property屬性
property能夠像屬性一樣的呼叫方法,比較像別的語言中的getter與setter。
class Foo:
def func(self):
pass
# 定義property屬性
@property
def prop(self): # 這個地方必須只寫一個self引數,不能多不能少,多了就報錯
pass
# ############### 呼叫 ###############
foo_obj = Foo()
foo_obj.func() # 呼叫例項方法
foo_obj.prop # 呼叫property屬性
注意:
- 定義 時只有一個
self
引數,不多不少 - 呼叫時不用加括號
@property 這種的,都叫裝飾器
另一個例子
class Pager:
def __init__(self, current_page):
# 使用者當前請求的頁碼(第一頁、第二頁...)
self.current_page = current_page
# 每頁預設顯示10條資料
self.per_items = 10
@property
def start(self):
val = (self.current_page - 1) * self.per_items
return val
@property
def end(self):
val = self.current_page * self.per_items
return val
# ############### 呼叫 ###############
p = Pager(1)
p.start # 就是起始值,即:m
p.end # 就是結束值,即:n
6.1 用裝飾器建立
經典類中只有一種@property
,而新式類有3種:
@property
@屬性名.setter
@屬性名.deleter
class Goods:
"""python3中預設繼承object類
以python2、3執行此程式的結果不同,因為只有在python3中才有@xxx.setter @xxx.deleter
"""
@property
def price(self):
print('@property')
@price.setter
def price(self, value):
print('@price.setter')
@price.deleter
def price(self):
print('@price.deleter')
# ############### 呼叫 ###############
obj = Goods()
obj.price # 自動執行 @property 修飾的 price 方法,並獲取方法的返回值
obj.price = 123 # 自動執行 @price.setter 修飾的 price 方法,並將 123 賦值給方法的引數
del obj.price # 自動執行 @price.deleter 修飾的 price 方法
- 經典類中的屬性只有一種訪問方式,其對應被 @property 修飾的方法
- 新式類中的屬性有三種訪問方式,並分別對應了三個被@property、@方法名.setter、@方法名.deleter修飾的方法
6.2 使用類方法建立
新式類和經典類使用這種方法是一樣的
步驟:
- 先定義好每個方法: getter, setter, deleter
- 最後在類的內部用
property
方法呼叫傳參,得到的返回值即為屬性
class Foo(object):
def get_bar(self):
print("getter...")
return 'laowang'
def set_bar(self, value):
"""必須兩個引數"""
print("setter...")
return 'set value' + value
def del_bar(self):
print("deleter...")
return 'laowang'
BAR = property(get_bar, set_bar, del_bar, "description...")
obj = Foo()
obj.BAR # 自動呼叫第一個引數中定義的方法:get_bar
obj.BAR = "alex" # 自動呼叫第二個引數中定義的方法:set_bar方法,並將“alex”當作引數傳入
desc = Foo.BAR.__doc__ # 自動獲取第四個引數中設定的值:description...
print(desc)
del obj.BAR # 自動呼叫第三個引數中定義的方法:del_bar方法
property方法的引數:
- 第一個引數是方法名,呼叫 物件.屬性 時自動觸發執行方法
- 第二個引數是方法名,呼叫 物件.屬性 = XXX 時自動觸發執行方法
- 第三個引數是方法名,呼叫 del 物件.屬性 時自動觸發執行方法
- 第四個引數是字串,呼叫 物件.屬性.doc ,此引數是該屬性的描述資訊
django中常用property
property的應用——當作getter和setter:
class Money(object):
def __init__(self):
self.__money = 0
# 使用裝飾器對money進行裝飾,那麼會自動新增一個叫money的屬性,當呼叫獲取money的值時,呼叫裝飾的方法
@property
def money(self):
return self.__money
# 使用裝飾器對money進行裝飾,當對money設定值時,呼叫裝飾的方法
@money.setter
def money(self