天天看點

溫故而知新--day2

溫故而知新--day2

類是一個抽象的概念,是指對現實生活中一類具有共同特征的事物的抽象。其實列化後稱為對象。類裡面由類屬性組成,類屬性可以分為資料屬性和函數屬性(函數屬性又稱為類方法)。舉個例子,人類是一各抽象的概念這就相當于一個類,我們的一個個的人,就是人類這個類執行個體化後的對象,人可以有錢這個具體的變量,這可以稱為資料屬性,而錢的多少可以是由父輩繼承過來的,也可以靠自己的奮鬥組成;一些動作如吃飯,可以是為視為函數屬性或者類方法。

python中的類屬性就是在類中定義的變量,它可以跟随這類為執行個體化後生成的對象提供資料。

不過類屬性并不是不可被改變的,它可以通過指派的方式改變。

對于某些情況,我們的每個對象賦予不同的屬性,這就需要使用一個特殊的方法:​<code>​__init__()​</code>​,這個函數可以視為構造函數。

上面說過類方法實質上就是一個個函數,不過這些函數大多要把第一的參數值設為​<code>​self​</code>​(這是一個約定俗成的規則,也可以用其他的名字),表示的是執行個體化後的對象本身。後面的參數可以和其他函數一樣設定。

上面的例子中,我定義可一個類,裡面由name屬性和eat方法,執行個體化後,又調用了eat方法。需要注意的是,我們調用方法的時侯并沒有 傳入self參數,這是因為python在執行過程中會自動傳入把對象本身傳入到self中,使用​<code>​classmethod​</code>​裝飾器時的cls也是一樣的道理。

面向對象

面向對象程式設計——Object Oriented Programming,簡稱OOP,是一種程式設計思想。OOP把對象作為程式的基本單元,一個對象包含了資料和操作資料的函數。

面向過程的程式設計把計算機程式視為一系列的指令集合,即一組函數的順序執行。為了簡化程式設計,面向過程把函數繼續切分為子函數,即把大塊函數通過切割成小塊函數來降低系統的複雜度。

而面向對象的程式設計把計算機程式視為一組對象的集合,而每個對象都可以接收其他對象發過來的消息,并處理這些消息,計算機程式的執行就是一系列消息在各個對象之間傳遞。

在Python中,所有資料類型都可以視為對象,當然也可以自定義對象。自定義的對象資料類型就是面向對象中的類(Class)的概念。在學習面向對象這個程式設計範式的過程中,有三個名詞我們必須了解其概念并熟悉如何運用,那就是封裝、繼承、多态。

封裝就類似于把類當作一個箱子,把資料屬性和函數屬性裝在一起,在使用的這個箱子的過程中,我們隻需要根據箱子外面開的洞,把東西從外面放進去或把從裡面拿出來。也就是說封裝的本質時為了區分内外。

實作封裝的方法:

雙下劃線:__

<code>class Dog: __name = "Dog" d = Dog() print(d.__name) # AttributeError: 'Dog' object has no attribute '__name' print(d._Dog__name)</code>

這種方法,雖然可以不然使用者直接通過字段名通路,但是可以通過._類名__字段名

的方式通路,通路起來較為複雜。

單下劃線:_

<code>class Dog: _name = "Dog" d = Dog() print(d._name) # Dog</code>

這是python程式員約定俗成的命名方式,表示此字段不允許外部通路。

在實際開發中,我們并不建議将屬性設定為私有的,因為這會導緻子類無法通路。但假如直接暴露個使用者的話,也不太合适,是以建議使用裝飾器:

property

setter

顧名思義,繼承是指在使用自己沒有的屬性時,可以尋找父類的屬性,找得到就用,找不到就報錯;舉個例子,繼承有點像現實中的繼承财産,假如你沒有錢可以使用從父輩那裡繼承過來的财産使用,但不同的是:假如你已經掙了錢,那麼就不能直接從父輩中拿錢過來用了。是以,繼承是需要父子關系的,其中一個父類可以有多個子類,而一個子類也可以有多個父類。

例子中的​<code>​super()​</code>​是子類調用父類的方法,其格式是:super().方法(參數)。

關于繼承的順序

假如一個類有多個父類,那麼它是如何選擇從哪個父類開始找的呢?

上述例子,B和C繼承A,D繼承B和C。在Python中的繼承順序有兩種:

深度優先

廣度優先

溫故而知新--day2
python2 經典類是按深度優先來繼承的,新式類是按廣度優先來繼承的。 python3 統一按廣度優先來繼承的。 另外python3不再區分經典類和新式類,即寫的類不繼承object也是新式類。

子類在繼承了父類的方法後,可以對父類已有的方法給出新的實作版本,這個動作稱之為方法重寫。通過方法重寫我們可以讓父類的同一個行為在子類中擁有不同的實作版本,當我們調用這個經過子類重寫的方法時,不同的子類對象會表現出不同的行為,這個就是多态。比如貓和狗都屬于生物,它們都有叫這個方法,但是他們叫之後的效果是不一樣的,這種現象就類似于多态。

類常用的雙下劃線方法

如果方法名前如果有兩個下劃線,則表示該成員是私有成員,私有成員隻能由類内部調用。python的類中就有這些雙下劃線方法,我們在定義自己的類的同時,可以對其進行重寫,以達到自己想要的效果。

表示類的描述資訊

module 表示目前操作的對象在那個子產品

class 表示目前操作的對象的類是什麼

構造函數,執行個體化時自動執行,接收對象,禁止傳回任何值,更多見​<code>​__new__​</code>​。

析構方法,當對象在記憶體中被釋放時,自動觸發執行。由解釋器進性垃圾回收時自動觸發,無需我們自定義。

典型的應用場景:

建立資料庫類,用該類執行個體化出資料庫連結對象,對象本身是存放于使用者空間記憶體中,而連結則是由作業系統管理的,存放于核心空間記憶體中

當程式結束時,python隻會回收自己的記憶體空間,即使用者态記憶體,而作業系統的資源則沒有被回收,這就需要我們定制__del__,在對象被删除前向作業系統發起關閉資料庫連結的系統調用,回收資源

為對象加括号時觸發。

dict 以字典的形式存儲類或對象的所有成員

slots 在元組中存儲對象的屬性,可以大大減少記憶體,該屬性不可被繼承

雖然__slots__可以限制程式員新增對象屬性,但是可以通過在__slots__中添加__dict__解決。可我們使用__slots__的主要目的是替換以散清單形式存儲資料的字典,降低所需的記憶體,是以如何使用還看個人的選擇。另外假如要把執行個體作為弱引用的目标,可以加入__weakref__屬性。

str 将python中的對象轉換為字元串, 主要面向使用者,其目的是可讀性

repr 将python中的對象轉換為字元串, 面向的是python解釋器,或者說開發人員,其目的是準确性

使用順序:

print 函數調用:

沒有說明用str或repr時,優先__str__

指定為str沒有__str__時,使用__repr__

指定為repr沒有__repr__時,傳回對象記憶體位址

指令行終端調用:

有__repr__傳回__repr__,無則傳回對象記憶體位址

使用指令行運作,​<code>​python -i cls.py​</code>​ (cls.py是代碼所在檔案)

可以自定義格式化,使用​<code>​format()​</code>​方法調用。

__getattr__

通路屬性(方法或字段)不存在時觸發,即抛出AttributeError時觸發

__getattribute__

通路屬性時,不管存不存在都觸發,不存在時抛出AttributeError,是以其先于__getattr__

__setattr__

設定屬性(增/改)時觸發(可以放入__dict__)

__delattr__

删除屬性時觸發

注意: 重寫這幾個方法(包括下面的__xxxitem__方法),特别容易造成無限遞歸,注意這裡操作的是​<code>​__dict__​</code>​,而不是通過​<code>​.​</code>​的方式。

例子,注意如何操作__dict__,以及這幾種方法什麼時候執行。

__getitem__

用于索引操作:擷取

__setitem__

用于索引操作:設定

__delitem__

用于索引操作:删除

注:索引操作是指用中括号操作,用​<code>​.​</code>​操作調用的是__xxxattr__。這裡操作的也是​<code>​__dict__​</code>​

疊代器協定:對象必須提供一個next方法,執行該方法要麼傳回下一項,要麼抛出StopIteration異常,終止疊代器。

在指令行中運作:

​<code>​for​</code>​循環可以調用​<code>​__iter__​</code>​方法,将對象變為可疊代對象,同時for循環内部遇到​<code>​StopIteration​</code>​異常時,停止疊代。

為了支援for循環,我們可以為上面的類增加一個​<code>​__iter__​</code>​方法:

關于​<code>​iter​</code>​函數:iter函數可以調用​<code>​__iter__​</code>​方法,生成可疊代對象,且它有兩個參數​<code>​iter(object[, sentinel])​</code>​:

第一個參數是遵循可疊代協定或支援序列協定(有 __getitem__()

方法,且數字參數從 0 開始)的對象;

第二個參數有值的話,object 必須是可調用的對象。這種情況下生成的疊代器,每次疊代調用它的 __next__()

方法時都會不帶實參地調用 object;如果傳回的結果是 sentinel

則觸發 StopIteration

,否則傳回調用結果(這種效果可以作為哨兵使用)。

上下文管理協定,即with語句,為了讓一個對象相容with語句,必須在這個對象的類中聲明​<code>​__enter__​</code>​和​<code>​__exit__​</code>​方法。with語句開始時,上下文管理對象會調用​<code>​__enter__​</code>​方法,with語句運作結束後,調用​<code>​__exit__​</code>​方法。

優點是可以自動釋放資源,在一些需要管理資源的場景(檔案、網絡連接配接、鎖等)大有用處。

使用标準庫實作上下文管理器:

​<code>​@contextlib,contextmanager​</code>​+​<code>​yield​</code>​:在被contextmanager裝飾的生成器中,yield前的部分相當于​<code>​__enter__​</code>​,之後的部分相當于​<code>​__exit__​</code>​。例子:

**python一切皆對象。**所有的類也都是對象,那麼類是有誰産生的呢?實際上python中的類是由​<code>​type​</code>​類執行個體化産生的,而為了避免無限溯源,​<code>​type​</code>​類又是其本身的執行個體化。為了更好的了解,可以看看下面這幅圖:

溫故而知新--day2

type與object

兩幅圖都是正确的,左邊強調str、type和LineItem是object的子類,右邊強調str、object、LineItem是type的執行個體。 object和type的關系很特别:object是type的執行個體,type是object的子類。

回歸到​<code>​__new__​</code>​和​<code>​__metaclass__​</code>​上,前面在__init__裡說過:​<code>​__init__​</code>​稱為構造函數,實質上用于構造執行個體的方法是​<code>​__new__​</code>​。​<code>​__new__​</code>​是一個經過特殊處理的類方法,不必使用classmethod,它必須傳回一個執行個體,而這個執行個體會作為​<code>​__init__​</code>​的第一個參數(self),是以​<code>​__init__​</code>​實質上就是“初始化方法”,一般來說我們不需要重寫​<code>​__new__​</code>​方法,繼承object的就已經夠用了。

​<code>​__metaclass__​</code>​是用來表示該類由誰來執行個體化建立的。

反射

反射是程式可以通路、測試和修改其本身狀态或行為的一種能力,利用反射可以實作可插拔機制,可以提前定義好接口,隻有在接口實作後真正執行,提高協同開發的效率。

hasattr(obj, name)

判斷是否可以調用,即可否調用obj.name

getattr(obj, name, default=None)

通路某個屬性,有則傳回obj.name

; 無就傳回default

的内容

setattr(obj, key, val)

等同obj.key = val

, 不過val參數可以使用lambda函數

delattr(obj, key)

等同于del obj.key

例子:

裝飾器

在我們日常開發的過程中,往往不能做到面面俱到或由于當時業務不需要,是以後期需要用到某些功能時可能要修改某些代碼,但是直接寫個原函數可能造成不可挽回的損失,為了解決這種情況,我們可以使用裝飾器來維護代碼。除此之外,裝飾器經常用于有切面需求的場景,比如:插入日志、性能測試、事務處理、緩存、權限校驗等場景,它 是解決這類問題的絕佳設計。裝飾器允許向一個現有的對象添加新的功能,同時又不改變其結構。裝飾器本質上就是一個函數,其相對于高階函數+函數嵌套+閉包的組合體,三者合一成為裝飾器。

想要使用裝飾器首先要了解一些基本的概念。

文法糖@

在python中

<code>@decdef test(): pass</code>

相對于:​<code>​test = dec(test)​</code>​

裝飾器會在被裝飾函數定義完之後立即執行

<code>def dec(func): print("dec running") # dec running @dec def test(): pass</code>

可以看到,我并沒有調用test函數,dec就執行了,是以在導入子產品時需要注意裝飾器是否允許被執行。

閉包

閉包是延伸了作用域的函數,可以通路定義體之外定義的非全局變量。其主要的形式就是用一個函數嵌套另一個函數,以達到作用域延伸的效果。

<code>def make_avg(): """計算平均數""" num_list = [] def avg(new_value): num_list.append(new_value) return sum(num_list)/len(num_list) return avg a = make_avg() print(a(10)) # 10 print(a(20)) # 15 print(a(30)) # 20</code>

溫故而知新--day2

閉包示意圖

綜上,閉包是一種函數,它會保留定義函數時存在的自由變量的綁定,在調用函數時,雖定義作用域不可用,但仍然可以使用這些綁定。

前面說過,裝飾器實質上就是高階函數+函數嵌套+閉包的組合體,是以其基本的格式如下:

為了更加便于了解,下面舉一個例子,用一個裝飾器計算函數的運作時間:

需要注意的是,在日常開發過程中,我們應該把裝飾器放在其他的檔案中,在使用時可以導入到本子產品中使用。

上面計算函數運作時間這個例子有一個缺點:原函數的​<code>​__name__​</code>​和​<code>​__doc__​</code>​屬性被覆寫了,為了解決這個問題,我們可以引入​<code>​functools.wraps​</code>​把原函數相關屬性複制到裝飾器中。

如果想檢視更多關于​<code>​functools​</code>​庫的内容,可以檢視官方文檔​​functools子產品​​

靜态屬性。可以使方法在調用時不需要加括号就可以直接使用。被裝飾後不能被指派,除非使用xx.setter

再裝飾一遍這個同名方法。

<code>class Foo: def __init__(self, name, age): self._name = name self._age = age @property def name(self): return self._name @property def age(self): return self._age @age.setter def age(self, value): self._age = value f = Foo("lczmx", 18) print(f.name) # f.name = "xxx" # AttributeError: can't set attribut f.age = 20</code>

classmethod

類方法。可以直接通過類名.方法名()

調用,不需要執行個體化,也不需要創cls參數。

<code>class Foo: @classmethod def get_name(cls, data: dict): return data.get('name') data = {'name': 'test'} print(Foo.get_name(data))</code>

staticmethod

靜态方法。類似于普通的函數,第一個參數并不是self或cls,調用時可以由類調用,也可以由對象調用。

<code>class Foo: @staticmethod def get_name(data: dict): return data.get('name') data = {'name': 'test'} print(Foo.get_name(data)) f = Foo() print(f.get_name(data))</code>

functools.lru_cache一個為函數提供緩存功能的裝飾器,緩存 maxsize 組傳入參數,在下次以相同參數調用時直接傳回上一次的結果。用以節約高開銷或I/O函數的調用時間。

<code>from functools import wraps from time import time def clock(func): """計算函數運作時間""" @wraps(func) def clocked(*args, **kwargs): start = time() res = func(*args, **kwargs) end = time() func_name = func.__name__ print('{0}({1}) 執行了[{2:.8f}s]'.format( func_name, ",".join(repr(i) for i in args), end-start)) return res return clocked @clock def fib(num): """計算斐波那契數列""" return num if num &lt; 2 else (fib(num - 2) + fib(num - 1)) print(fib(5))</code>

結果

<code>fib(1) 執行了[0.00000000s] fib(0) 執行了[0.00000000s] fib(1) 執行了[0.00000000s] fib(2) 執行了[0.00099683s] fib(3) 執行了[0.00299120s] fib(0) 執行了[0.00000000s] fib(1) 執行了[0.00000000s] fib(2) 執行了[0.00101113s] fib(1) 執行了[0.00000000s] fib(0) 執行了[0.00000000s] fib(1) 執行了[0.00000000s] fib(2) 執行了[0.02294302s] fib(3) 執行了[0.02392578s] fib(4) 執行了[0.02593160s] fib(5) 執行了[0.02892280s] 5</code>

使用lru_cache後:

<code>from functools import wraps, lru_cache from time import time def clock(func): """計算函數運作時間""" @wraps(func) def clocked(*args, **kwargs): start = time() res = func(*args, **kwargs) end = time() func_name = func.__name__ print('{0}({1}) 執行了[{2:.8f}s]'.format( func_name, ",".join(repr(i) for i in args), end-start)) return res return clocked @lru_cache() @clock def fib(num): """計算斐波那契數列""" return num if num &lt; 2 else (fib(num - 2) + fib(num - 1)) print(fib(5))</code>

<code>fib(1) 執行了[0.00000000s]fib(0) 執行了[0.00000000s] fib(2) 執行了[0.00000000s] fib(3) 執行了[0.00000000s] fib(4) 執行了[0.00000000s] fib(5) 執行了[0.00098634s] 5</code>

可以發現,使用可lru_cache可以大大減少對于某些重複計算,極大地優化性能,其除了可以在遞歸算法中使用,也可以在web擷取資訊的應用中發揮作用。lru_cache有兩個參數可以配置lru_cache(maxsize=128, type=False),是以使用lru_cache時需要用像調用函數一樣使用。

maxsize 指定能存儲多少個調用的結果

typed 為True時,把不同類型的結果分開儲存,如把1和1.0區分開來。

functools.singledispatch

由于python沒有函數重載的概念,是以假如有 用一個函數處理不同的事情的情況就很難處理,最簡單的方法是使用大量的if-elif-else進性判斷。但是,這樣做會造成函數的越寫越臃腫,而且耦合度高。為此經過深思熟慮python3.4最終把singledispatch加入了标準庫,使解決這類問題可以使用子產品化的方法處理。

使用@singledispatch可以根據第一個參數的類型,以不同的方式處理資料,使原本的函數變為泛函數。

<code>from functools import singledispatch from numbers import Integral from collections import abc @singledispatch def print_typed(data): """處理object類型的基函數""" print("object (%s):" % type(data), data) @print_typed.register(str) def _(s): print("字元串:%s" % s) @print_typed.register(Integral) # Integral是int的虛拟超類 def _(num): print("數字: %d" % num) # 可以放多個 @print_typed.register(tuple) @print_typed.register(abc.MutableSequence) # list等 def _(seq): print("seq: ", seq) if __name__ == '__main__': print_typed("123") # 字元串:123 print_typed([1, 2, 3]) # seq: [1, 2, 3] print_typed({"name": "lczmx"}) # object (&lt;class 'dict'&gt;): {'name': 'lczmx'}</code>

從​<code>​functools.lru_cache​</code>​可以看出,裝飾器是可以加參數的,其具體的本質就是一個函數包裹着一個裝飾器,如

等同于​<code>​test = dec(arg=123)(test)​</code>​,下面舉個例子,優化之前計算函數運作時間的裝飾器,為其加上一個參數,可以自定義格式化輸出:

結果:

描述符

描述符是對多個屬性運用相同存取邏輯的一種方式,是實作了特定協定的類,這個協定包括:​<code>​__get__​</code>​、​<code>​__set__​</code>​、​<code>​__delete__​</code>​,property就實作了這三個,在實際開發過程中,一般隻需要實作部分即可。

根據實作的方法可以把描述符分為資料描述符和非資料描述符:

資料描述符:至少實作__set__

非資料描述符:沒有實作__set__

描述符的用法是:建立一個執行個體,作為另一個類的屬性。

下面展示​<code>​__get__​</code>​和​<code>​__set__​</code>​的一般用法,以及對應的參數設定和怎麼在其他的類中使用。

例子中有幾個要點:1. 操作的是托管類(Item)的對象的__dict__;2. 托管類中兩次定義屬性,init中的執行個體屬性在執行個體化時會委托給描述符,是以類屬性不會被替換掉。

注意事項:

一 描述符本身應該定義成新式類,被代理的類也應該是新式類

二 必須把描述符定義成這個類的類屬性,不能為定義到構造函數中

三 要嚴格遵循該優先級,優先級由高到底分别是

類屬性

資料描述符

執行個體屬性

非資料描述符

找不到的屬性觸發__getattr__()

上面這個例子可以進一步改進,把​<code>​price = Viladate("price")​</code>​換為​<code>​price = Viladate()​</code>​,即動态生成字段名。要實作這個功能,需要用到類裝飾器,它和函數裝飾器差不多,差别就是把參數的func換成類,傳回值是原來的類或者新類。

為了更加貼近真實場景,這次分别用不同的檔案定義

​<code>​./model.py​</code>​:

​<code>​./main.py​</code>​:

描述符用法總結:

要設定隻讀屬性可以使用property

用于驗證的描述符可以隻有__set__

僅有__get__

的描述符可以實作高效緩存

沒有__set__

的描述符可以被覆寫